diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 10924aaa18785..7d83c0851eb8c 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -90,10 +90,13 @@ disabled: - x-pack/test_serverless/functional/test_suites/common/config.ts - x-pack/test_serverless/functional/test_suites/observability/config.ts - x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts + - x-pack/test_serverless/functional/test_suites/observability/config.examples.ts - x-pack/test_serverless/functional/test_suites/search/config.ts - x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts + - x-pack/test_serverless/functional/test_suites/search/config.examples.ts - x-pack/test_serverless/functional/test_suites/security/config.ts - x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts + - x-pack/test_serverless/functional/test_suites/security/config.examples.ts defaultQueue: 'n2-4-spot' enabled: @@ -279,7 +282,6 @@ enabled: - x-pack/test/functional/apps/data_views/config.ts - x-pack/test/functional/apps/dev_tools/config.ts - x-pack/test/functional/apps/discover/config.ts - - x-pack/test/functional/apps/discover_log_explorer/config.ts - x-pack/test/functional/apps/graph/config.ts - x-pack/test/functional/apps/grok_debugger/config.ts - x-pack/test/functional/apps/home/config.ts @@ -313,6 +315,7 @@ enabled: - x-pack/test/functional/apps/ml/short_tests/config.ts - x-pack/test/functional/apps/ml/stack_management_jobs/config.ts - x-pack/test/functional/apps/monitoring/config.ts + - x-pack/test/functional/apps/observability_log_explorer/config.ts - x-pack/test/functional/apps/remote_clusters/config.ts - x-pack/test/functional/apps/reporting_management/config.ts - x-pack/test/functional/apps/rollup_job/config.ts diff --git a/.buildkite/hooks/post-command b/.buildkite/hooks/post-command index d9a8ad668da0b..13e0f4d795828 100755 --- a/.buildkite/hooks/post-command +++ b/.buildkite/hooks/post-command @@ -1,3 +1,7 @@ #!/usr/bin/env bash -.buildkite/scripts/lifecycle/post_command.sh +if [[ "$BUILDKITE_AGENT_NAME" =~ ^bk-agent ]]; then + echo "Pipeline file triggered from outside the kibana executors, skipping post_command" +else + .buildkite/scripts/lifecycle/post_command.sh +fi diff --git a/.buildkite/hooks/pre-command b/.buildkite/hooks/pre-command index 58a2c5f0b499d..95e2975d094c3 100644 --- a/.buildkite/hooks/pre-command +++ b/.buildkite/hooks/pre-command @@ -1,3 +1,7 @@ #!/usr/bin/env bash -source .buildkite/scripts/lifecycle/pre_command.sh +if [[ "$BUILDKITE_AGENT_NAME" =~ ^bk-agent ]]; then + echo "Pipeline file triggered from outside the kibana executors, skipping pre_command" +else + source .buildkite/scripts/lifecycle/pre_command.sh +fi diff --git a/.buildkite/pipelines/on_merge_unsupported_ftrs.yml b/.buildkite/pipelines/on_merge_unsupported_ftrs.yml index 30e7929fd6e19..5ee55e63c1408 100644 --- a/.buildkite/pipelines/on_merge_unsupported_ftrs.yml +++ b/.buildkite/pipelines/on_merge_unsupported_ftrs.yml @@ -96,8 +96,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" - command: .buildkite/scripts/steps/functional/osquery_cypress.sh label: 'Osquery Cypress Tests' @@ -110,8 +108,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-osquery/**/*" - command: .buildkite/scripts/steps/functional/synthetics_plugin.sh label: 'Synthetics @elastic/synthetics Tests' diff --git a/.buildkite/pipelines/pipeline.kibana-serverless-release.yaml b/.buildkite/pipelines/pipeline.kibana-serverless-release.yaml new file mode 100644 index 0000000000000..0eec18471e2be --- /dev/null +++ b/.buildkite/pipelines/pipeline.kibana-serverless-release.yaml @@ -0,0 +1,12 @@ +steps: + - label: ":releasethekraken: Release kibana" + # https://regex101.com/r/tY52jo/1 + if: build.tag =~ /^deploy@\d+\$/ + trigger: gpctl-promote-with-e2e-tests + build: + env: + SERVICE_COMMIT_HASH: "${BUILDKITE_COMMIT:0:12}" + REMOTE_SERVICE_CONFIG: https://raw.githubusercontent.com/elastic/serverless-gitops/main/gen/gpctl/kibana/config.yaml + SERVICE: kibana-controller + NAMESPACE: kibana-ci + IMAGE_NAME: kibana-serverless diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index bb96479a2b83a..e36a1434bee88 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -69,6 +69,19 @@ steps: - exit_status: '-1' limit: 3 + - command: SERVERLESS_ENVIRONMENT=observability.examples .buildkite/scripts/steps/functional/serverless_ftr.sh + label: 'Serverless Observability Examples Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 40 + soft_fail: + - exit_status: 10 + retry: + automatic: + - exit_status: '-1' + limit: 3 + - command: SERVERLESS_ENVIRONMENT=search .buildkite/scripts/steps/functional/serverless_ftr.sh label: 'Serverless Search Tests' agents: @@ -82,6 +95,19 @@ steps: - exit_status: '-1' limit: 3 + - command: SERVERLESS_ENVIRONMENT=search.examples .buildkite/scripts/steps/functional/serverless_ftr.sh + label: 'Serverless Search Examples Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 40 + soft_fail: + - exit_status: 10 + retry: + automatic: + - exit_status: '-1' + limit: 3 + - command: SERVERLESS_ENVIRONMENT=security .buildkite/scripts/steps/functional/serverless_ftr.sh label: 'Serverless Security Tests' agents: @@ -95,34 +121,44 @@ steps: - exit_status: '-1' limit: 3 - - command: .buildkite/scripts/steps/functional/security_serverless.sh - label: 'Serverless Security Cypress Tests' + - command: SERVERLESS_ENVIRONMENT=security.examples .buildkite/scripts/steps/functional/serverless_ftr.sh + label: 'Serverless Security Examples Tests' agents: queue: n2-4-spot depends_on: build timeout_in_minutes: 40 - parallelism: 16 - soft_fail: true + soft_fail: + - exit_status: 10 retry: automatic: - - exit_status: '*' - limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" + - exit_status: '-1' + limit: 3 - - command: .buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh - label: 'Serverless Security Defend Workflows Cypress Tests' + - command: .buildkite/scripts/steps/functional/security_serverless.sh + label: 'Serverless Security Cypress Tests' agents: queue: n2-4-spot depends_on: build timeout_in_minutes: 40 + parallelism: 12 soft_fail: true retry: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" + + # status_exception: Native role management is not enabled in this Elasticsearch instance + # - command: .buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh + # label: 'Serverless Security Defend Workflows Cypress Tests' + # agents: + # queue: n2-4-spot + # depends_on: build + # timeout_in_minutes: 40 + # soft_fail: true + # retry: + # automatic: + # - exit_status: '*' + # limit: 1 - command: .buildkite/scripts/steps/functional/security_serverless_investigations.sh label: 'Serverless Security Investigations Cypress Tests' @@ -136,8 +172,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" - command: .buildkite/scripts/steps/functional/security_serverless_explore.sh label: 'Serverless Security Explore Cypress Tests' @@ -151,8 +185,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" - command: .buildkite/scripts/steps/lint.sh label: 'Linting' diff --git a/.buildkite/pipelines/pull_request/defend_workflows.yml b/.buildkite/pipelines/pull_request/defend_workflows.yml index 953cc3ab971fa..00f701c4e16d9 100644 --- a/.buildkite/pipelines/pull_request/defend_workflows.yml +++ b/.buildkite/pipelines/pull_request/defend_workflows.yml @@ -4,25 +4,21 @@ steps: agents: queue: n2-4-spot depends_on: build - timeout_in_minutes: 120 + timeout_in_minutes: 60 parallelism: 2 retry: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" - command: .buildkite/scripts/steps/functional/defend_workflows_vagrant.sh label: 'Defend Workflows Endpoint Cypress Tests' agents: queue: n2-4-virt depends_on: build - timeout_in_minutes: 120 - parallelism: 5 + timeout_in_minutes: 60 + parallelism: 6 retry: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" diff --git a/.buildkite/pipelines/pull_request/osquery_cypress.yml b/.buildkite/pipelines/pull_request/osquery_cypress.yml index 07e26e8f1ff6b..50c4dd4a3faa5 100644 --- a/.buildkite/pipelines/pull_request/osquery_cypress.yml +++ b/.buildkite/pipelines/pull_request/osquery_cypress.yml @@ -10,8 +10,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-osquery/**/*" - command: .buildkite/scripts/steps/functional/osquery_cypress_burn.sh label: 'Osquery Cypress Tests, burning changed specs' @@ -22,19 +20,16 @@ steps: soft_fail: true retry: automatic: false - artifact_paths: - - "target/kibana-osquery/**/*" - - command: .buildkite/scripts/steps/functional/security_serverless_osquery.sh - label: 'Serverless Osquery Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 50 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 - artifact_paths: - - "target/kibana-osquery/**/*" + # Error: self-signed certificate in certificate chain + # - command: .buildkite/scripts/steps/functional/security_serverless_osquery.sh + # label: 'Serverless Osquery Cypress Tests' + # agents: + # queue: n2-4-spot + # depends_on: build + # timeout_in_minutes: 50 + # parallelism: 6 + # retry: + # automatic: + # - exit_status: '*' + # limit: 1 diff --git a/.buildkite/pipelines/pull_request/response_ops.yml b/.buildkite/pipelines/pull_request/response_ops.yml index b2ef5199fab91..38ca242949d86 100644 --- a/.buildkite/pipelines/pull_request/response_ops.yml +++ b/.buildkite/pipelines/pull_request/response_ops.yml @@ -10,5 +10,3 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" diff --git a/.buildkite/pipelines/pull_request/response_ops_cases.yml b/.buildkite/pipelines/pull_request/response_ops_cases.yml index af2e58b65ab34..312c62c01c732 100644 --- a/.buildkite/pipelines/pull_request/response_ops_cases.yml +++ b/.buildkite/pipelines/pull_request/response_ops_cases.yml @@ -9,5 +9,3 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-solution/**/*" diff --git a/.buildkite/pipelines/pull_request/security_solution.yml b/.buildkite/pipelines/pull_request/security_solution.yml index 58b416548ec5f..2b7a6faaf212c 100644 --- a/.buildkite/pipelines/pull_request/security_solution.yml +++ b/.buildkite/pipelines/pull_request/security_solution.yml @@ -10,8 +10,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - 'target/kibana-security-solution/**/*' - command: .buildkite/scripts/steps/functional/security_solution_explore.sh label: 'Explore - Security Solution Cypress Tests' @@ -24,8 +22,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - 'target/kibana-security-solution/**/*' - command: .buildkite/scripts/steps/functional/security_solution_investigations.sh label: 'Investigations - Security Solution Cypress Tests' @@ -38,8 +34,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - 'target/kibana-security-solution/**/*' - command: .buildkite/scripts/steps/functional/security_solution_burn.sh label: 'Security Solution Cypress tests, burning changed specs' @@ -51,8 +45,6 @@ steps: retry: automatic: false soft_fail: true - artifact_paths: - - 'target/kibana-security-solution/**/*' - command: .buildkite/scripts/steps/code_generation/security_solution_codegen.sh label: 'Security Solution OpenAPI codegen' diff --git a/.buildkite/pipelines/pull_request/threat_intelligence.yml b/.buildkite/pipelines/pull_request/threat_intelligence.yml index f9b9050d28d95..b91be5faffdec 100644 --- a/.buildkite/pipelines/pull_request/threat_intelligence.yml +++ b/.buildkite/pipelines/pull_request/threat_intelligence.yml @@ -10,5 +10,3 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-threat-intelligence/**/*" diff --git a/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml b/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml index 27e55dfced9d7..0acdb66f8d5f2 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml @@ -1,4 +1,17 @@ +# This pipeline serves as the entry point for your service's quality gates definitions. When +# properly configured, it will be invoked automatically as part of the automated +# promotion process once a new version was rolled out in one of the various cloud stages. +# +# The updated environment is provided via ENVIRONMENT variable. The seedling +# step will branch and execute pipeline snippets at the following location: +# pipeline.tests-qa.yaml +# pipeline.tests-staging.yaml +# pipeline.tests-production.yaml +# +# Docs: https://docs.elastic.dev/serverless/qualitygates + env: + TEAM_CHANNEL: "#kibana-mission-control" ENVIRONMENT: ${ENVIRONMENT?} steps: @@ -8,3 +21,7 @@ steps: command: "make -C /agent run-environment-tests" agents: image: "docker.elastic.co/ci-agent-images/quality-gate-seedling:0.0.2" + +notify: + - slack: "${TEAM_CHANNEL?}" + if: build.branch == "main" && build.state == "failed" diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml index 5a0738ead7d9c..1c30a7f734df4 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml @@ -1,10 +1,29 @@ +# These pipeline steps constitute the quality gate for your service within the production +# environment. Incorporate any necessary additional logic to validate the service's integrity. +# A failure in this pipeline build will prevent further progression to the subsequent stage. + steps: - label: ":pipeline::fleet::seedling: Trigger Observability Kibana Tests for ${ENVIRONMENT}" command: echo "replace me with Observability specific Kibana tests" - agent: + agents: image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364" - label: ":pipeline::lock::seedling: Trigger Security Kibana Tests for ${ENVIRONMENT}" command: echo "replace me with Security specific Kibana tests" - agent: + agents: image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364" + + - label: ":rocket: Run cp e2e tests" + trigger: "ess-k8s-production-e2e-tests" + build: + message: "${BUILDKITE_MESSAGE}" + env: + REGION_ID: aws-us-east-1 + NAME_PREFIX: ci_test_${SERVICE}-promotion_ + + - wait: ~ + + - label: ":judge::seedling: Trigger Manual Tests Phase" + command: "make -C /agent trigger-manual-verification-phase" + agents: + image: "docker.elastic.co/ci-agent-images/manual-verification-agent:0.0.2" diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml index 669b306bc2ceb..e03e986f65833 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml @@ -1,20 +1,39 @@ +# These pipeline steps constitute the quality gate for your service within the QA environment. +# Incorporate any necessary additional logic to validate the service's integrity. A failure in +# this pipeline build will prevent further progression to the subsequent stage. + steps: - label: ":pipeline::kibana::seedling: Trigger Kibana Tests for ${ENVIRONMENT}" command: echo "replace me with Kibana specific tests" - agent: + agents: image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364" - label: ":pipeline::fleet::seedling: Trigger Fleet Kibana Tests for ${ENVIRONMENT}" command: echo "replace me with Fleet specific Kibana tests" - agent: + agents: image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364" - label: ":pipeline::lock::seedling: Trigger Security Kibana Tests for ${ENVIRONMENT}" command: echo "replace me with Security specific Kibana tests" - agent: + agents: image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364" - label: ":pipeline::lock::seedling: Trigger Control Plane Kibana Tests for ${ENVIRONMENT}" command: echo "replace me with Control Plane specific Kibana tests" - agent: + agents: image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364" + + - label: ":rocket: Run cp e2e tests" + trigger: "ess-k8s-qa-e2e-tests-daily" + build: + message: "${BUILDKITE_MESSAGE}" + env: + REGION_ID: aws-eu-west-1 + NAME_PREFIX: ci_test_kibana-promotion_ + + - wait: ~ + + - label: ":judge::seedling: Trigger Manual Tests Phase" + command: "make -C /agent trigger-manual-verification-phase" + agents: + image: "docker.elastic.co/ci-agent-images/manual-verification-agent:0.0.2" diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml index 5a0738ead7d9c..83bfd0d27e34c 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml @@ -1,10 +1,29 @@ +# These pipeline steps constitute the quality gate for your service within the staging environment. +# Incorporate any necessary additional logic to validate the service's integrity. A failure in +# this pipeline build will prevent further progression to the subsequent stage. + steps: - label: ":pipeline::fleet::seedling: Trigger Observability Kibana Tests for ${ENVIRONMENT}" command: echo "replace me with Observability specific Kibana tests" - agent: + agents: image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364" - label: ":pipeline::lock::seedling: Trigger Security Kibana Tests for ${ENVIRONMENT}" command: echo "replace me with Security specific Kibana tests" - agent: + agents: image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364" + + - label: ":rocket: Run cp e2e tests" + trigger: "ess-k8s-staging-e2e-tests" + build: + message: "${BUILDKITE_MESSAGE}" + env: + REGION_ID: aws-us-east-1 + NAME_PREFIX: ci_test_kibana-promotion_ + + - wait: ~ + + - label: ":judge::seedling: Trigger Manual Tests Phase" + command: "make -C /agent trigger-manual-verification-phase" + agents: + image: "docker.elastic.co/ci-agent-images/manual-verification-agent:0.0.2" diff --git a/.buildkite/pipelines/serverless.yml b/.buildkite/pipelines/serverless.yml index f78905d383ba9..e7b5dbc299722 100644 --- a/.buildkite/pipelines/serverless.yml +++ b/.buildkite/pipelines/serverless.yml @@ -34,6 +34,19 @@ steps: - exit_status: '*' limit: 1 + - command: SERVERLESS_ENVIRONMENT=observability.examples .buildkite/scripts/steps/functional/serverless_ftr.sh + label: 'Serverless Observability Examples Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 40 + retry: + automatic: + - exit_status: '-1' + limit: 3 + - exit_status: '*' + limit: 1 + - command: SERVERLESS_ENVIRONMENT=search .buildkite/scripts/steps/functional/serverless_ftr.sh label: 'Serverless Search Tests' agents: @@ -47,6 +60,19 @@ steps: - exit_status: '*' limit: 1 + - command: SERVERLESS_ENVIRONMENT=search.examples .buildkite/scripts/steps/functional/serverless_ftr.sh + label: 'Serverless Search Examples Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 40 + retry: + automatic: + - exit_status: '-1' + limit: 3 + - exit_status: '*' + limit: 1 + - command: SERVERLESS_ENVIRONMENT=security .buildkite/scripts/steps/functional/serverless_ftr.sh label: 'Serverless Security Tests' agents: @@ -60,19 +86,30 @@ steps: - exit_status: '*' limit: 1 + - command: SERVERLESS_ENVIRONMENT=security.examples .buildkite/scripts/steps/functional/serverless_ftr.sh + label: 'Serverless Security Examples Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 40 + retry: + automatic: + - exit_status: '-1' + limit: 3 + - exit_status: '*' + limit: 1 + - command: .buildkite/scripts/steps/functional/security_serverless.sh label: 'Serverless Security Cypress Tests' agents: queue: n2-4-spot depends_on: build timeout_in_minutes: 40 - parallelism: 16 + parallelism: 12 retry: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-serverless/**/*" - command: .buildkite/scripts/steps/functional/security_serverless_explore.sh label: 'Serverless Explore - Security Solution Cypress Tests' @@ -85,8 +122,6 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-serverless/**/*" - command: .buildkite/scripts/steps/functional/security_serverless_investigations.sh label: 'Serverless Investigations - Security Solution Cypress Tests' @@ -99,5 +134,3 @@ steps: automatic: - exit_status: '*' limit: 1 - artifact_paths: - - "target/kibana-security-serverless/**/*" diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts index cadfa23b53f9d..6fe1bb1b251e1 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts @@ -119,6 +119,15 @@ const uploadPipeline = (pipelineContent: string | object) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/apm_cypress.yml')); } + if ( + (await doAnyChangesMatch([/^x-pack\/plugins\/observability_onboarding/])) || + GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + ) { + pipeline.push( + getPipeline('.buildkite/pipelines/pull_request/observability_onboarding_cypress.yml') + ); + } + if ( (await doAnyChangesMatch([/^x-pack\/plugins\/profiling/])) || GITHUB_PR_LABELS.includes('ci:all-cypress-suites') diff --git a/.buildkite/scripts/steps/artifacts/docker_image.sh b/.buildkite/scripts/steps/artifacts/docker_image.sh index 3743aedfdc655..6a0c23384ef65 100755 --- a/.buildkite/scripts/steps/artifacts/docker_image.sh +++ b/.buildkite/scripts/steps/artifacts/docker_image.sh @@ -98,12 +98,14 @@ steps: - label: ":argo: Update kibana image tag for kibana-controller using gpctl" async: true branches: main - trigger: gpctl-promote + trigger: gpctl-promote-with-e2e-tests build: env: SERVICE_COMMIT_HASH: "$GIT_ABBREV_COMMIT" - REMOTE_SERVICE_CONFIG: https://raw.githubusercontent.com/elastic/serverless-gitops/main/gen/gpctl/kibana/config.yaml - + SERVICE: kibana-controller + NAMESPACE: kibana-ci + IMAGE_NAME: kibana-serverless + REMOTE_SERVICE_CONFIG: https://raw.githubusercontent.com/elastic/serverless-gitops/main/gen/gpctl/kibana/dev.yaml EOF else diff --git a/.buildkite/scripts/steps/functional/common.sh b/.buildkite/scripts/steps/functional/common.sh index f9b77890030c9..e6d13190b32cb 100755 --- a/.buildkite/scripts/steps/functional/common.sh +++ b/.buildkite/scripts/steps/functional/common.sh @@ -21,3 +21,7 @@ if [[ -d "$cacheDir" ]]; then fi is_test_execution_step + +# logins into docker as a common step for functional tests +echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co +trap 'docker logout docker.elastic.co' EXIT diff --git a/.buildkite/scripts/steps/functional/defend_workflows.sh b/.buildkite/scripts/steps/functional/defend_workflows.sh index 555d6cba2d374..111fa6a23d289 100755 --- a/.buildkite/scripts/steps/functional/defend_workflows.sh +++ b/.buildkite/scripts/steps/functional/defend_workflows.sh @@ -12,4 +12,5 @@ echo "--- Defend Workflows Cypress tests" cd x-pack/plugins/security_solution +set +e yarn cypress:dw:run; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/defend_workflows_vagrant.sh b/.buildkite/scripts/steps/functional/defend_workflows_vagrant.sh index 57b7b43163400..0dfabadb90687 100755 --- a/.buildkite/scripts/steps/functional/defend_workflows_vagrant.sh +++ b/.buildkite/scripts/steps/functional/defend_workflows_vagrant.sh @@ -12,4 +12,5 @@ echo "--- Defend Workflows Endpoint Cypress tests" cd x-pack/plugins/security_solution +set +e yarn cypress:dw:endpoint:run; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/osquery_cypress.sh b/.buildkite/scripts/steps/functional/osquery_cypress.sh index c6cc98d71ce07..da27c27559a45 100755 --- a/.buildkite/scripts/steps/functional/osquery_cypress.sh +++ b/.buildkite/scripts/steps/functional/osquery_cypress.sh @@ -12,4 +12,6 @@ export JOB=kibana-osquery-cypress echo "--- Osquery Cypress tests" -yarn --cwd x-pack/plugins/osquery cypress:run +cd x-pack/plugins/osquery + +yarn --cwd x-pack/plugins/osquery cypress:run \ No newline at end of file diff --git a/.buildkite/scripts/steps/functional/osquery_cypress_burn.sh b/.buildkite/scripts/steps/functional/osquery_cypress_burn.sh index b7fd648e53939..8aa5f28ce67cd 100755 --- a/.buildkite/scripts/steps/functional/osquery_cypress_burn.sh +++ b/.buildkite/scripts/steps/functional/osquery_cypress_burn.sh @@ -14,4 +14,4 @@ buildkite-agent meta-data set "${BUILDKITE_JOB_ID}_is_test_execution_step" 'fals echo "--- Osquery Cypress tests, burning changed specs (Chrome)" -yarn --cwd x-pack/plugins/osquery cypress:changed-specs-only +yarn --cwd x-pack/plugins/osquery cypress:changed-specs-only \ No newline at end of file diff --git a/.buildkite/scripts/steps/functional/response_ops.sh b/.buildkite/scripts/steps/functional/response_ops.sh index 1c065b2373b66..05e740bbf8d55 100755 --- a/.buildkite/scripts/steps/functional/response_ops.sh +++ b/.buildkite/scripts/steps/functional/response_ops.sh @@ -12,4 +12,5 @@ echo "--- Response Ops Cypress Tests on Security Solution" cd x-pack/test/security_solution_cypress +set +e yarn cypress:run:respops:ess; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/response_ops_cases.sh b/.buildkite/scripts/steps/functional/response_ops_cases.sh index 52eb3fce1985e..895edf0395dd0 100755 --- a/.buildkite/scripts/steps/functional/response_ops_cases.sh +++ b/.buildkite/scripts/steps/functional/response_ops_cases.sh @@ -12,4 +12,5 @@ echo "--- Response Ops Cases Cypress Tests on Security Solution" cd x-pack/test/security_solution_cypress +set +e yarn cypress:run:cases:ess; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/security_serverless.sh b/.buildkite/scripts/steps/functional/security_serverless.sh index 0a14478414bf3..984697ba6129b 100644 --- a/.buildkite/scripts/steps/functional/security_serverless.sh +++ b/.buildkite/scripts/steps/functional/security_serverless.sh @@ -12,4 +12,5 @@ echo "--- Security Serverless Cypress Tests" cd x-pack/test/security_solution_cypress +set +e yarn cypress:run:serverless; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/security_serverless_explore.sh b/.buildkite/scripts/steps/functional/security_serverless_explore.sh index 805f4fe147180..64cd304a4bff2 100644 --- a/.buildkite/scripts/steps/functional/security_serverless_explore.sh +++ b/.buildkite/scripts/steps/functional/security_serverless_explore.sh @@ -12,4 +12,5 @@ echo "--- Explore - Security Solution Cypress Tests" cd x-pack/test/security_solution_cypress +set +e yarn cypress:explore:run:serverless; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/security_serverless_investigations.sh b/.buildkite/scripts/steps/functional/security_serverless_investigations.sh index 15f249d474c40..49a9392f23de1 100644 --- a/.buildkite/scripts/steps/functional/security_serverless_investigations.sh +++ b/.buildkite/scripts/steps/functional/security_serverless_investigations.sh @@ -12,4 +12,5 @@ echo "--- Investigations Cypress Tests on Serverless" cd x-pack/test/security_solution_cypress +set +e yarn cypress:investigations:run:serverless; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/security_serverless_osquery.sh b/.buildkite/scripts/steps/functional/security_serverless_osquery.sh index 60312fcaf681a..d631424552fa8 100755 --- a/.buildkite/scripts/steps/functional/security_serverless_osquery.sh +++ b/.buildkite/scripts/steps/functional/security_serverless_osquery.sh @@ -2,10 +2,13 @@ set -euo pipefail -source .buildkite/scripts/common/util.sh +source .buildkite/scripts/steps/functional/common.sh source .buildkite/scripts/steps/functional/common_cypress.sh -.buildkite/scripts/bootstrap.sh +# TODO: remove the line below to use build artifacts for tests. +# in addition to remove the line, we will have to expose the kibana install dir into the downloaded build location +# by exporting a var like: +# export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} node scripts/build_kibana_platform_plugins.js export JOB=kibana-osquery-cypress-serverless diff --git a/.buildkite/scripts/steps/functional/security_solution.sh b/.buildkite/scripts/steps/functional/security_solution.sh index f02419a0b6f8c..ffd82a3601ba4 100755 --- a/.buildkite/scripts/steps/functional/security_solution.sh +++ b/.buildkite/scripts/steps/functional/security_solution.sh @@ -12,4 +12,5 @@ echo "--- Security Solution Cypress tests (Chrome)" cd x-pack/test/security_solution_cypress +set +e yarn cypress:run:ess; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/security_solution_explore.sh b/.buildkite/scripts/steps/functional/security_solution_explore.sh index e7d21b61f7209..03295b993dafd 100644 --- a/.buildkite/scripts/steps/functional/security_solution_explore.sh +++ b/.buildkite/scripts/steps/functional/security_solution_explore.sh @@ -12,4 +12,5 @@ echo "--- Explore Cypress Tests on Security Solution" cd x-pack/test/security_solution_cypress +set +e yarn cypress:explore:run:ess; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/security_solution_investigations.sh b/.buildkite/scripts/steps/functional/security_solution_investigations.sh index 217e79bc9ecfc..11e0d1fb7fea7 100644 --- a/.buildkite/scripts/steps/functional/security_solution_investigations.sh +++ b/.buildkite/scripts/steps/functional/security_solution_investigations.sh @@ -12,4 +12,5 @@ echo "--- Investigations - Security Solution Cypress Tests" cd x-pack/test/security_solution_cypress +set +e yarn cypress:investigations:run:ess; status=$?; yarn junit:merge && exit $status diff --git a/.buildkite/scripts/steps/functional/serverless_ftr.sh b/.buildkite/scripts/steps/functional/serverless_ftr.sh index 72a69897e54e3..335b4b97f1445 100755 --- a/.buildkite/scripts/steps/functional/serverless_ftr.sh +++ b/.buildkite/scripts/steps/functional/serverless_ftr.sh @@ -12,6 +12,10 @@ if [[ "$SERVERLESS_ENVIRONMENT" == "search" ]]; then "x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts" "x-pack/test_serverless/functional/test_suites/search/config.ts" ) +elif [[ "$SERVERLESS_ENVIRONMENT" == "search.examples" ]]; then + SERVERLESS_CONFIGS=( + "x-pack/test_serverless/functional/test_suites/search/config.examples.ts" + ) elif [[ "$SERVERLESS_ENVIRONMENT" == "observability" ]]; then SERVERLESS_CONFIGS=( "x-pack/test_serverless/api_integration/test_suites/observability/config.ts" @@ -19,12 +23,20 @@ elif [[ "$SERVERLESS_ENVIRONMENT" == "observability" ]]; then "x-pack/test_serverless/functional/test_suites/observability/config.ts" "x-pack/test_serverless/functional/test_suites/observability/cypress/config_headless.ts" ) +elif [[ "$SERVERLESS_ENVIRONMENT" == "observability.examples" ]]; then + SERVERLESS_CONFIGS=( + "x-pack/test_serverless/functional/test_suites/observability/config.examples.ts" + ) elif [[ "$SERVERLESS_ENVIRONMENT" == "security" ]]; then SERVERLESS_CONFIGS=( "x-pack/test_serverless/api_integration/test_suites/security/config.ts" "x-pack/test_serverless/api_integration/test_suites/security/config.feature_flags.ts" "x-pack/test_serverless/functional/test_suites/security/config.ts" ) +elif [[ "$SERVERLESS_ENVIRONMENT" == "security.examples" ]]; then + SERVERLESS_CONFIGS=( + "x-pack/test_serverless/functional/test_suites/security/config.examples.ts" + ) fi EXIT_CODE=0 diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index e9014da1cefae..2afc5f4037148 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -26,7 +26,7 @@ const STORYBOOKS = [ 'dashboard_enhanced', 'dashboard', 'data', - 'discover_log_explorer', + 'log_explorer', 'embeddable', 'expression_error', 'expression_image', diff --git a/.buildkite/scripts/steps/test/jest_integration.sh b/.buildkite/scripts/steps/test/jest_integration.sh index fd7b9a1d6ad54..6ebff3ae984b8 100755 --- a/.buildkite/scripts/steps/test/jest_integration.sh +++ b/.buildkite/scripts/steps/test/jest_integration.sh @@ -8,5 +8,9 @@ is_test_execution_step .buildkite/scripts/bootstrap.sh +echo '--- Docker login' +echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co +trap 'docker logout docker.elastic.co' EXIT + echo '--- Jest Integration Tests' .buildkite/scripts/steps/test/jest_parallel.sh jest.integration.config.js diff --git a/.eslintrc.js b/.eslintrc.js index e1153546e8154..5b5ef73ed3806 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1180,6 +1180,7 @@ module.exports = { overrides: [ { files: [ + 'x-pack/packages/security-solution/features/**/*.{js,mjs,ts,tsx}', 'x-pack/packages/security-solution/navigation/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/security_solution_ess/**/*.{js,mjs,ts,tsx}', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4f050e3bf422f..cd4fe15e8827c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -329,7 +329,6 @@ packages/kbn-dev-utils @elastic/kibana-operations examples/developer_examples @elastic/appex-sharedux examples/discover_customization_examples @elastic/kibana-data-discovery x-pack/plugins/discover_enhanced @elastic/kibana-data-discovery -x-pack/plugins/discover_log_explorer @elastic/infra-monitoring-ui src/plugins/discover @elastic/kibana-data-discovery packages/kbn-discover-utils @elastic/kibana-data-discovery packages/kbn-doc-links @elastic/docs @@ -472,6 +471,7 @@ packages/kbn-lint-ts-projects-cli @elastic/kibana-operations x-pack/plugins/lists @elastic/security-detection-engine examples/locator_examples @elastic/kibana-app-services examples/locator_explorer @elastic/kibana-app-services +x-pack/plugins/log_explorer @elastic/infra-monitoring-ui packages/kbn-logging @elastic/kibana-core packages/kbn-logging-mocks @elastic/kibana-core x-pack/plugins/logs_shared @elastic/infra-monitoring-ui @@ -524,6 +524,7 @@ packages/kbn-object-versioning @elastic/appex-sharedux x-pack/plugins/observability_ai_assistant @elastic/obs-ai-assistant x-pack/packages/observability/alert_details @elastic/actionable-observability x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops +x-pack/plugins/observability_log_explorer @elastic/infra-monitoring-ui x-pack/plugins/observability_onboarding @elastic/apm-ui x-pack/plugins/observability @elastic/actionable-observability x-pack/plugins/observability_shared @elastic/observability-ui @@ -544,6 +545,7 @@ packages/kbn-plugin-helpers @elastic/kibana-operations examples/portable_dashboards_example @elastic/kibana-presentation examples/preboot_example @elastic/kibana-security @elastic/kibana-core src/plugins/presentation_util @elastic/kibana-presentation +x-pack/plugins/profiling_data_access @elastic/profiling-ui x-pack/plugins/profiling @elastic/profiling-ui x-pack/packages/kbn-random-sampling @elastic/kibana-visualizations packages/kbn-react-field @elastic/kibana-data-discovery @@ -600,6 +602,7 @@ x-pack/plugins/searchprofiler @elastic/platform-deployment-management x-pack/test/security_api_integration/packages/helpers @elastic/kibana-security x-pack/plugins/security @elastic/kibana-security x-pack/plugins/security_solution_ess @elastic/security-solution +x-pack/packages/security-solution/features @elastic/security-threat-hunting-explore x-pack/test/cases_api_integration/common/plugins/security_solution @elastic/response-ops x-pack/packages/security-solution/navigation @elastic/security-threat-hunting-explore x-pack/plugins/security_solution @elastic/security-solution @@ -748,6 +751,10 @@ test/plugin_functional/plugins/ui_settings_plugin @elastic/kibana-core packages/kbn-ui-shared-deps-npm @elastic/kibana-operations packages/kbn-ui-shared-deps-src @elastic/kibana-operations packages/kbn-ui-theme @elastic/kibana-operations +packages/kbn-unified-data-table @elastic/kibana-data-discovery +packages/kbn-unified-doc-viewer @elastic/kibana-data-discovery +examples/unified_doc_viewer @elastic/kibana-core +src/plugins/unified_doc_viewer @elastic/kibana-data-discovery packages/kbn-unified-field-list @elastic/kibana-data-discovery examples/unified_field_list_examples @elastic/kibana-data-discovery src/plugins/unified_histogram @elastic/kibana-data-discovery @@ -832,6 +839,13 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations /x-pack/test_serverless/api_integration/test_suites/common/scripts_tests @elastic/kibana-data-discovery /x-pack/test_serverless/api_integration/test_suites/common/search_oss @elastic/kibana-data-discovery /x-pack/test_serverless/api_integration/test_suites/common/search_xpack @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/field_formats @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/partial_results @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/search @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/search_examples @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples @elastic/kibana-data-discovery # Visualizations /src/plugins/visualize/ @elastic/kibana-visualizations @@ -1043,6 +1057,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /.github/codeql @elastic/kibana-security /.github/workflows/codeql.yml @elastic/kibana-security /src/dev/eslint/security_eslint_rule_tests.ts @elastic/kibana-security +/src/core/server/integration_tests/config/check_dynamic_config.test.ts @elastic/kibana-security /src/plugins/telemetry/server/config/telemetry_labels.ts @elastic/kibana-security /test/interactive_setup_api_integration/ @elastic/kibana-security /test/interactive_setup_functional/ @elastic/kibana-security diff --git a/.github/workflows/create-deploy-tag.yml b/.github/workflows/create-deploy-tag.yml new file mode 100644 index 0000000000000..b230f2a929287 --- /dev/null +++ b/.github/workflows/create-deploy-tag.yml @@ -0,0 +1,179 @@ +--- +# - This workflow creates a tag with the format "deploy@" on the main branch. +# - It is triggered manually from the GitHub Actions UI. +# - It is only allowed to run on the main branch and ensures that the tag is created +# on the main branch only in a verification step. +# This is only to prevent accidental creation of the tag on other branches and cannot be used to prevent malicious creation of the tag. + +name: "Serverless: Promote to QA" + +on: + workflow_dispatch: + inputs: + commit: + description: "Commit to promote (default: latest commit on main)" + +concurrency: + group: ${{ github.workflow }} + +jobs: + create-deploy-tag: + # Temporary, we need a way to limit this to a GitHub team instead of specific users + if: contains('["watson","clintandrewhall","kobelb","lukeelmers","thomasneirynck","jbudz","mistic","delanni","Ikuni17"]', github.triggering_actor) + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Select commit to be tagged + run: | + commit="${{ github.event.inputs.commit || github.sha }}" + echo "COMMIT=${commit}" >> "${GITHUB_ENV}" + - name: Verify selected or newer commit isn't already tagged + run: | + git tag --contains ${COMMIT} | grep -P "^deploy@\d+$" && { + echo "A deploy-tag already exists on the selected or newer commit!" + exit 1 + } || true + - name: Verify branch + run: | + if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then + echo "This workflow can only be run on the main branch" + exit 1 + fi + - name: Prepare tag + run: | + tag_name="deploy@$(date +%s)" + echo "TAG_NAME=${tag_name}" >> "${GITHUB_ENV}" + - name: Create tag + run: | + git tag ${TAG_NAME} ${COMMIT} + git push origin "refs/tags/${TAG_NAME}" + - name: Post Slack success message + if: success() + uses: slackapi/slack-github-action@v1.24.0 + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Promotion of a new to QA has been initiated 🎉\n\nOnce promotion is complete, please begin any required manual testing.\n\n*Remember:* Promotion to Staging is currently a manual process and will proceed once the build is signed off in QA." + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Initiated by:*\n" + }, + { + "type": "mrkdwn", + "text": "*Commit:*\n" + }, + { + "type": "mrkdwn", + "text": "*Git tag:*\n" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Expected Staging promotion date:" + }, + "accessory": { + "type": "datepicker", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + }, + "action_id": "datepicker-action" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Useful links:*\n\n • \n • \n • \n • \n • " + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Day 1 to-do list*" + }, + "accessory": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "Verify successful promotion to QA" + }, + "value": "value-0" + }, + { + "text": { + "type": "mrkdwn", + "text": "Notify Security Solution team to beging manual testing" + }, + "value": "value-1" + } + ], + "action_id": "checkboxes-action" + } + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.DEPLOY_TAGGER_SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + - name: Post Slack failure message + if: failure() + uses: slackapi/slack-github-action@v1.24.0 + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Creation of deploy tag on failed ⛔️" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Initiated by:*\n" + }, + { + "type": "mrkdwn", + "text": "*Commit:*\n" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Useful links:*\n\n • \n • " + } + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.DEPLOY_TAGGER_SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK diff --git a/.i18nrc.json b/.i18nrc.json index 2463d023971ed..eb098a7011a15 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -122,9 +122,11 @@ "visTypeXy": "src/plugins/vis_types/xy", "visualizations": "src/plugins/visualizations", "visualizationUiComponents": "packages/kbn-visualization-ui-components", + "unifiedDocViewer": ["src/plugins/unified_doc_viewer", "packages/kbn-unified-doc-viewer"], "unifiedSearch": "src/plugins/unified_search", "unifiedFieldList": "packages/kbn-unified-field-list", - "unifiedHistogram": "src/plugins/unified_histogram" + "unifiedHistogram": "src/plugins/unified_histogram", + "unifiedDataTable": "packages/kbn-unified-data-table" }, "translations": [] } diff --git a/api_docs/actions.devdocs.json b/api_docs/actions.devdocs.json index 12eec5ba79786..ab2fd467cd958 100644 --- a/api_docs/actions.devdocs.json +++ b/api_docs/actions.devdocs.json @@ -3247,9 +3247,7 @@ "section": "def-common.ActionTypeExecutorResult", "text": "ActionTypeExecutorResult" }, - ">; enqueueExecution: (options: ", - "ExecuteOptions", - ") => Promise; bulkEnqueueExecution: (options: ", + ">; bulkEnqueueExecution: (options: ", "ExecuteOptions", "[]) => Promise; ephemeralEnqueuedExecution: (options: ", "ExecuteOptions", @@ -3261,14 +3259,10 @@ "section": "def-server.RunNowResult", "text": "RunNowResult" }, - ">; listTypes: ({ featureId, includeSystemActionTypes, }?: ListTypesOptions) => Promise<", - { - "pluginId": "actions", - "scope": "common", - "docId": "kibActionsPluginApi", - "section": "def-common.ActionType", - "text": "ActionType" - }, + ">; listTypes: ({ featureId, includeSystemActionTypes, }?: ", + "ListTypesParams", + ") => Promise<", + "ConnectorType", "[]>; isActionTypeEnabled: (actionTypeId: string, options?: { notifyUsage: boolean; }) => boolean; isPreconfigured: (connectorId: string) => boolean; isSystemAction: (connectorId: string) => boolean; getGlobalExecutionLogWithAuth: ({ dateStart, dateEnd, filter, page, perPage, sort, namespaces, }: ", { "pluginId": "actions", diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index e377ceae177bc..1675e5db1912c 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 267 | 0 | 261 | 28 | +| 267 | 0 | 261 | 30 | ## Client diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index a3c2ae0b512d6..17d00954ff5e6 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index a8df73deaa4e4..e69040bbcb687 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.devdocs.json b/api_docs/alerting.devdocs.json index 9fd1a0f9641b4..b6e47ef82750d 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -900,7 +900,9 @@ "\nInstalls index template that uses installed component template\nPrior to installation, simulates the installation to check for possible\nconflicts. Simulate should return an empty mapping if a template\nconflicts with an already installed template." ], "signature": [ - "({ logger, esClient, indexPatterns, totalFieldsLimit, }: CreateConcreteWriteIndexOpts) => Promise" + "(opts: ", + "CreateConcreteWriteIndexOpts", + ") => Promise" ], "path": "x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts", "deprecated": false, @@ -911,7 +913,7 @@ "id": "def-server.createConcreteWriteIndex.$1", "type": "Object", "tags": [], - "label": "{\n logger,\n esClient,\n indexPatterns,\n totalFieldsLimit,\n}", + "label": "opts", "description": [], "signature": [ "CreateConcreteWriteIndexOpts" @@ -968,7 +970,7 @@ "\nCreates ILM policy if it doesn't already exist, updates it if it does" ], "signature": [ - "({ logger, esClient, name, policy, }: CreateOrUpdateIlmPolicyOpts) => Promise" + "({ logger, esClient, name, policy, dataStreamAdapter, }: CreateOrUpdateIlmPolicyOpts) => Promise" ], "path": "x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts", "deprecated": false, @@ -979,7 +981,7 @@ "id": "def-server.createOrUpdateIlmPolicy.$1", "type": "Object", "tags": [], - "label": "{\n logger,\n esClient,\n name,\n policy,\n}", + "label": "{\n logger,\n esClient,\n name,\n policy,\n dataStreamAdapter,\n}", "description": [], "signature": [ "CreateOrUpdateIlmPolicyOpts" @@ -1062,6 +1064,48 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "alerting", + "id": "def-server.getDataStreamAdapter", + "type": "Function", + "tags": [], + "label": "getDataStreamAdapter", + "description": [], + "signature": [ + "(opts: ", + "GetDataStreamAdapterOpts", + ") => ", + { + "pluginId": "alerting", + "scope": "server", + "docId": "kibAlertingPluginApi", + "section": "def-server.DataStreamAdapter", + "text": "DataStreamAdapter" + } + ], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "alerting", + "id": "def-server.getDataStreamAdapter.$1", + "type": "Object", + "tags": [], + "label": "opts", + "description": [], + "signature": [ + "GetDataStreamAdapterOpts" + ], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "alerting", "id": "def-server.getEsErrorMessage", @@ -1105,7 +1149,7 @@ "label": "getIndexTemplate", "description": [], "signature": [ - "({ componentTemplateRefs, ilmPolicyName, indexPatterns, kibanaVersion, namespace, totalFieldsLimit, }: GetIndexTemplateOpts) => ", + "({ componentTemplateRefs, ilmPolicyName, indexPatterns, kibanaVersion, namespace, totalFieldsLimit, dataStreamAdapter, }: GetIndexTemplateOpts) => ", "IndicesPutIndexTemplateRequest" ], "path": "x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts", @@ -1117,7 +1161,7 @@ "id": "def-server.getIndexTemplate.$1", "type": "Object", "tags": [], - "label": "{\n componentTemplateRefs,\n ilmPolicyName,\n indexPatterns,\n kibanaVersion,\n namespace,\n totalFieldsLimit,\n}", + "label": "{\n componentTemplateRefs,\n ilmPolicyName,\n indexPatterns,\n kibanaVersion,\n namespace,\n totalFieldsLimit,\n dataStreamAdapter,\n}", "description": [], "signature": [ "GetIndexTemplateOpts" @@ -1517,6 +1561,118 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "alerting", + "id": "def-server.DataStreamAdapter", + "type": "Interface", + "tags": [], + "label": "DataStreamAdapter", + "description": [], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "alerting", + "id": "def-server.DataStreamAdapter.isUsingDataStreams", + "type": "Function", + "tags": [], + "label": "isUsingDataStreams", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "alerting", + "id": "def-server.DataStreamAdapter.getIndexTemplateFields", + "type": "Function", + "tags": [], + "label": "getIndexTemplateFields", + "description": [], + "signature": [ + "(alias: string, pattern: string) => ", + "IndexTemplateFields" + ], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "alerting", + "id": "def-server.DataStreamAdapter.getIndexTemplateFields.$1", + "type": "string", + "tags": [], + "label": "alias", + "description": [], + "signature": [ + "string" + ], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "alerting", + "id": "def-server.DataStreamAdapter.getIndexTemplateFields.$2", + "type": "string", + "tags": [], + "label": "pattern", + "description": [], + "signature": [ + "string" + ], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "alerting", + "id": "def-server.DataStreamAdapter.createStream", + "type": "Function", + "tags": [], + "label": "createStream", + "description": [], + "signature": [ + "(opts: ", + "CreateConcreteWriteIndexOpts", + ") => Promise" + ], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "alerting", + "id": "def-server.DataStreamAdapter.createStream.$1", + "type": "Object", + "tags": [], + "label": "opts", + "description": [], + "signature": [ + "CreateConcreteWriteIndexOpts" + ], + "path": "x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "alerting", "id": "def-server.FindResult", @@ -1894,7 +2050,7 @@ "section": "def-common.RuleTypeState", "text": "RuleTypeState" }, - ", InstanceState extends { [x: string]: unknown; } = { [x: string]: unknown; }, InstanceContext extends { [x: string]: unknown; } = { [x: string]: unknown; }, ActionGroupIds extends string = never, RecoveryActionGroupId extends string = never, AlertData extends ", + ", InstanceState extends Record = Record, InstanceContext extends { [x: string]: unknown; } = { [x: string]: unknown; }, ActionGroupIds extends string = never, RecoveryActionGroupId extends string = never, AlertData extends ", { "pluginId": "alerting", "scope": "common", @@ -1996,6 +2152,29 @@ "path": "x-pack/plugins/alerting/server/plugin.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-server.PluginSetupContract.getDataStreamAdapter", + "type": "Function", + "tags": [], + "label": "getDataStreamAdapter", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "alerting", + "scope": "server", + "docId": "kibAlertingPluginApi", + "section": "def-server.DataStreamAdapter", + "text": "DataStreamAdapter" + } + ], + "path": "x-pack/plugins/alerting/server/plugin.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] } ], "initialIsOpen": false @@ -2101,7 +2280,7 @@ "section": "def-common.RuleTypeState", "text": "RuleTypeState" }, - ", InstanceState extends { [x: string]: unknown; } = { [x: string]: unknown; }, InstanceContext extends { [x: string]: unknown; } = { [x: string]: unknown; }, ActionGroupIds extends string = string, RecoveryActionGroupId extends string = string, AlertData extends ", + ", InstanceState extends Record = Record, InstanceContext extends { [x: string]: unknown; } = { [x: string]: unknown; }, ActionGroupIds extends string = string, RecoveryActionGroupId extends string = string, AlertData extends ", { "pluginId": "alerting", "scope": "common", @@ -4197,21 +4376,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-server.AlertInstanceState", - "type": "Type", - "tags": [], - "label": "AlertInstanceState", - "description": [], - "signature": [ - "{ [x: string]: unknown; }" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-server.BulkEditOperation", @@ -4333,6 +4497,21 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "alerting", + "id": "def-server.LatestAlertInstanceStateSchema", + "type": "Type", + "tags": [], + "label": "LatestAlertInstanceStateSchema", + "description": [], + "signature": [ + "{ [x: string]: any; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "alerting", "id": "def-server.PartialRule", @@ -4554,7 +4733,7 @@ }, ">; getAlertState: (params: ", "GetAlertStateParams", - ") => Promise; getAlertSummary: (params: ", + ") => Promise | undefined; alertInstances?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined; alertRecoveredInstances?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined; previousStartedAt?: string | null | undefined; summaryActions?: Record> | undefined; } & {}>>; getAlertSummary: (params: ", "GetAlertSummaryParams", ") => Promise<", { @@ -5529,67 +5708,6 @@ ], "returnComment": [], "initialIsOpen": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.wrappedStateRt", - "type": "Function", - "tags": [], - "label": "wrappedStateRt", - "description": [], - "signature": [ - "() => ", - "TypeC", - "<{ wrapped: ", - "Type", - "; trackedAlerts: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ alertId: ", - "StringC", - "; alertUuid: ", - "StringC", - "; started: ", - "StringC", - "; flappingHistory: ", - "ArrayC", - "<", - "BooleanC", - ">; flapping: ", - "BooleanC", - "; pendingRecoveredCount: ", - "NumberC", - "; }>>; trackedAlertsRecovered: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ alertId: ", - "StringC", - "; alertUuid: ", - "StringC", - "; started: ", - "StringC", - "; flappingHistory: ", - "ArrayC", - "<", - "BooleanC", - ">; flapping: ", - "BooleanC", - "; pendingRecoveredCount: ", - "NumberC", - "; }>>; }>" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/lifecycle_state.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [], - "initialIsOpen": false } ], "interfaces": [ @@ -9389,18 +9507,6 @@ } ], "enums": [ - { - "parentPluginId": "alerting", - "id": "def-common.ActionsCompletion", - "type": "Enum", - "tags": [], - "label": "ActionsCompletion", - "description": [], - "path": "x-pack/packages/kbn-alerting-state-types/src/rule_task_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.HealthStatus", @@ -9539,36 +9645,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertInstanceMeta", - "type": "Type", - "tags": [], - "label": "AlertInstanceMeta", - "description": [], - "signature": [ - "{ lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; uuid?: string | undefined; }" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertInstanceState", - "type": "Type", - "tags": [], - "label": "AlertInstanceState", - "description": [], - "signature": [ - "{ [x: string]: unknown; }" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.ALERTS_FEATURE_ID", @@ -9848,15 +9924,90 @@ }, { "parentPluginId": "alerting", - "id": "def-common.LastScheduledActions", + "id": "def-common.LatestAlertInstanceMetaSchema", "type": "Type", "tags": [], - "label": "LastScheduledActions", + "label": "LatestAlertInstanceMetaSchema", "description": [], "signature": [ - "{ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }" + "{ readonly uuid?: string | undefined; readonly lastScheduledActions?: Readonly<{ actions?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; readonly flappingHistory?: boolean[] | undefined; readonly flapping?: boolean | undefined; readonly maintenanceWindowIds?: string[] | undefined; readonly pendingRecoveredCount?: number | undefined; }" ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.LatestAlertInstanceStateSchema", + "type": "Type", + "tags": [], + "label": "LatestAlertInstanceStateSchema", + "description": [], + "signature": [ + "{ [x: string]: any; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.LatestLastScheduledActionsSchema", + "type": "Type", + "tags": [], + "label": "LatestLastScheduledActionsSchema", + "description": [], + "signature": [ + "{ readonly actions?: Record> | undefined; readonly subgroup?: string | undefined; readonly date: string; readonly group: string; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.LatestRawAlertInstanceSchema", + "type": "Type", + "tags": [], + "label": "LatestRawAlertInstanceSchema", + "description": [], + "signature": [ + "{ readonly meta?: Readonly<{ uuid?: string | undefined; lastScheduledActions?: Readonly<{ actions?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; readonly state?: Record | undefined; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.LatestTaskStateSchema", + "type": "Type", + "tags": [], + "label": "LatestTaskStateSchema", + "description": [], + "signature": [ + "{ readonly alertTypeState?: Record | undefined; readonly alertInstances?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined; readonly alertRecoveredInstances?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined; readonly previousStartedAt?: string | null | undefined; readonly summaryActions?: Record> | undefined; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.LatestThrottledActionSchema", + "type": "Type", + "tags": [], + "label": "LatestThrottledActionSchema", + "description": [], + "signature": [ + "{ [x: string]: Readonly<{} & { date: string; }>; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -10160,21 +10311,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-common.RawAlertInstance", - "type": "Type", - "tags": [], - "label": "RawAlertInstance", - "description": [], - "signature": [ - "{ state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; uuid?: string | undefined; } | undefined; }" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.READ_FLAPPING_SETTINGS_SUB_FEATURE_ID", @@ -10622,21 +10758,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-common.RuleTaskState", - "type": "Type", - "tags": [], - "label": "RuleTaskState", - "description": [], - "signature": [ - "{ alertTypeState?: { [x: string]: unknown; } | undefined; alertInstances?: { [x: string]: { state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; uuid?: string | undefined; } | undefined; }; } | undefined; alertRecoveredInstances?: { [x: string]: { state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; uuid?: string | undefined; } | undefined; }; } | undefined; previousStartedAt?: Date | null | undefined; summaryActions?: { [x: string]: { date: Date; }; } | undefined; }" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/rule_task_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.RuleTypeParams", @@ -10752,21 +10873,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-common.ThrottledActions", - "type": "Type", - "tags": [], - "label": "ThrottledActions", - "description": [], - "signature": [ - "{ [x: string]: { date: Date; }; }" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.TrackedLifecycleAlertState", @@ -12796,22 +12902,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-common.DateFromString", - "type": "Object", - "tags": [], - "label": "DateFromString", - "description": [], - "signature": [ - "Type", - "" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/date_from_string.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.DEFAULT_FLAPPING_SETTINGS", @@ -13190,66 +13280,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-common.rawAlertInstance", - "type": "Object", - "tags": [], - "label": "rawAlertInstance", - "description": [], - "signature": [ - "PartialC", - "<{ state: ", - "RecordC", - "<", - "StringC", - ", ", - "UnknownC", - ">; meta: ", - "PartialC", - "<{ lastScheduledActions: ", - "IntersectionC", - "<[", - "PartialC", - "<{ subgroup: ", - "StringC", - "; }>, ", - "TypeC", - "<{ group: ", - "StringC", - "; date: ", - "Type", - "; }>, ", - "PartialC", - "<{ actions: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ date: ", - "Type", - "; }>>; }>]>; flappingHistory: ", - "ArrayC", - "<", - "BooleanC", - ">; flapping: ", - "BooleanC", - "; maintenanceWindowIds: ", - "ArrayC", - "<", - "StringC", - ">; pendingRecoveredCount: ", - "NumberC", - "; uuid: ", - "StringC", - "; }>; }>" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.RecoveredActionGroup", @@ -13356,166 +13386,6 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.ruleParamsSchema", - "type": "Object", - "tags": [], - "label": "ruleParamsSchema", - "description": [], - "signature": [ - "IntersectionC", - "<[", - "TypeC", - "<{ alertId: ", - "StringC", - "; }>, ", - "PartialC", - "<{ spaceId: ", - "StringC", - "; }>]>" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/rule_task_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.ruleStateSchema", - "type": "Object", - "tags": [], - "label": "ruleStateSchema", - "description": [], - "signature": [ - "PartialC", - "<{ alertTypeState: ", - "RecordC", - "<", - "StringC", - ", ", - "UnknownC", - ">; alertInstances: ", - "RecordC", - "<", - "StringC", - ", ", - "PartialC", - "<{ state: ", - "RecordC", - "<", - "StringC", - ", ", - "UnknownC", - ">; meta: ", - "PartialC", - "<{ lastScheduledActions: ", - "IntersectionC", - "<[", - "PartialC", - "<{ subgroup: ", - "StringC", - "; }>, ", - "TypeC", - "<{ group: ", - "StringC", - "; date: ", - "Type", - "; }>, ", - "PartialC", - "<{ actions: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ date: ", - "Type", - "; }>>; }>]>; flappingHistory: ", - "ArrayC", - "<", - "BooleanC", - ">; flapping: ", - "BooleanC", - "; maintenanceWindowIds: ", - "ArrayC", - "<", - "StringC", - ">; pendingRecoveredCount: ", - "NumberC", - "; uuid: ", - "StringC", - "; }>; }>>; alertRecoveredInstances: ", - "RecordC", - "<", - "StringC", - ", ", - "PartialC", - "<{ state: ", - "RecordC", - "<", - "StringC", - ", ", - "UnknownC", - ">; meta: ", - "PartialC", - "<{ lastScheduledActions: ", - "IntersectionC", - "<[", - "PartialC", - "<{ subgroup: ", - "StringC", - "; }>, ", - "TypeC", - "<{ group: ", - "StringC", - "; date: ", - "Type", - "; }>, ", - "PartialC", - "<{ actions: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ date: ", - "Type", - "; }>>; }>]>; flappingHistory: ", - "ArrayC", - "<", - "BooleanC", - ">; flapping: ", - "BooleanC", - "; maintenanceWindowIds: ", - "ArrayC", - "<", - "StringC", - ">; pendingRecoveredCount: ", - "NumberC", - "; uuid: ", - "StringC", - "; }>; }>>; previousStartedAt: ", - "UnionC", - "<[", - "NullC", - ", ", - "Type", - "]>; summaryActions: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ date: ", - "Type", - "; }>>; }>" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/rule_task_instance.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false } ] } diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index dc0e5a6b0c78d..74220f3e48d63 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 786 | 1 | 755 | 46 | +| 790 | 1 | 759 | 49 | ## Client diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index 05aa88a397978..8bd384647c099 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -408,7 +408,7 @@ "label": "APIEndpoint", "description": [], "signature": [ - "\"POST /internal/apm/data_view/static\" | \"GET /internal/apm/data_view/title\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name\" | \"POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}/samples\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions\" | \"POST /internal/apm/latency/overall_distribution/transactions\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/services/{serviceName}/metrics/nodes\" | \"GET /internal/apm/services/{serviceName}/metrics/serverless/charts\" | \"GET /internal/apm/services/{serviceName}/metrics/serverless/summary\" | \"GET /internal/apm/services/{serviceName}/metrics/serverless/functions_overview\" | \"GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/dependency\" | \"GET /internal/apm/services\" | \"POST /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search 2023-10-31\" | \"POST /api/apm/services/{serviceName}/annotation 2023-10-31\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/anomaly_charts\" | \"GET /internal/apm/services/{serviceName}/alerts_count\" | \"GET /internal/apm/service-groups\" | \"GET /internal/apm/service-group\" | \"POST /internal/apm/service-group\" | \"DELETE /internal/apm/service-group\" | \"GET /internal/apm/service-group/services\" | \"GET /internal/apm/service-group/counts\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/traces/find\" | \"POST /internal/apm/traces/aggregated_critical_path\" | \"GET /internal/apm/traces/{traceId}/transactions/{transactionId}\" | \"GET /internal/apm/traces/{traceId}/spans/{spanId}\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name\" | \"GET /internal/apm/rule_types/transaction_error_rate/chart_preview\" | \"GET /internal/apm/rule_types/error_count/chart_preview\" | \"GET /internal/apm/rule_types/transaction_duration/chart_preview\" | \"GET /api/apm/settings/agent-configuration 2023-10-31\" | \"GET /api/apm/settings/agent-configuration/view 2023-10-31\" | \"DELETE /api/apm/settings/agent-configuration 2023-10-31\" | \"PUT /api/apm/settings/agent-configuration 2023-10-31\" | \"POST /api/apm/settings/agent-configuration/search 2023-10-31\" | \"GET /api/apm/settings/agent-configuration/environments 2023-10-31\" | \"GET /api/apm/settings/agent-configuration/agent_name 2023-10-31\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"POST /internal/apm/settings/anomaly-detection/update_to_v3\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps 2023-10-31\" | \"POST /api/apm/sourcemaps 2023-10-31\" | \"DELETE /api/apm/sourcemaps/{id} 2023-10-31\" | \"POST /api/apm/androidmaps 2023-10-31\" | \"POST /internal/apm/sourcemaps/migrate_fleet_artifacts\" | \"GET /internal/apm/fleet/has_apm_policies\" | \"GET /internal/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema 2023-10-31\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/fleet/java_agent_versions\" | \"GET /internal/apm/dependencies/top_dependencies\" | \"GET /internal/apm/dependencies/upstream_services\" | \"GET /internal/apm/dependencies/metadata\" | \"GET /internal/apm/dependencies/charts/latency\" | \"GET /internal/apm/dependencies/charts/throughput\" | \"GET /internal/apm/dependencies/charts/error_rate\" | \"GET /internal/apm/dependencies/operations\" | \"GET /internal/apm/dependencies/charts/distribution\" | \"GET /internal/apm/dependencies/operations/spans\" | \"GET /internal/apm/correlations/field_candidates/transactions\" | \"GET /internal/apm/correlations/field_value_stats/transactions\" | \"POST /internal/apm/correlations/field_value_pairs/transactions\" | \"POST /internal/apm/correlations/significant_correlations/transactions\" | \"POST /internal/apm/correlations/p_values/transactions\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\" | \"GET /internal/apm/agent_keys\" | \"GET /internal/apm/agent_keys/privileges\" | \"POST /internal/apm/api_key/invalidate\" | \"POST /api/apm/agent_keys 2023-10-31\" | \"GET /internal/apm/storage_explorer\" | \"GET /internal/apm/services/{serviceName}/storage_details\" | \"GET /internal/apm/storage_chart\" | \"GET /internal/apm/storage_explorer/privileges\" | \"GET /internal/apm/storage_explorer_summary_stats\" | \"GET /internal/apm/storage_explorer/is_cross_cluster_search\" | \"GET /internal/apm/storage_explorer/get_services\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/children\" | \"GET /internal/apm/services/{serviceName}/infrastructure_attributes\" | \"GET /internal/apm/debug-telemetry\" | \"GET /internal/apm/time_range_metadata\" | \"GET /internal/apm/settings/labs\" | \"GET /internal/apm/get_agents_per_service\" | \"GET /internal/apm/get_latest_agent_versions\" | \"GET /internal/apm/services/{serviceName}/agent_instances\" | \"GET /internal/apm/services/{serviceName}/mobile/filters\" | \"GET /internal/apm/mobile-services/{serviceName}/most_used_charts\" | \"GET /internal/apm/mobile-services/{serviceName}/transactions/charts/sessions\" | \"GET /internal/apm/mobile-services/{serviceName}/transactions/charts/http_requests\" | \"GET /internal/apm/mobile-services/{serviceName}/stats\" | \"GET /internal/apm/mobile-services/{serviceName}/location/stats\" | \"GET /internal/apm/mobile-services/{serviceName}/terms\" | \"GET /internal/apm/mobile-services/{serviceName}/main_statistics\" | \"GET /internal/apm/mobile-services/{serviceName}/detailed_statistics\" | \"GET /internal/apm/diagnostics\" | \"POST /internal/apm/assistant/get_apm_timeseries\" | \"GET /internal/apm/assistant/get_service_summary\" | \"GET /internal/apm/assistant/get_error_document\" | \"POST /internal/apm/assistant/get_correlation_values\" | \"GET /internal/apm/assistant/get_downstream_dependencies\" | \"GET /internal/apm/assistant/get_services_list\"" + "\"POST /internal/apm/data_view/static\" | \"GET /internal/apm/data_view/title\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name\" | \"POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}/samples\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions\" | \"POST /internal/apm/latency/overall_distribution/transactions\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/services/{serviceName}/metrics/nodes\" | \"GET /internal/apm/services/{serviceName}/metrics/serverless/charts\" | \"GET /internal/apm/services/{serviceName}/metrics/serverless/summary\" | \"GET /internal/apm/services/{serviceName}/metrics/serverless/functions_overview\" | \"GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/dependency\" | \"GET /internal/apm/services\" | \"POST /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search 2023-10-31\" | \"POST /api/apm/services/{serviceName}/annotation 2023-10-31\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/anomaly_charts\" | \"GET /internal/apm/services/{serviceName}/alerts_count\" | \"GET /internal/apm/service-groups\" | \"GET /internal/apm/service-group\" | \"POST /internal/apm/service-group\" | \"DELETE /internal/apm/service-group\" | \"GET /internal/apm/service-group/services\" | \"GET /internal/apm/service-group/counts\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/traces/find\" | \"POST /internal/apm/traces/aggregated_critical_path\" | \"GET /internal/apm/traces/{traceId}/transactions/{transactionId}\" | \"GET /internal/apm/traces/{traceId}/spans/{spanId}\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name\" | \"GET /internal/apm/rule_types/transaction_error_rate/chart_preview\" | \"GET /internal/apm/rule_types/error_count/chart_preview\" | \"GET /internal/apm/rule_types/transaction_duration/chart_preview\" | \"GET /api/apm/settings/agent-configuration 2023-10-31\" | \"GET /api/apm/settings/agent-configuration/view 2023-10-31\" | \"DELETE /api/apm/settings/agent-configuration 2023-10-31\" | \"PUT /api/apm/settings/agent-configuration 2023-10-31\" | \"POST /api/apm/settings/agent-configuration/search 2023-10-31\" | \"GET /api/apm/settings/agent-configuration/environments 2023-10-31\" | \"GET /api/apm/settings/agent-configuration/agent_name 2023-10-31\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"POST /internal/apm/settings/anomaly-detection/update_to_v3\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps 2023-10-31\" | \"POST /api/apm/sourcemaps 2023-10-31\" | \"DELETE /api/apm/sourcemaps/{id} 2023-10-31\" | \"POST /internal/apm/sourcemaps/migrate_fleet_artifacts\" | \"GET /internal/apm/fleet/has_apm_policies\" | \"GET /internal/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema 2023-10-31\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/fleet/java_agent_versions\" | \"GET /internal/apm/dependencies/top_dependencies\" | \"GET /internal/apm/dependencies/upstream_services\" | \"GET /internal/apm/dependencies/metadata\" | \"GET /internal/apm/dependencies/charts/latency\" | \"GET /internal/apm/dependencies/charts/throughput\" | \"GET /internal/apm/dependencies/charts/error_rate\" | \"GET /internal/apm/dependencies/operations\" | \"GET /internal/apm/dependencies/charts/distribution\" | \"GET /internal/apm/dependencies/operations/spans\" | \"GET /internal/apm/correlations/field_candidates/transactions\" | \"GET /internal/apm/correlations/field_value_stats/transactions\" | \"POST /internal/apm/correlations/field_value_pairs/transactions\" | \"POST /internal/apm/correlations/significant_correlations/transactions\" | \"POST /internal/apm/correlations/p_values/transactions\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\" | \"GET /internal/apm/agent_keys\" | \"GET /internal/apm/agent_keys/privileges\" | \"POST /internal/apm/api_key/invalidate\" | \"POST /api/apm/agent_keys 2023-10-31\" | \"GET /internal/apm/storage_explorer\" | \"GET /internal/apm/services/{serviceName}/storage_details\" | \"GET /internal/apm/storage_chart\" | \"GET /internal/apm/storage_explorer/privileges\" | \"GET /internal/apm/storage_explorer_summary_stats\" | \"GET /internal/apm/storage_explorer/is_cross_cluster_search\" | \"GET /internal/apm/storage_explorer/get_services\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/children\" | \"GET /internal/apm/services/{serviceName}/infrastructure_attributes\" | \"GET /internal/apm/debug-telemetry\" | \"GET /internal/apm/time_range_metadata\" | \"GET /internal/apm/settings/labs\" | \"GET /internal/apm/get_agents_per_service\" | \"GET /internal/apm/get_latest_agent_versions\" | \"GET /internal/apm/services/{serviceName}/agent_instances\" | \"GET /internal/apm/services/{serviceName}/mobile/filters\" | \"GET /internal/apm/mobile-services/{serviceName}/most_used_charts\" | \"GET /internal/apm/mobile-services/{serviceName}/transactions/charts/sessions\" | \"GET /internal/apm/mobile-services/{serviceName}/transactions/charts/http_requests\" | \"GET /internal/apm/mobile-services/{serviceName}/stats\" | \"GET /internal/apm/mobile-services/{serviceName}/location/stats\" | \"GET /internal/apm/mobile-services/{serviceName}/terms\" | \"GET /internal/apm/mobile-services/{serviceName}/main_statistics\" | \"GET /internal/apm/mobile-services/{serviceName}/detailed_statistics\" | \"GET /internal/apm/diagnostics\" | \"POST /internal/apm/assistant/get_apm_timeseries\" | \"GET /internal/apm/assistant/get_service_summary\" | \"GET /internal/apm/assistant/get_error_document\" | \"POST /internal/apm/assistant/get_correlation_values\" | \"GET /internal/apm/assistant/get_downstream_dependencies\" | \"POST /internal/apm/assistant/get_services_list\"" ], "path": "x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts", "deprecated": false, @@ -455,9 +455,9 @@ "label": "APMServerRouteRepository", "description": [], "signature": [ - "{ \"GET /internal/apm/assistant/get_services_list\": { endpoint: \"GET /internal/apm/assistant/get_services_list\"; params?: ", + "{ \"POST /internal/apm/assistant/get_services_list\": { endpoint: \"POST /internal/apm/assistant/get_services_list\"; params?: ", "TypeC", - "<{ query: ", + "<{ body: ", "IntersectionC", "<[", "TypeC", @@ -468,36 +468,32 @@ "; }>, ", "PartialC", "<{ 'service.environment': ", + "StringC", + "; healthStatus: ", + "ArrayC", + "<", "UnionC", "<[", "LiteralC", - "<\"ENVIRONMENT_NOT_DEFINED\">, ", + "<", + "ServiceHealthStatus", + ".unknown>, ", "LiteralC", - "<\"ENVIRONMENT_ALL\">, ", - "BrandC", "<", - "StringC", - ", ", - { - "pluginId": "@kbn/io-ts-utils", - "scope": "common", - "docId": "kibKbnIoTsUtilsPluginApi", - "section": "def-common.NonEmptyStringBrand", - "text": "NonEmptyStringBrand" - }, - ">]>; }>]>; }> | undefined; handler: ({}: ", + "ServiceHealthStatus", + ".healthy>, ", + "LiteralC", + "<", + "ServiceHealthStatus", + ".warning>, ", + "LiteralC", + "<", + "ServiceHealthStatus", + ".critical>]>>; }>]>; }> | undefined; handler: ({}: ", "APMRouteHandlerResources", - " & { params: { query: { start: string; end: string; } & { 'service.environment'?: \"ENVIRONMENT_NOT_DEFINED\" | \"ENVIRONMENT_ALL\" | ", - "Branded", - " | undefined; }; }; }) => Promise<{ content: ApmServicesListContent; }>; } & ", + " & { params: { body: { start: string; end: string; } & { 'service.environment'?: string | undefined; healthStatus?: ", + "ServiceHealthStatus", + "[] | undefined; }; }; }) => Promise<{ content: ApmServicesListContent; }>; } & ", "APMRouteCreateOptions", "; \"GET /internal/apm/assistant/get_downstream_dependencies\": { endpoint: \"GET /internal/apm/assistant/get_downstream_dependencies\"; params?: ", "TypeC", @@ -689,25 +685,7 @@ "IntersectionC", "<[", "TypeC", - "<{ 'service.environment': ", - "UnionC", - "<[", - "LiteralC", - "<\"ENVIRONMENT_NOT_DEFINED\">, ", - "LiteralC", - "<\"ENVIRONMENT_ALL\">, ", - "BrandC", - "<", - "StringC", - ", ", - { - "pluginId": "@kbn/io-ts-utils", - "scope": "common", - "docId": "kibKbnIoTsUtilsPluginApi", - "section": "def-common.NonEmptyStringBrand", - "text": "NonEmptyStringBrand" - }, - ">]>; 'service.name': ", + "<{ 'service.name': ", "StringC", "; title: ", "StringC", @@ -791,23 +769,15 @@ "StringC", "; offset: ", "StringC", + "; 'service.environment': ", + "StringC", "; }>]>>; start: ", "StringC", "; end: ", "StringC", "; }>; }> | undefined; handler: ({}: ", "APMRouteHandlerResources", - " & { params: { body: { stats: ({ 'service.environment': \"ENVIRONMENT_NOT_DEFINED\" | \"ENVIRONMENT_ALL\" | ", - "Branded", - "; 'service.name': string; title: string; timeseries: ({ name: ", + " & { params: { body: { stats: ({ 'service.name': string; title: string; timeseries: ({ name: ", "ApmTimeseriesType", ".transactionThroughput | ", "ApmTimeseriesType", @@ -823,7 +793,7 @@ "LatencyAggregationType", "; } & { 'transaction.type'?: string | undefined; }) | { name: ", "ApmTimeseriesType", - ".errorEventRate; }; } & { filter?: string | undefined; offset?: string | undefined; })[]; start: string; end: string; }; }; }) => Promise<{ content: Omit<", + ".errorEventRate; }; } & { filter?: string | undefined; offset?: string | undefined; 'service.environment'?: string | undefined; })[]; start: string; end: string; }; }; }) => Promise<{ content: Omit<", "ApmTimeseries", ", \"data\">[]; data: ", "ApmTimeseries", @@ -3171,28 +3141,6 @@ "APMRouteHandlerResources", ") => Promise; } & ", "APMRouteCreateOptions", - "; \"POST /api/apm/androidmaps 2023-10-31\": { endpoint: \"POST /api/apm/androidmaps 2023-10-31\"; params?: ", - "TypeC", - "<{ body: ", - "TypeC", - "<{ service_name: ", - "StringC", - "; service_version: ", - "StringC", - "; map_file: ", - "Type", - "; }>; }> | undefined; handler: ({}: ", - "APMRouteHandlerResources", - " & { params: { body: { service_name: string; service_version: string; map_file: string; }; }; }) => Promise<", - { - "pluginId": "fleet", - "scope": "server", - "docId": "kibFleetPluginApi", - "section": "def-server.Artifact", - "text": "Artifact" - }, - " | undefined>; } & ", - "APMRouteCreateOptions", "; \"DELETE /api/apm/sourcemaps/{id} 2023-10-31\": { endpoint: \"DELETE /api/apm/sourcemaps/{id} 2023-10-31\"; params?: ", "TypeC", "<{ path: ", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index b2bff86331a71..1f3e0da71d41a 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) for ques | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 29 | 0 | 29 | 118 | +| 29 | 0 | 29 | 119 | ## Client diff --git a/api_docs/apm_data_access.mdx b/api_docs/apm_data_access.mdx index 7f9d6eb4d6979..7e0cb19b76349 100644 --- a/api_docs/apm_data_access.mdx +++ b/api_docs/apm_data_access.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apmDataAccess title: "apmDataAccess" image: https://source.unsplash.com/400x175/?github description: API docs for the apmDataAccess plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apmDataAccess'] --- import apmDataAccessObj from './apm_data_access.devdocs.json'; diff --git a/api_docs/asset_manager.mdx b/api_docs/asset_manager.mdx index b3aea5514a082..7c9e072b32a5a 100644 --- a/api_docs/asset_manager.mdx +++ b/api_docs/asset_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/assetManager title: "assetManager" image: https://source.unsplash.com/400x175/?github description: API docs for the assetManager plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'assetManager'] --- import assetManagerObj from './asset_manager.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index 1e40fdd64b159..b095e025ebb25 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index 83a8cc40306b6..b35d2c259da58 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 4f65b84fa40f2..391741661db72 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.devdocs.json b/api_docs/cases.devdocs.json index 15f422cdf22ba..5d00886d09ee8 100644 --- a/api_docs/cases.devdocs.json +++ b/api_docs/cases.devdocs.json @@ -1091,7 +1091,14 @@ "\nReturn the UI capabilities for each type of operation. These strings must match the values defined in the UI\nhere: x-pack/plugins/cases/public/client/helpers/capabilities.ts" ], "signature": [ - "() => { all: readonly [\"create_cases\", \"read_cases\", \"update_cases\", \"push_cases\", \"cases_connectors\"]; read: readonly [\"read_cases\", \"cases_connectors\"]; delete: readonly [\"delete_cases\"]; }" + "() => ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CasesUiCapabilities", + "text": "CasesUiCapabilities" + } ], "path": "x-pack/plugins/cases/common/utils/capabilities.ts", "deprecated": false, @@ -1108,7 +1115,14 @@ "label": "getApiTags", "description": [], "signature": [ - "(owner: \"cases\" | \"observability\" | \"securitySolution\") => { all: readonly [\"casesSuggestUserProfiles\", \"bulkGetUserProfiles\", \"casesGetConnectorsConfigure\", string, string]; read: readonly [\"casesSuggestUserProfiles\", \"bulkGetUserProfiles\", \"casesGetConnectorsConfigure\", string]; delete: readonly [string]; }" + "(owner: \"cases\" | \"observability\" | \"securitySolution\") => ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CasesApiTags", + "text": "CasesApiTags" + } ], "path": "x-pack/plugins/cases/common/utils/api_tags.ts", "deprecated": false, @@ -1236,6 +1250,62 @@ } ], "interfaces": [ + { + "parentPluginId": "cases", + "id": "def-common.CasesApiTags", + "type": "Interface", + "tags": [], + "label": "CasesApiTags", + "description": [], + "path": "x-pack/plugins/cases/common/utils/api_tags.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-common.CasesApiTags.all", + "type": "Object", + "tags": [], + "label": "all", + "description": [], + "signature": [ + "readonly string[]" + ], + "path": "x-pack/plugins/cases/common/utils/api_tags.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "cases", + "id": "def-common.CasesApiTags.read", + "type": "Object", + "tags": [], + "label": "read", + "description": [], + "signature": [ + "readonly string[]" + ], + "path": "x-pack/plugins/cases/common/utils/api_tags.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "cases", + "id": "def-common.CasesApiTags.delete", + "type": "Object", + "tags": [], + "label": "delete", + "description": [], + "signature": [ + "readonly string[]" + ], + "path": "x-pack/plugins/cases/common/utils/api_tags.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "cases", "id": "def-common.CasesPermissions", @@ -1327,6 +1397,62 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "cases", + "id": "def-common.CasesUiCapabilities", + "type": "Interface", + "tags": [], + "label": "CasesUiCapabilities", + "description": [], + "path": "x-pack/plugins/cases/common/utils/capabilities.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-common.CasesUiCapabilities.all", + "type": "Object", + "tags": [], + "label": "all", + "description": [], + "signature": [ + "readonly string[]" + ], + "path": "x-pack/plugins/cases/common/utils/capabilities.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "cases", + "id": "def-common.CasesUiCapabilities.read", + "type": "Object", + "tags": [], + "label": "read", + "description": [], + "signature": [ + "readonly string[]" + ], + "path": "x-pack/plugins/cases/common/utils/capabilities.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "cases", + "id": "def-common.CasesUiCapabilities.delete", + "type": "Object", + "tags": [], + "label": "delete", + "description": [], + "signature": [ + "readonly string[]" + ], + "path": "x-pack/plugins/cases/common/utils/capabilities.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "cases", "id": "def-common.Ecs", @@ -2031,6 +2157,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "cases", + "id": "def-common.CASES_CONNECTORS_CAPABILITY", + "type": "string", + "tags": [], + "label": "CASES_CONNECTORS_CAPABILITY", + "description": [], + "signature": [ + "\"cases_connectors\"" + ], + "path": "x-pack/plugins/cases/common/constants/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "cases", "id": "def-common.CASES_URL", @@ -2651,6 +2792,23 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "cases", + "id": "def-common.GET_CONNECTORS_CONFIGURE_API_TAG", + "type": "string", + "tags": [], + "label": "GET_CONNECTORS_CONFIGURE_API_TAG", + "description": [ + "\nThis tag is registered for the connectors (configure) get API" + ], + "signature": [ + "\"casesGetConnectorsConfigure\"" + ], + "path": "x-pack/plugins/cases/common/constants/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "cases", "id": "def-common.GetRelatedCasesByAlertResponse", diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index cfaf4b7b3056d..34f1b185e06bc 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 94 | 0 | 75 | 27 | +| 104 | 0 | 84 | 27 | ## Client diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index 613b1af2ac128..80664fabad87d 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index c65150369cd92..73b5ef4c2517e 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index 89d22b4cadf85..8b0f6a65ddd8e 100644 --- a/api_docs/cloud_chat.mdx +++ b/api_docs/cloud_chat.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChat title: "cloudChat" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChat plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_chat_provider.mdx b/api_docs/cloud_chat_provider.mdx index 497715c6452dc..89e27b560261f 100644 --- a/api_docs/cloud_chat_provider.mdx +++ b/api_docs/cloud_chat_provider.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChatProvider title: "cloudChatProvider" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChatProvider plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChatProvider'] --- import cloudChatProviderObj from './cloud_chat_provider.devdocs.json'; diff --git a/api_docs/cloud_data_migration.mdx b/api_docs/cloud_data_migration.mdx index 0220ebe6fce30..5410157a3a4e8 100644 --- a/api_docs/cloud_data_migration.mdx +++ b/api_docs/cloud_data_migration.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDataMigration title: "cloudDataMigration" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDataMigration plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDataMigration'] --- import cloudDataMigrationObj from './cloud_data_migration.devdocs.json'; diff --git a/api_docs/cloud_defend.mdx b/api_docs/cloud_defend.mdx index 5daa2dc23ed19..765d3226f9c3a 100644 --- a/api_docs/cloud_defend.mdx +++ b/api_docs/cloud_defend.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDefend title: "cloudDefend" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDefend plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDefend'] --- import cloudDefendObj from './cloud_defend.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index 73671acb55813..e0b54f328ba2c 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 5f74e4dd1dccb..113f43f209db5 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index e03f7d1496f1a..6318a4faa542c 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/content_management.mdx b/api_docs/content_management.mdx index a9b98bbe50122..9886b7a3ea00b 100644 --- a/api_docs/content_management.mdx +++ b/api_docs/content_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/contentManagement title: "contentManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the contentManagement plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'contentManagement'] --- import contentManagementObj from './content_management.devdocs.json'; diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 180b52069b3cd..a5dd74b6330d1 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 883d17498c404..66ba3a87e78bb 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index bdd762edacbc5..91b1eb51f287b 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index 7eae636681801..a45b5025b2fe9 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.devdocs.json b/api_docs/data.devdocs.json index 17cf390695f9e..31a0b4ae7ada9 100644 --- a/api_docs/data.devdocs.json +++ b/api_docs/data.devdocs.json @@ -7620,6 +7620,77 @@ "trackAdoption": false } ] + }, + { + "parentPluginId": "data", + "id": "def-public.DataPublicPluginStartActions.createFiltersFromMultiValueClickAction", + "type": "Function", + "tags": [], + "label": "createFiltersFromMultiValueClickAction", + "description": [], + "signature": [ + "({ data, negate, }: { data: { cells: { column: number; row: number; }[]; table: Pick<", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.Datatable", + "text": "Datatable" + }, + ", \"meta\" | \"rows\" | \"columns\">; relation?: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.BooleanRelation", + "text": "BooleanRelation" + }, + " | undefined; }[]; timeFieldName?: string | undefined; negate?: boolean | undefined; }) => Promise<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[] | undefined>" + ], + "path": "src/plugins/data/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "data", + "id": "def-public.DataPublicPluginStartActions.createFiltersFromMultiValueClickAction.$1", + "type": "Object", + "tags": [], + "label": "__0", + "description": [], + "signature": [ + "{ data: { cells: { column: number; row: number; }[]; table: Pick<", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.Datatable", + "text": "Datatable" + }, + ", \"meta\" | \"rows\" | \"columns\">; relation?: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.BooleanRelation", + "text": "BooleanRelation" + }, + " | undefined; }[]; timeFieldName?: string | undefined; negate?: boolean | undefined; }" + ], + "path": "src/plugins/data/public/actions/filters/create_filters_from_multi_value_click.ts", + "deprecated": false, + "trackAdoption": false + } + ] } ], "initialIsOpen": false @@ -13608,6 +13679,10 @@ "plugin": "@kbn/unified-field-list", "path": "packages/kbn-unified-field-list/src/hooks/use_existing_fields.ts" }, + { + "plugin": "@kbn/unified-field-list", + "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" + }, { "plugin": "@kbn/event-annotation-components", "path": "packages/kbn-event-annotation-components/components/group_editor_controls/group_editor_controls.tsx" @@ -13620,10 +13695,6 @@ "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" }, - { - "plugin": "@kbn/unified-field-list", - "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx" @@ -13898,15 +13969,15 @@ }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" + "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" + "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/services/es_index_service.ts" + "path": "x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts" }, { "plugin": "transform", @@ -21373,6 +21444,10 @@ "plugin": "@kbn/unified-field-list", "path": "packages/kbn-unified-field-list/src/hooks/use_existing_fields.ts" }, + { + "plugin": "@kbn/unified-field-list", + "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" + }, { "plugin": "@kbn/event-annotation-components", "path": "packages/kbn-event-annotation-components/components/group_editor_controls/group_editor_controls.tsx" @@ -21385,10 +21460,6 @@ "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" }, - { - "plugin": "@kbn/unified-field-list", - "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx" @@ -21663,15 +21734,15 @@ }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" + "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" + "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/services/es_index_service.ts" + "path": "x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts" }, { "plugin": "transform", diff --git a/api_docs/data.mdx b/api_docs/data.mdx index 3173f22684c86..70949b17f4987 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3308 | 33 | 2581 | 26 | +| 3311 | 33 | 2584 | 26 | ## Client diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index 4610c901d482c..62d0861bbbf19 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3308 | 33 | 2581 | 26 | +| 3311 | 33 | 2584 | 26 | ## Client diff --git a/api_docs/data_search.devdocs.json b/api_docs/data_search.devdocs.json index 3b5cd7151721e..ceb9136800672 100644 --- a/api_docs/data_search.devdocs.json +++ b/api_docs/data_search.devdocs.json @@ -32457,6 +32457,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "data", + "id": "def-common.ESQL_SEARCH_STRATEGY", + "type": "string", + "tags": [], + "label": "ESQL_SEARCH_STRATEGY", + "description": [], + "signature": [ + "\"esql\"" + ], + "path": "src/plugins/data/common/search/strategies/esql_search/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "data", "id": "def-common.EsQuerySearchAfter", diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index da269e2ad8258..fde4b2f40f862 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3308 | 33 | 2581 | 26 | +| 3311 | 33 | 2584 | 26 | ## Client diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 0eadf9e93ada3..d2fa815d40409 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 0f3fd5fc08760..c10490b5d7a9a 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index 15c25492cdafa..6e41bcc12d78a 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.devdocs.json b/api_docs/data_views.devdocs.json index 4a845a6057727..347f1f24fed09 100644 --- a/api_docs/data_views.devdocs.json +++ b/api_docs/data_views.devdocs.json @@ -367,6 +367,10 @@ "plugin": "@kbn/unified-field-list", "path": "packages/kbn-unified-field-list/src/hooks/use_existing_fields.ts" }, + { + "plugin": "@kbn/unified-field-list", + "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" + }, { "plugin": "@kbn/event-annotation-components", "path": "packages/kbn-event-annotation-components/components/group_editor_controls/group_editor_controls.tsx" @@ -379,10 +383,6 @@ "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" }, - { - "plugin": "@kbn/unified-field-list", - "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx" @@ -657,15 +657,15 @@ }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" + "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" + "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/services/es_index_service.ts" + "path": "x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts" }, { "plugin": "transform", @@ -8331,6 +8331,10 @@ "plugin": "@kbn/unified-field-list", "path": "packages/kbn-unified-field-list/src/hooks/use_existing_fields.ts" }, + { + "plugin": "@kbn/unified-field-list", + "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" + }, { "plugin": "@kbn/event-annotation-components", "path": "packages/kbn-event-annotation-components/components/group_editor_controls/group_editor_controls.tsx" @@ -8343,10 +8347,6 @@ "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" }, - { - "plugin": "@kbn/unified-field-list", - "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx" @@ -8621,15 +8621,15 @@ }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" + "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" + "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/services/es_index_service.ts" + "path": "x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts" }, { "plugin": "transform", @@ -15405,6 +15405,10 @@ "plugin": "@kbn/unified-field-list", "path": "packages/kbn-unified-field-list/src/hooks/use_existing_fields.ts" }, + { + "plugin": "@kbn/unified-field-list", + "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" + }, { "plugin": "@kbn/event-annotation-components", "path": "packages/kbn-event-annotation-components/components/group_editor_controls/group_editor_controls.tsx" @@ -15417,10 +15421,6 @@ "plugin": "lens", "path": "x-pack/plugins/lens/public/data_views_service/loader.ts" }, - { - "plugin": "@kbn/unified-field-list", - "path": "packages/kbn-unified-field-list/src/services/field_stats/load_field_stats.ts" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx" @@ -15695,15 +15695,15 @@ }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" + "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts" + "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx" }, { "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/services/es_index_service.ts" + "path": "x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts" }, { "plugin": "transform", diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 96bf7eb84fd99..0d85b10f37cbd 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index b67f64a5f94a1..dec5ec2987ed8 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index e5c6f8f7f98bb..a01b818334271 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -22,7 +22,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | @kbn/es-query, @kbn/visualization-ui-components, securitySolution, observability, timelines, lists, threatIntelligence, savedSearch, dataViews, logsShared, savedObjectsManagement, unifiedSearch, controls, @kbn/unified-field-list, @kbn/event-annotation-components, lens, triggersActionsUi, dataVisualizer, ml, visTypeTimeseries, apm, exploratoryView, fleet, stackAlerts, infra, canvas, enterpriseSearch, graph, transform, upgradeAssistant, uptime, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, data | - | | | @kbn/es-query, @kbn/visualization-ui-components, securitySolution, observability, timelines, lists, threatIntelligence, savedSearch, data, logsShared, savedObjectsManagement, unifiedSearch, controls, @kbn/unified-field-list, @kbn/event-annotation-components, lens, triggersActionsUi, dataVisualizer, ml, visTypeTimeseries, apm, exploratoryView, fleet, stackAlerts, infra, canvas, enterpriseSearch, graph, transform, upgradeAssistant, uptime, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega | - | | | home, data, esUiShared, savedObjectsManagement, ml, exploratoryView, fleet, observability, apm, indexLifecycleManagement, observabilityOnboarding, synthetics, upgradeAssistant, uptime, ux, kibanaOverview | - | -| | share, uiActions, guidedOnboarding, home, management, spaces, security, savedObjects, serverless, visualizations, controls, dashboard, savedObjectsTagging, expressionXY, lens, expressionMetricVis, expressionGauge, alerting, triggersActionsUi, cases, licenseManagement, advancedSettings, maps, dataVisualizer, aiops, ml, exploratoryView, fleet, observability, infra, profiling, apm, expressionImage, expressionMetric, expressionError, expressionRevealImage, expressionRepeatImage, expressionShape, indexManagement, crossClusterReplication, enterpriseSearch, globalSearchBar, graph, grokdebugger, indexLifecycleManagement, ingestPipelines, logstash, monitoring, observabilityOnboarding, osquery, devTools, painlessLab, remoteClusters, rollup, searchprofiler, newsfeed, securitySolution, snapshotRestore, synthetics, transform, upgradeAssistant, uptime, ux, watcher, cloudDataMigration, console, filesManagement, kibanaOverview, visDefaultEditor, expressionHeatmap, expressionLegacyMetricVis, expressionPartitionVis, expressionTagcloud, visTypeTable, visTypeTimelion, visTypeTimeseries, visTypeVega, visTypeVislib | - | +| | share, uiActions, guidedOnboarding, home, management, spaces, security, savedObjects, indexManagement, serverless, visualizations, controls, dashboard, savedObjectsTagging, expressionXY, lens, expressionMetricVis, expressionGauge, alerting, triggersActionsUi, cases, licenseManagement, advancedSettings, maps, dataVisualizer, aiops, ml, exploratoryView, fleet, observability, infra, profiling, apm, expressionImage, expressionMetric, expressionError, expressionRevealImage, expressionRepeatImage, expressionShape, crossClusterReplication, enterpriseSearch, globalSearchBar, graph, grokdebugger, indexLifecycleManagement, ingestPipelines, logstash, monitoring, observabilityOnboarding, osquery, devTools, painlessLab, remoteClusters, rollup, searchprofiler, newsfeed, securitySolution, snapshotRestore, synthetics, transform, upgradeAssistant, uptime, ux, watcher, cloudDataMigration, console, filesManagement, kibanaOverview, visDefaultEditor, expressionHeatmap, expressionLegacyMetricVis, expressionPartitionVis, expressionTagcloud, visTypeTable, visTypeTimelion, visTypeTimeseries, visTypeVega, visTypeVislib | - | | | encryptedSavedObjects, actions, data, ml, securitySolution, logstash, cloudChat | - | | | actions, ml, savedObjectsTagging, enterpriseSearch | - | | | @kbn/core-saved-objects-browser-internal, @kbn/core, savedObjects, presentationUtil, visualizations, dataVisualizer, ml, aiops, dashboardEnhanced, graph, lens, securitySolution, eventAnnotation, @kbn/core-saved-objects-browser-mocks | - | @@ -36,7 +36,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | @kbn/core-saved-objects-api-server-internal, @kbn/core-saved-objects-import-export-server-internal, @kbn/core-saved-objects-server-internal, @kbn/core-saved-objects-api-browser, @kbn/core-saved-objects-browser-internal, home, fleet, osquery, securitySolution, @kbn/core-saved-objects-browser-mocks, graph, lists, alerting | - | | | alerting, discover, securitySolution | - | | | securitySolution | - | -| | inspector, data, licensing, security, savedObjects, dataViewEditor, unifiedSearch, embeddable, visualizations, controls, dashboard, savedObjectsTagging, eventAnnotation, dataViewFieldEditor, lens, triggersActionsUi, cases, observabilityShared, telemetry, advancedSettings, maps, exploratoryView, fleet, observability, banners, reporting, timelines, cloudSecurityPosture, runtimeFields, indexManagement, dashboardEnhanced, imageEmbeddable, graph, monitoring, securitySolution, synthetics, transform, uptime, cloudLinks, console, dataViewManagement, filesManagement, uiActions, visTypeVislib | - | +| | inspector, data, licensing, security, savedObjects, runtimeFields, indexManagement, dataViewEditor, unifiedSearch, embeddable, visualizations, controls, dashboard, savedObjectsTagging, dataViewFieldEditor, eventAnnotation, lens, triggersActionsUi, cases, observabilityShared, telemetry, advancedSettings, maps, exploratoryView, fleet, observability, banners, reporting, timelines, cloudSecurityPosture, dashboardEnhanced, imageEmbeddable, graph, monitoring, securitySolution, synthetics, uptime, cloudLinks, console, dataViewManagement, filesManagement, uiActions, visTypeVislib | - | | | observability, @kbn/securitysolution-data-table, securitySolution | - | | | @kbn/securitysolution-data-table, securitySolution | - | | | securitySolution | - | @@ -70,7 +70,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | @kbn/core-saved-objects-api-server-internal | - | | | @kbn/core-saved-objects-api-server-internal | - | | | @kbn/core-saved-objects-api-server-internal, canvas, @kbn/core-saved-objects-browser-internal | - | -| | @kbn/core-saved-objects-server-internal, @kbn/core-plugins-server-internal, savedObjectsTagging, @kbn/core-saved-objects-server-mocks | - | | | @kbn/core, kibanaUtils, expressions, data, savedObjectsTaggingOss, embeddable, visualizations, controls, savedObjectsTagging, uiActionsEnhanced, lens, maps, canvas, dashboardEnhanced, globalSearchProviders, @kbn/core-saved-objects-api-browser, savedObjects, savedObjectsManagement, eventAnnotation, graph, dashboard | - | | | @kbn/core-saved-objects-browser, @kbn/core-saved-objects-browser-internal, @kbn/core, home, savedObjects, visualizations, lens, visTypeTimeseries, @kbn/core-saved-objects-browser-mocks | - | | | @kbn/core-saved-objects-browser-internal, savedObjects, @kbn/core-saved-objects-browser-mocks | - | diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index d6a24b5788ad9..b09904070bf9a 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -107,7 +107,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [plugin_context.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts#:~:text=legacy), [plugin_context.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts#:~:text=legacy) | - | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/plugin.ts#:~:text=AsyncPlugin), [plugin.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/plugin.ts#:~:text=AsyncPlugin) | 8.8.0 | | | [plugin_manifest_parser.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.ts#:~:text=extraPublicDirs), [plugin_manifest_parser.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.ts#:~:text=extraPublicDirs), [plugin_manifest_parser.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.ts#:~:text=extraPublicDirs), [plugin_manifest_parser.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.ts#:~:text=extraPublicDirs), [plugin_manifest_parser.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.ts#:~:text=extraPublicDirs), [plugin_manifest_parser.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.ts#:~:text=extraPublicDirs) | - | -| | [plugin_context.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts#:~:text=getAllIndices), [plugin_context.ts](https://github.com/elastic/kibana/tree/main/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts#:~:text=getAllIndices) | - | @@ -266,15 +265,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [import_dashboards.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts#:~:text=migrationVersion) | - | | | [import_dashboards.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts#:~:text=migrationVersion), [import_dashboards.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts#:~:text=migrationVersion), [import_dashboards.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts#:~:text=migrationVersion), [import_dashboards.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts#:~:text=migrationVersion) | - | | | [collect_references_deep.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts#:~:text=SavedObjectAttributes), [collect_references_deep.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts#:~:text=SavedObjectAttributes), [collect_references_deep.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts#:~:text=SavedObjectAttributes), [collect_references_deep.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts#:~:text=SavedObjectAttributes) | - | -| | [saved_objects_service.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts#:~:text=getAllIndices) | - | - - - -## @kbn/core-saved-objects-server-mocks - -| Deprecated API | Reference location(s) | Remove By | -| ---------------|-----------|-----------| -| | [saved_objects_service.mock.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-mocks/src/saved_objects_service.mock.ts#:~:text=getAllIndices), [saved_objects_service.mock.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-mocks/src/saved_objects_service.mock.ts#:~:text=getAllIndices) | - | @@ -1398,7 +1388,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [request_handler_context.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/server/request_handler_context.ts#:~:text=authz) | - | | | [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/types.ts#:~:text=SavedObject), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/types.ts#:~:text=SavedObject), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.ts#:~:text=SavedObject), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.ts#:~:text=SavedObject), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.test.ts#:~:text=SavedObject), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.test.ts#:~:text=SavedObject), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.test.ts#:~:text=SavedObject), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts#:~:text=SavedObject), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts#:~:text=SavedObject), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts#:~:text=SavedObject)+ 3 more | - | | | [references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/references.ts#:~:text=SavedObjectReference), [references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/references.ts#:~:text=SavedObjectReference), [references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/references.ts#:~:text=SavedObjectReference), [references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/references.ts#:~:text=SavedObjectReference), [references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/references.ts#:~:text=SavedObjectReference), [references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/common/references.ts#:~:text=SavedObjectReference), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.ts#:~:text=SavedObjectReference), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.ts#:~:text=SavedObjectReference), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.ts#:~:text=SavedObjectReference), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/public/utils.ts#:~:text=SavedObjectReference)+ 11 more | - | -| | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/server/plugin.ts#:~:text=getAllIndices) | - | | | [tag.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/saved_objects_tagging/server/saved_objects/tag.ts#:~:text=convertToMultiNamespaceTypeVersion) | - | @@ -1493,16 +1482,16 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes), [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes) | - | | | [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject) | - | | | [timelines.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts#:~:text=convertToMultiNamespaceTypeVersion), [notes.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts#:~:text=convertToMultiNamespaceTypeVersion), [pinned_events.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts#:~:text=convertToMultiNamespaceTypeVersion), [legacy_saved_object_mappings.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_saved_object_mappings.ts#:~:text=convertToMultiNamespaceTypeVersion) | - | -| | [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [trusted_app_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [trusted_app_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [receiver.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [receiver.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID)+ 36 more | - | +| | [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [trusted_app_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [trusted_app_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [receiver.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [receiver.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID)+ 37 more | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME) | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION) | - | -| | [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [event_filter_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [event_filter_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [security_lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID)+ 34 more | - | +| | [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [event_filter_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [event_filter_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [security_lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID)+ 35 more | - | | | [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_NAME) | - | | | [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [create_event_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION) | - | -| | [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID)+ 18 more | - | +| | [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [host_isolation_exceptions_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID)+ 19 more | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/host_isolation_exceptions/index.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/host_isolation_exceptions/index.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME) | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/host_isolation_exceptions/index.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/host_isolation_exceptions/index.ts#:~:text=ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION) | - | -| | [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [blocklist_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [blocklist_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/blocklist/constants.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/blocklist/constants.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID)+ 16 more | - | +| | [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [blocklist_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [blocklist_validator.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/blocklist/constants.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/blocklist/constants.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_ID)+ 17 more | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/blocklist/constants.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_NAME), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/blocklist/constants.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/blocklists/index.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/blocklists/index.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_NAME) | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/blocklist/constants.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_DESCRIPTION), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/blocklist/constants.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/blocklists/index.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/blocklists/index.ts#:~:text=ENDPOINT_BLOCKLISTS_LIST_DESCRIPTION) | - | @@ -1615,10 +1604,9 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| | | [register_transform_health_rule_type.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts#:~:text=alertFactory) | - | -| | [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [es_index_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/services/es_index_service.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title), [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [es_index_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/services/es_index_service.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title) | - | -| | [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [es_index_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/services/es_index_service.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title), [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [es_index_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/services/es_index_service.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title) | - | -| | [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [es_index_service.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/services/es_index_service.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title) | - | -| | [toast_notification_text.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx#:~:text=toMountPoint), [toast_notification_text.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx#:~:text=toMountPoint), [step_create_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx#:~:text=toMountPoint), [step_create_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx#:~:text=toMountPoint), [step_create_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx#:~:text=toMountPoint), [step_create_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx#:~:text=toMountPoint), [step_create_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx#:~:text=toMountPoint), [step_details_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx#:~:text=toMountPoint), [step_details_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx#:~:text=toMountPoint), [step_details_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx#:~:text=toMountPoint)+ 19 more | - | +| | [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [use_data_view_exists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [use_data_view_exists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title) | - | +| | [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [use_data_view_exists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [use_data_view_exists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title) | - | +| | [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts#:~:text=title), [filter_term_form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx#:~:text=title), [use_data_view_exists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts#:~:text=title), [transforms.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/routes/api/transforms.ts#:~:text=title), [common.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts#:~:text=title) | - | | | [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/app.tsx#:~:text=KibanaThemeProvider), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/app.tsx#:~:text=KibanaThemeProvider), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/app.tsx#:~:text=KibanaThemeProvider) | - | | | [license.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/server/services/license.ts#:~:text=license%24) | 8.8.0 | | | [mount_management_section.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/transform/public/app/mount_management_section.ts#:~:text=savedObjects) | - | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index 4d9206678e728..bbdf4234fb74b 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index df3eec5b6c75f..6b0064c8f0858 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.devdocs.json b/api_docs/discover.devdocs.json index fbe23a8c3fc4a..13f444604cc4f 100644 --- a/api_docs/discover.devdocs.json +++ b/api_docs/discover.devdocs.json @@ -24,6 +24,188 @@ } ], "interfaces": [ + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService", + "type": "Interface", + "tags": [], + "label": "DiscoverCustomizationService", + "description": [], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService.set", + "type": "Function", + "tags": [], + "label": "set", + "description": [], + "signature": [ + "(customization: ", + { + "pluginId": "discover", + "scope": "public", + "docId": "kibDiscoverPluginApi", + "section": "def-public.DiscoverCustomization", + "text": "DiscoverCustomization" + }, + ") => void" + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService.set.$1", + "type": "CompoundType", + "tags": [], + "label": "customization", + "description": [], + "signature": [ + { + "pluginId": "discover", + "scope": "public", + "docId": "kibDiscoverPluginApi", + "section": "def-public.DiscoverCustomization", + "text": "DiscoverCustomization" + } + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService.get$", + "type": "Function", + "tags": [], + "label": "get$", + "description": [], + "signature": [ + "(id: TCustomizationId) => ", + "Observable", + " | Extract<", + { + "pluginId": "discover", + "scope": "public", + "docId": "kibDiscoverPluginApi", + "section": "def-public.TopNavCustomization", + "text": "TopNavCustomization" + }, + ", { id: TCustomizationId; }> | Extract<", + { + "pluginId": "discover", + "scope": "public", + "docId": "kibDiscoverPluginApi", + "section": "def-public.UnifiedHistogramCustomization", + "text": "UnifiedHistogramCustomization" + }, + ", { id: TCustomizationId; }> | undefined>" + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService.get$.$1", + "type": "Uncategorized", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "TCustomizationId" + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService.enable", + "type": "Function", + "tags": [], + "label": "enable", + "description": [], + "signature": [ + "(id: \"search_bar\" | \"top_nav\" | \"unified_histogram\") => void" + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService.enable.$1", + "type": "CompoundType", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "\"search_bar\" | \"top_nav\" | \"unified_histogram\"" + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService.disable", + "type": "Function", + "tags": [], + "label": "disable", + "description": [], + "signature": [ + "(id: \"search_bar\" | \"top_nav\" | \"unified_histogram\") => void" + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomizationService.disable.$1", + "type": "CompoundType", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "\"search_bar\" | \"top_nav\" | \"unified_histogram\"" + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "discover", "id": "def-public.DiscoverProfileOptions", @@ -387,6 +569,153 @@ } ], "initialIsOpen": false + }, + { + "parentPluginId": "discover", + "id": "def-public.SearchBarCustomization", + "type": "Interface", + "tags": [], + "label": "SearchBarCustomization", + "description": [], + "path": "src/plugins/discover/public/customizations/customization_types/search_bar_customization.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "discover", + "id": "def-public.SearchBarCustomization.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "\"search_bar\"" + ], + "path": "src/plugins/discover/public/customizations/customization_types/search_bar_customization.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.SearchBarCustomization.CustomDataViewPicker", + "type": "CompoundType", + "tags": [], + "label": "CustomDataViewPicker", + "description": [], + "signature": [ + "React.ComponentType<{}> | undefined" + ], + "path": "src/plugins/discover/public/customizations/customization_types/search_bar_customization.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.SearchBarCustomization.PrependFilterBar", + "type": "CompoundType", + "tags": [], + "label": "PrependFilterBar", + "description": [], + "signature": [ + "React.ComponentType<{}> | undefined" + ], + "path": "src/plugins/discover/public/customizations/customization_types/search_bar_customization.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.SearchBarCustomization.CustomSearchBar", + "type": "CompoundType", + "tags": [], + "label": "CustomSearchBar", + "description": [], + "signature": [ + "React.ComponentType<", + { + "pluginId": "navigation", + "scope": "public", + "docId": "kibNavigationPluginApi", + "section": "def-public.TopNavMenuProps", + "text": "TopNavMenuProps" + }, + "<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.AggregateQuery", + "text": "AggregateQuery" + }, + ">> | undefined" + ], + "path": "src/plugins/discover/public/customizations/customization_types/search_bar_customization.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "discover", + "id": "def-public.TopNavCustomization", + "type": "Interface", + "tags": [], + "label": "TopNavCustomization", + "description": [], + "path": "src/plugins/discover/public/customizations/customization_types/top_nav_customization.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "discover", + "id": "def-public.TopNavCustomization.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "\"top_nav\"" + ], + "path": "src/plugins/discover/public/customizations/customization_types/top_nav_customization.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.TopNavCustomization.defaultMenu", + "type": "Object", + "tags": [], + "label": "defaultMenu", + "description": [], + "signature": [ + "TopNavDefaultMenu", + " | undefined" + ], + "path": "src/plugins/discover/public/customizations/customization_types/top_nav_customization.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.TopNavCustomization.getMenuItems", + "type": "Function", + "tags": [], + "label": "getMenuItems", + "description": [], + "signature": [ + "(() => ", + "TopNavMenuItem", + "[]) | undefined" + ], + "path": "src/plugins/discover/public/customizations/customization_types/top_nav_customization.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false } ], "enums": [], @@ -433,7 +762,7 @@ "label": "DiscoverContainerProps", "description": [], "signature": [ - "{ overrideServices: Partial<", + "{ isLoading?: boolean | undefined; overrideServices: Partial<", "DiscoverServices", ">; scopedHistory: ", { @@ -443,7 +772,7 @@ "section": "def-common.ScopedHistory", "text": "ScopedHistory" }, - "; customize: ", + "; customizationCallbacks: ", { "pluginId": "discover", "scope": "public", @@ -451,13 +780,50 @@ "section": "def-public.CustomizationCallback", "text": "CustomizationCallback" }, - "; }" + "[]; }" ], "path": "src/plugins/discover/public/components/discover_container/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "discover", + "id": "def-public.DiscoverCustomization", + "type": "Type", + "tags": [], + "label": "DiscoverCustomization", + "description": [], + "signature": [ + { + "pluginId": "discover", + "scope": "public", + "docId": "kibDiscoverPluginApi", + "section": "def-public.SearchBarCustomization", + "text": "SearchBarCustomization" + }, + " | ", + { + "pluginId": "discover", + "scope": "public", + "docId": "kibDiscoverPluginApi", + "section": "def-public.TopNavCustomization", + "text": "TopNavCustomization" + }, + " | ", + { + "pluginId": "discover", + "scope": "public", + "docId": "kibDiscoverPluginApi", + "section": "def-public.UnifiedHistogramCustomization", + "text": "UnifiedHistogramCustomization" + } + ], + "path": "src/plugins/discover/public/customizations/customization_service.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "discover", "id": "def-public.DiscoverProfileId", @@ -588,6 +954,29 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false + }, + { + "parentPluginId": "discover", + "id": "def-public.UnifiedHistogramCustomization", + "type": "Type", + "tags": [], + "label": "UnifiedHistogramCustomization", + "description": [], + "signature": [ + "UnifiedHistogramCustomizationId & Pick<", + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramContainerProps", + "text": "UnifiedHistogramContainerProps" + }, + ", \"onBrushEnd\" | \"disabledActions\" | \"onFilter\" | \"withDefaultActions\">" + ], + "path": "src/plugins/discover/public/customizations/customization_types/histogram_customization.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false } ], "objects": [], @@ -602,24 +991,6 @@ "deprecated": false, "trackAdoption": false, "children": [ - { - "parentPluginId": "discover", - "id": "def-public.DiscoverSetup.docViews", - "type": "Object", - "tags": [], - "label": "docViews", - "description": [], - "signature": [ - "{ addDocView(docViewRaw: ", - "DocViewInput", - " | ", - "DocViewInputFn", - "): void; }" - ], - "path": "src/plugins/discover/public/plugin.tsx", - "deprecated": false, - "trackAdoption": false - }, { "parentPluginId": "discover", "id": "def-public.DiscoverSetup.locator", diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index 9251bed21f4e0..a63ae77760646 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 79 | 0 | 52 | 15 | +| 98 | 0 | 71 | 15 | ## Client diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index f7f82769628b9..675890ffa1ac8 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/ecs_data_quality_dashboard.mdx b/api_docs/ecs_data_quality_dashboard.mdx index f7dae10eb09b4..90e742bda6198 100644 --- a/api_docs/ecs_data_quality_dashboard.mdx +++ b/api_docs/ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ecsDataQualityDashboard title: "ecsDataQualityDashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the ecsDataQualityDashboard plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ecsDataQualityDashboard'] --- import ecsDataQualityDashboardObj from './ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/elastic_assistant.mdx b/api_docs/elastic_assistant.mdx index b54c2cf5e5cac..ace6dbb2a4c7d 100644 --- a/api_docs/elastic_assistant.mdx +++ b/api_docs/elastic_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/elasticAssistant title: "elasticAssistant" image: https://source.unsplash.com/400x175/?github description: API docs for the elasticAssistant plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'elasticAssistant'] --- import elasticAssistantObj from './elastic_assistant.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 3833618281f9c..be41e8d5bc699 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index a35dd8b2a6f39..8fd457f066ffe 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index 4f5b0901138fc..cf6cd38a5251c 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 666593267385b..07418305cdc71 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index a623814d2d906..f5aaa1ac5bd5c 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index e48a46cfcb57a..d72d41002cf5d 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 09c8d934dfea1..edabbf7f2b416 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/exploratory_view.mdx b/api_docs/exploratory_view.mdx index 2fc551f75db51..d977e9d3c6444 100644 --- a/api_docs/exploratory_view.mdx +++ b/api_docs/exploratory_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/exploratoryView title: "exploratoryView" image: https://source.unsplash.com/400x175/?github description: API docs for the exploratoryView plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'exploratoryView'] --- import exploratoryViewObj from './exploratory_view.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index ffadc334f7246..cf353aeb1663c 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 099b5e151ad5b..4dc1d165e75fb 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 50567fb3c9bd4..bd676f00bd162 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 511647d6a7650..e07f5ea5cd63f 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index 5a8071726a871..38013982ab985 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 84d8834f2e1a9..0e8902fbf0a0f 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index bd596eef5f64b..fcae81f5883cf 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 012b3c8283002..039b050b32bc6 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index 5e5fa0092ce64..9f2f3434acb56 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index 5a3567bbaa35b..8f9d323b53023 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index d75699a0bc67b..f3e7d5eeaee3c 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index 3a2af466166ea..214d3d21153e2 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index 0b67bdbb64966..7f245d3889e1f 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.devdocs.json b/api_docs/expressions.devdocs.json index 8396810919408..8bd5ba8e59463 100644 --- a/api_docs/expressions.devdocs.json +++ b/api_docs/expressions.devdocs.json @@ -6975,6 +6975,20 @@ "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "expressions", + "id": "def-public.Datatable.warning", + "type": "string", + "tags": [], + "label": "warning", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -7499,7 +7513,7 @@ "\nTracks state of execution.\n\n- `not-started` - before .start() method was called.\n- `pending` - immediately after .start() method is called.\n- `result` - when expression execution completed.\n- `error` - when execution failed with error." ], "signature": [ - "\"error\" | \"pending\" | \"not-started\" | \"result\"" + "\"error\" | \"pending\" | \"result\" | \"not-started\"" ], "path": "src/plugins/expressions/common/execution/container.ts", "deprecated": false, @@ -11686,7 +11700,7 @@ "label": "padding", "description": [], "signature": [ - "\"m\" | \"s\" | \"xs\" | \"l\" | \"xl\" | undefined" + "\"m\" | \"s\" | \"l\" | \"xs\" | \"xl\" | undefined" ], "path": "src/plugins/expressions/public/react_expression_renderer/react_expression_renderer.tsx", "deprecated": false, @@ -18368,6 +18382,20 @@ "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "expressions", + "id": "def-server.Datatable.warning", + "type": "string", + "tags": [], + "label": "warning", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -18892,7 +18920,7 @@ "\nTracks state of execution.\n\n- `not-started` - before .start() method was called.\n- `pending` - immediately after .start() method is called.\n- `result` - when expression execution completed.\n- `error` - when execution failed with error." ], "signature": [ - "\"error\" | \"pending\" | \"not-started\" | \"result\"" + "\"error\" | \"pending\" | \"result\" | \"not-started\"" ], "path": "src/plugins/expressions/common/execution/container.ts", "deprecated": false, @@ -30133,6 +30161,20 @@ "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "expressions", + "id": "def-common.Datatable.warning", + "type": "string", + "tags": [], + "label": "warning", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -31189,7 +31231,7 @@ "\nTracks state of execution.\n\n- `not-started` - before .start() method was called.\n- `pending` - immediately after .start() method is called.\n- `result` - when expression execution completed.\n- `error` - when execution failed with error." ], "signature": [ - "\"error\" | \"pending\" | \"not-started\" | \"result\"" + "\"error\" | \"pending\" | \"result\" | \"not-started\"" ], "path": "src/plugins/expressions/common/execution/container.ts", "deprecated": false, @@ -39366,7 +39408,7 @@ }, "[]; meta?: ", "DatatableMeta", - " | undefined; }" + " | undefined; warning?: string | undefined; }" ], "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, @@ -39424,7 +39466,7 @@ }, "[]; meta?: ", "DatatableMeta", - " | undefined; }" + " | undefined; warning?: string | undefined; }" ], "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, @@ -41815,7 +41857,7 @@ }, " | { [x: string]: any; })[]; type: \"datatable\"; meta?: ", "DatatableMeta", - " | undefined; }>" + " | undefined; warning?: string | undefined; }>" ], "path": "src/plugins/expressions/common/expression_functions/specs/map_column.ts", "deprecated": false, diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 06f029f56890d..ce5cf4cc0939f 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2205 | 17 | 1746 | 5 | +| 2208 | 17 | 1749 | 5 | ## Client diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 4cfb788a38eaa..db13ee18b015e 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index a236ab0be0474..bc8dff8530b8e 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 23c3684b63bef..7cb2ca900ab10 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.mdx b/api_docs/files.mdx index 9c5b048df424c..54c4bb33663b7 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index 74a3b30e990d2..fe86c6e0074a2 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index d4770a5393235..58bf0969ecb00 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index c8dc6be2ffc7a..53425f7e23d24 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index 9cf887d5ed9c9..a1230a39ce3f2 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 2384ea3a83b81..9915ed0e2802a 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/image_embeddable.mdx b/api_docs/image_embeddable.mdx index 3a4b6ea650395..586539ca0d0aa 100644 --- a/api_docs/image_embeddable.mdx +++ b/api_docs/image_embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/imageEmbeddable title: "imageEmbeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the imageEmbeddable plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'imageEmbeddable'] --- import imageEmbeddableObj from './image_embeddable.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index f35561697892e..c394ca0f93615 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 61ea64ac08109..25a6849dd773c 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index e9596cddc338c..0cccce355c006 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index e0a6543f30b6f..5b47285b9edcd 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 72cc862872291..c3c4259afdfb1 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index 3c5907d8de45c..9d13f55b01c3d 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ 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: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index 53fecfd5a7ddd..6e51456817f54 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index c8974ddedaf08..9bb30d89b5db3 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerting_api_integration_helpers.mdx b/api_docs/kbn_alerting_api_integration_helpers.mdx index d37c1916e8378..c60de50828f02 100644 --- a/api_docs/kbn_alerting_api_integration_helpers.mdx +++ b/api_docs/kbn_alerting_api_integration_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-api-integration-helpers title: "@kbn/alerting-api-integration-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-api-integration-helpers plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-api-integration-helpers'] --- import kbnAlertingApiIntegrationHelpersObj from './kbn_alerting_api_integration_helpers.devdocs.json'; diff --git a/api_docs/kbn_alerting_state_types.devdocs.json b/api_docs/kbn_alerting_state_types.devdocs.json index ada44596c7d8f..df8c83dfdc58a 100644 --- a/api_docs/kbn_alerting_state_types.devdocs.json +++ b/api_docs/kbn_alerting_state_types.devdocs.json @@ -114,105 +114,135 @@ }, { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.AlertInstanceMeta", + "id": "def-common.LatestAlertInstanceMetaSchema", "type": "Type", "tags": [], - "label": "AlertInstanceMeta", + "label": "LatestAlertInstanceMetaSchema", "description": [], "signature": [ - "{ lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; uuid?: string | undefined; }" + "{ readonly uuid?: string | undefined; readonly lastScheduledActions?: Readonly<{ actions?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; readonly flappingHistory?: boolean[] | undefined; readonly flapping?: boolean | undefined; readonly maintenanceWindowIds?: string[] | undefined; readonly pendingRecoveredCount?: number | undefined; }" ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.AlertInstanceState", + "id": "def-common.LatestAlertInstanceStateSchema", "type": "Type", "tags": [], - "label": "AlertInstanceState", + "label": "LatestAlertInstanceStateSchema", "description": [], "signature": [ - "{ [x: string]: unknown; }" + "{ [x: string]: any; }" ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.LastScheduledActions", + "id": "def-common.LatestLastScheduledActionsSchema", "type": "Type", "tags": [], - "label": "LastScheduledActions", + "label": "LatestLastScheduledActionsSchema", "description": [], "signature": [ - "{ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }" + "{ readonly actions?: Record> | undefined; readonly subgroup?: string | undefined; readonly date: string; readonly group: string; }" ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.RawAlertInstance", + "id": "def-common.LatestRawAlertInstanceSchema", "type": "Type", "tags": [], - "label": "RawAlertInstance", + "label": "LatestRawAlertInstanceSchema", "description": [], "signature": [ - "{ state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; uuid?: string | undefined; } | undefined; }" + "{ readonly meta?: Readonly<{ uuid?: string | undefined; lastScheduledActions?: Readonly<{ actions?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; readonly state?: Record | undefined; }" ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.RuleTaskParams", + "id": "def-common.LatestTaskStateSchema", "type": "Type", "tags": [], - "label": "RuleTaskParams", + "label": "LatestTaskStateSchema", "description": [], "signature": [ - "{ alertId: string; } & { spaceId?: string | undefined; }" + "{ readonly alertTypeState?: Record | undefined; readonly alertInstances?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined; readonly alertRecoveredInstances?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined; readonly previousStartedAt?: string | null | undefined; readonly summaryActions?: Record> | undefined; }" ], - "path": "x-pack/packages/kbn-alerting-state-types/src/rule_task_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.RuleTaskState", + "id": "def-common.LatestThrottledActionSchema", "type": "Type", "tags": [], - "label": "RuleTaskState", + "label": "LatestThrottledActionSchema", "description": [], "signature": [ - "{ alertTypeState?: { [x: string]: unknown; } | undefined; alertInstances?: { [x: string]: { state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; uuid?: string | undefined; } | undefined; }; } | undefined; alertRecoveredInstances?: { [x: string]: { state?: { [x: string]: unknown; } | undefined; meta?: { lastScheduledActions?: ({ subgroup?: string | undefined; } & { group: string; date: Date; } & { actions?: { [x: string]: { date: Date; }; } | undefined; }) | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; uuid?: string | undefined; } | undefined; }; } | undefined; previousStartedAt?: Date | null | undefined; summaryActions?: { [x: string]: { date: Date; }; } | undefined; }" + "{ [x: string]: Readonly<{} & { date: string; }>; }" ], - "path": "x-pack/packages/kbn-alerting-state-types/src/rule_task_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.ThrottledActions", + "id": "def-common.MutableLatestAlertInstanceMetaSchema", "type": "Type", "tags": [], - "label": "ThrottledActions", + "label": "MutableLatestAlertInstanceMetaSchema", "description": [], "signature": [ - "{ [x: string]: { date: Date; }; }" + "{ uuid?: Mutable; lastScheduledActions?: Mutable> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined>; flappingHistory?: Mutable; flapping?: Mutable; maintenanceWindowIds?: Mutable; pendingRecoveredCount?: Mutable; }" ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/alerting-state-types", + "id": "def-common.MutableLatestTaskStateSchema", + "type": "Type", + "tags": [], + "label": "MutableLatestTaskStateSchema", + "description": [], + "signature": [ + "{ alertTypeState?: Mutable | undefined>; alertInstances?: Mutable> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined>; alertRecoveredInstances?: Mutable> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined>; previousStartedAt?: Mutable; summaryActions?: Mutable> | undefined>; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/alerting-state-types", + "id": "def-common.RuleTaskParams", + "type": "Type", + "tags": [], + "label": "RuleTaskParams", + "description": [], + "signature": [ + "{ alertId: string; } & { spaceId?: string | undefined; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/rule_task_instance.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -253,78 +283,78 @@ "objects": [ { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.DateFromString", + "id": "def-common.emptyState", "type": "Object", "tags": [], - "label": "DateFromString", + "label": "emptyState", "description": [], - "signature": [ - "Type", - "" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/date_from_string.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.rawAlertInstance", - "type": "Object", - "tags": [], - "label": "rawAlertInstance", - "description": [], - "signature": [ - "PartialC", - "<{ state: ", - "RecordC", - "<", - "StringC", - ", ", - "UnknownC", - ">; meta: ", - "PartialC", - "<{ lastScheduledActions: ", - "IntersectionC", - "<[", - "PartialC", - "<{ subgroup: ", - "StringC", - "; }>, ", - "TypeC", - "<{ group: ", - "StringC", - "; date: ", - "Type", - "; }>, ", - "PartialC", - "<{ actions: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ date: ", - "Type", - "; }>>; }>]>; flappingHistory: ", - "ArrayC", - "<", - "BooleanC", - ">; flapping: ", - "BooleanC", - "; maintenanceWindowIds: ", - "ArrayC", - "<", - "StringC", - ">; pendingRecoveredCount: ", - "NumberC", - "; uuid: ", - "StringC", - "; }>; }>" + "children": [ + { + "parentPluginId": "@kbn/alerting-state-types", + "id": "def-common.emptyState.alertTypeState", + "type": "Object", + "tags": [], + "label": "alertTypeState", + "description": [], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [] + }, + { + "parentPluginId": "@kbn/alerting-state-types", + "id": "def-common.emptyState.alertInstances", + "type": "Object", + "tags": [], + "label": "alertInstances", + "description": [], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [] + }, + { + "parentPluginId": "@kbn/alerting-state-types", + "id": "def-common.emptyState.alertRecoveredInstances", + "type": "Object", + "tags": [], + "label": "alertRecoveredInstances", + "description": [], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [] + }, + { + "parentPluginId": "@kbn/alerting-state-types", + "id": "def-common.emptyState.previousStartedAt", + "type": "Uncategorized", + "tags": [], + "label": "previousStartedAt", + "description": [], + "signature": [ + "null" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/alerting-state-types", + "id": "def-common.emptyState.summaryActions", + "type": "Object", + "tags": [], + "label": "summaryActions", + "description": [], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [] + } ], - "path": "x-pack/packages/kbn-alerting-state-types/src/alert_instance.ts", - "deprecated": false, - "trackAdoption": false, "initialIsOpen": false }, { @@ -353,138 +383,78 @@ }, { "parentPluginId": "@kbn/alerting-state-types", - "id": "def-common.ruleStateSchema", + "id": "def-common.stateSchemaByVersion", "type": "Object", "tags": [], - "label": "ruleStateSchema", + "label": "stateSchemaByVersion", "description": [], - "signature": [ - "PartialC", - "<{ alertTypeState: ", - "RecordC", - "<", - "StringC", - ", ", - "UnknownC", - ">; alertInstances: ", - "RecordC", - "<", - "StringC", - ", ", - "PartialC", - "<{ state: ", - "RecordC", - "<", - "StringC", - ", ", - "UnknownC", - ">; meta: ", - "PartialC", - "<{ lastScheduledActions: ", - "IntersectionC", - "<[", - "PartialC", - "<{ subgroup: ", - "StringC", - "; }>, ", - "TypeC", - "<{ group: ", - "StringC", - "; date: ", - "Type", - "; }>, ", - "PartialC", - "<{ actions: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ date: ", - "Type", - "; }>>; }>]>; flappingHistory: ", - "ArrayC", - "<", - "BooleanC", - ">; flapping: ", - "BooleanC", - "; maintenanceWindowIds: ", - "ArrayC", - "<", - "StringC", - ">; pendingRecoveredCount: ", - "NumberC", - "; uuid: ", - "StringC", - "; }>; }>>; alertRecoveredInstances: ", - "RecordC", - "<", - "StringC", - ", ", - "PartialC", - "<{ state: ", - "RecordC", - "<", - "StringC", - ", ", - "UnknownC", - ">; meta: ", - "PartialC", - "<{ lastScheduledActions: ", - "IntersectionC", - "<[", - "PartialC", - "<{ subgroup: ", - "StringC", - "; }>, ", - "TypeC", - "<{ group: ", - "StringC", - "; date: ", - "Type", - "; }>, ", - "PartialC", - "<{ actions: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ date: ", - "Type", - "; }>>; }>]>; flappingHistory: ", - "ArrayC", - "<", - "BooleanC", - ">; flapping: ", - "BooleanC", - "; maintenanceWindowIds: ", - "ArrayC", - "<", - "StringC", - ">; pendingRecoveredCount: ", - "NumberC", - "; uuid: ", - "StringC", - "; }>; }>>; previousStartedAt: ", - "UnionC", - "<[", - "NullC", - ", ", - "Type", - "]>; summaryActions: ", - "RecordC", - "<", - "StringC", - ", ", - "TypeC", - "<{ date: ", - "Type", - "; }>>; }>" - ], - "path": "x-pack/packages/kbn-alerting-state-types/src/rule_task_instance.ts", + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", "deprecated": false, "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/alerting-state-types", + "id": "def-common.stateSchemaByVersion.1", + "type": "Object", + "tags": [], + "label": "1", + "description": [], + "signature": [ + "{ up: (state: Record) => Readonly<{ alertTypeState?: Record | undefined; alertInstances?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined; alertRecoveredInstances?: Record> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined; previousStartedAt?: string | null | undefined; summaryActions?: Record> | undefined; } & {}>; schema: ", + { + "pluginId": "@kbn/config-schema", + "scope": "common", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-common.ObjectType", + "text": "ObjectType" + }, + "<{ alertTypeState: ", + { + "pluginId": "@kbn/config-schema", + "scope": "common", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-common.Type", + "text": "Type" + }, + " | undefined>; alertInstances: ", + { + "pluginId": "@kbn/config-schema", + "scope": "common", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-common.Type", + "text": "Type" + }, + "> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined>; alertRecoveredInstances: ", + { + "pluginId": "@kbn/config-schema", + "scope": "common", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-common.Type", + "text": "Type" + }, + "> | undefined; subgroup?: string | undefined; } & { date: string; group: string; }> | undefined; flappingHistory?: boolean[] | undefined; flapping?: boolean | undefined; maintenanceWindowIds?: string[] | undefined; pendingRecoveredCount?: number | undefined; } & {}> | undefined; state?: Record | undefined; } & {}>> | undefined>; previousStartedAt: ", + { + "pluginId": "@kbn/config-schema", + "scope": "common", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-common.Type", + "text": "Type" + }, + "; summaryActions: ", + { + "pluginId": "@kbn/config-schema", + "scope": "common", + "docId": "kibKbnConfigSchemaPluginApi", + "section": "def-common.Type", + "text": "Type" + }, + "> | undefined>; }>; }" + ], + "path": "x-pack/packages/kbn-alerting-state-types/src/task_state/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], "initialIsOpen": false } ] diff --git a/api_docs/kbn_alerting_state_types.mdx b/api_docs/kbn_alerting_state_types.mdx index 364e1880c198f..45459e55d57d2 100644 --- a/api_docs/kbn_alerting_state_types.mdx +++ b/api_docs/kbn_alerting_state_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-state-types title: "@kbn/alerting-state-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-state-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-state-types'] --- import kbnAlertingStateTypesObj from './kbn_alerting_state_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 16 | 0 | 15 | 0 | +| 23 | 0 | 22 | 0 | ## Common diff --git a/api_docs/kbn_alerts_as_data_utils.devdocs.json b/api_docs/kbn_alerts_as_data_utils.devdocs.json index 01bc8dd24d62c..2d2e9c678ef98 100644 --- a/api_docs/kbn_alerts_as_data_utils.devdocs.json +++ b/api_docs/kbn_alerts_as_data_utils.devdocs.json @@ -19,6 +19,54 @@ "common": { "classes": [], "functions": [ + { + "parentPluginId": "@kbn/alerts-as-data-utils", + "id": "def-common.buildAlertFieldsRequest", + "type": "Function", + "tags": [], + "label": "buildAlertFieldsRequest", + "description": [], + "signature": [ + "(fields: string[], excludeEcsData?: boolean | undefined) => { format?: string | undefined; field: string; include_unmapped: boolean; }[]" + ], + "path": "packages/kbn-alerts-as-data-utils/src/search/security/build_fields_request.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/alerts-as-data-utils", + "id": "def-common.buildAlertFieldsRequest.$1", + "type": "Array", + "tags": [], + "label": "fields", + "description": [], + "signature": [ + "string[]" + ], + "path": "packages/kbn-alerts-as-data-utils/src/search/security/build_fields_request.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/alerts-as-data-utils", + "id": "def-common.buildAlertFieldsRequest.$2", + "type": "CompoundType", + "tags": [], + "label": "excludeEcsData", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-alerts-as-data-utils/src/search/security/build_fields_request.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/alerts-as-data-utils", "id": "def-common.createSchemaFromFieldMap", @@ -170,6 +218,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/alerts-as-data-utils", + "id": "def-common.ALERT_EVENTS_FIELDS", + "type": "Array", + "tags": [], + "label": "ALERT_EVENTS_FIELDS", + "description": [], + "signature": [ + "string[]" + ], + "path": "packages/kbn-alerts-as-data-utils/src/search/security/fields.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/alerts-as-data-utils", "id": "def-common.AlertFieldMap", diff --git a/api_docs/kbn_alerts_as_data_utils.mdx b/api_docs/kbn_alerts_as_data_utils.mdx index d631150c0a90f..64b3d4ae9600c 100644 --- a/api_docs/kbn_alerts_as_data_utils.mdx +++ b/api_docs/kbn_alerts_as_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-as-data-utils title: "@kbn/alerts-as-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-as-data-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-as-data-utils'] --- import kbnAlertsAsDataUtilsObj from './kbn_alerts_as_data_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 25 | 0 | 25 | 0 | +| 29 | 0 | 29 | 0 | ## Common diff --git a/api_docs/kbn_alerts_ui_shared.devdocs.json b/api_docs/kbn_alerts_ui_shared.devdocs.json index 77aa446d1ecfb..a63af81c30848 100644 --- a/api_docs/kbn_alerts_ui_shared.devdocs.json +++ b/api_docs/kbn_alerts_ui_shared.devdocs.json @@ -19,6 +19,39 @@ "common": { "classes": [], "functions": [ + { + "parentPluginId": "@kbn/alerts-ui-shared", + "id": "def-common.AddMessageVariables", + "type": "Function", + "tags": [], + "label": "AddMessageVariables", + "description": [], + "signature": [ + "({ buttonTitle, messageVariables, paramsProperty, onSelectEventHandler, showButtonTitle, }: React.PropsWithChildren) => JSX.Element" + ], + "path": "packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/alerts-ui-shared", + "id": "def-common.AddMessageVariables.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n buttonTitle,\n messageVariables,\n paramsProperty,\n onSelectEventHandler,\n showButtonTitle = false,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/alerts-ui-shared", "id": "def-common.AlertLifecycleStatusBadge", diff --git a/api_docs/kbn_alerts_ui_shared.mdx b/api_docs/kbn_alerts_ui_shared.mdx index f6099e3a94a3a..0387bc14aa30c 100644 --- a/api_docs/kbn_alerts_ui_shared.mdx +++ b/api_docs/kbn_alerts_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-ui-shared title: "@kbn/alerts-ui-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-ui-shared plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-ui-shared'] --- import kbnAlertsUiSharedObj from './kbn_alerts_ui_shared.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 8 | 0 | 7 | 1 | +| 10 | 0 | 9 | 1 | ## Common diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index ecef44beb6612..a2dfb1d49ac93 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index fa2f5ee2548fd..c36b7b83b967c 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index 992023681a5b7..053d4313a1174 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index d469e52483593..5b8013b126b1a 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index fe0c82020289a..c2055ef8d02bc 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index 29577b4aa6140..df61103a09941 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_gainsight.mdx b/api_docs/kbn_analytics_shippers_gainsight.mdx index c495acdb88da5..fb844e6b7c754 100644 --- a/api_docs/kbn_analytics_shippers_gainsight.mdx +++ b/api_docs/kbn_analytics_shippers_gainsight.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-gainsight title: "@kbn/analytics-shippers-gainsight" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-gainsight plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-gainsight'] --- import kbnAnalyticsShippersGainsightObj from './kbn_analytics_shippers_gainsight.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index ddb75b94a1d03..30c1a22a8ec4a 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index feaecf2c009a5..09007f8b0e82e 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace_client.mdx b/api_docs/kbn_apm_synthtrace_client.mdx index 0b1c54700b357..a04a564cde49c 100644 --- a/api_docs/kbn_apm_synthtrace_client.mdx +++ b/api_docs/kbn_apm_synthtrace_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace-client title: "@kbn/apm-synthtrace-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace-client plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace-client'] --- import kbnApmSynthtraceClientObj from './kbn_apm_synthtrace_client.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 30662ff02b580..ce523adf57985 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index c8d93573025a8..9f4d642a9f4ef 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 03fe88c6c2db0..82da938a2e3b9 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_cell_actions.mdx b/api_docs/kbn_cell_actions.mdx index a1c1fd44d27b8..b14c9ededd480 100644 --- a/api_docs/kbn_cell_actions.mdx +++ b/api_docs/kbn_cell_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cell-actions title: "@kbn/cell-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cell-actions plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cell-actions'] --- import kbnCellActionsObj from './kbn_cell_actions.devdocs.json'; diff --git a/api_docs/kbn_chart_expressions_common.mdx b/api_docs/kbn_chart_expressions_common.mdx index 717ee4f9d9bda..e49db93b653a5 100644 --- a/api_docs/kbn_chart_expressions_common.mdx +++ b/api_docs/kbn_chart_expressions_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-expressions-common title: "@kbn/chart-expressions-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-expressions-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-expressions-common'] --- import kbnChartExpressionsCommonObj from './kbn_chart_expressions_common.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index ec7b964886ed3..6b8ed504dc92d 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 1fa4cea3f980d..a5c29e677ec23 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index bf0a79fc42361..50523b6543892 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index b831fb8a168bf..30878509c9908 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index 5cae7304d6845..001c90e047fae 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_code_editor.devdocs.json b/api_docs/kbn_code_editor.devdocs.json index 97a003dc517d0..eea9517c0bb23 100644 --- a/api_docs/kbn_code_editor.devdocs.json +++ b/api_docs/kbn_code_editor.devdocs.json @@ -27,7 +27,7 @@ "label": "CodeEditor", "description": [], "signature": [ - "({ languageId, value, onChange, width, options, overrideEditorWillMount, editorDidMount, editorWillMount, useDarkTheme: useDarkThemeProp, transparentBackground, suggestionProvider, signatureProvider, hoverProvider, placeholder, languageConfiguration, \"aria-label\": ariaLabel, isCopyable, allowFullScreen, }: React.PropsWithChildren<", + "({ languageId, value, onChange, width, height, options, overrideEditorWillMount, editorDidMount, editorWillMount, useDarkTheme: useDarkThemeProp, transparentBackground, suggestionProvider, signatureProvider, hoverProvider, placeholder, languageConfiguration, \"aria-label\": ariaLabel, isCopyable, allowFullScreen, }: React.PropsWithChildren<", "Props", ">) => JSX.Element" ], @@ -40,7 +40,7 @@ "id": "def-common.CodeEditor.$1", "type": "CompoundType", "tags": [], - "label": "{\n languageId,\n value,\n onChange,\n width,\n options,\n overrideEditorWillMount,\n editorDidMount,\n editorWillMount,\n useDarkTheme: useDarkThemeProp,\n transparentBackground,\n suggestionProvider,\n signatureProvider,\n hoverProvider,\n placeholder,\n languageConfiguration,\n 'aria-label': ariaLabel = i18n.translate('sharedUXPackages.codeEditor.ariaLabel', {\n defaultMessage: 'Code Editor',\n }),\n isCopyable = false,\n allowFullScreen = false,\n}", + "label": "{\n languageId,\n value,\n onChange,\n width,\n height = '100px',\n options,\n overrideEditorWillMount,\n editorDidMount,\n editorWillMount,\n useDarkTheme: useDarkThemeProp,\n transparentBackground,\n suggestionProvider,\n signatureProvider,\n hoverProvider,\n placeholder,\n languageConfiguration,\n 'aria-label': ariaLabel = i18n.translate('sharedUXPackages.codeEditor.ariaLabel', {\n defaultMessage: 'Code Editor',\n }),\n isCopyable = false,\n allowFullScreen = false,\n}", "description": [], "signature": [ "React.PropsWithChildren<", diff --git a/api_docs/kbn_code_editor.mdx b/api_docs/kbn_code_editor.mdx index 7b3670e6fde6c..909659dcf4b32 100644 --- a/api_docs/kbn_code_editor.mdx +++ b/api_docs/kbn_code_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor title: "@kbn/code-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor'] --- import kbnCodeEditorObj from './kbn_code_editor.devdocs.json'; diff --git a/api_docs/kbn_code_editor_mocks.mdx b/api_docs/kbn_code_editor_mocks.mdx index 883e7b41fdf2b..4d1b33ff13923 100644 --- a/api_docs/kbn_code_editor_mocks.mdx +++ b/api_docs/kbn_code_editor_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor-mocks title: "@kbn/code-editor-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor-mocks'] --- import kbnCodeEditorMocksObj from './kbn_code_editor_mocks.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index e145f92192da3..2d3190c184aa0 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 49ccc8f40a321..a044b568a4c6d 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.devdocs.json b/api_docs/kbn_config_mocks.devdocs.json index df37ecf488422..29e17d6500043 100644 --- a/api_docs/kbn_config_mocks.devdocs.json +++ b/api_docs/kbn_config_mocks.devdocs.json @@ -344,7 +344,15 @@ "section": "def-common.ChangedDeprecatedPaths", "text": "ChangedDeprecatedPaths" }, - ">, [], unknown>; } & ", + ">, [], unknown>; addDynamicConfigPaths: jest.MockInstance; setDynamicConfigOverrides: jest.MockInstance], unknown>; } & ", "IConfigService" ], "path": "packages/kbn-config-mocks/src/config_service.mock.ts", diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index 857ac58bf99a5..7dff56ea557ef 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 0a55f16979007..d98cbfcac9f03 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_editor.mdx b/api_docs/kbn_content_management_content_editor.mdx index 6656664fcefff..571943ac43efc 100644 --- a/api_docs/kbn_content_management_content_editor.mdx +++ b/api_docs/kbn_content_management_content_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-editor title: "@kbn/content-management-content-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-editor plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-editor'] --- import kbnContentManagementContentEditorObj from './kbn_content_management_content_editor.devdocs.json'; diff --git a/api_docs/kbn_content_management_tabbed_table_list_view.mdx b/api_docs/kbn_content_management_tabbed_table_list_view.mdx index ec829c7248be6..f840d96fb96a8 100644 --- a/api_docs/kbn_content_management_tabbed_table_list_view.mdx +++ b/api_docs/kbn_content_management_tabbed_table_list_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-tabbed-table-list-view title: "@kbn/content-management-tabbed-table-list-view" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-tabbed-table-list-view plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-tabbed-table-list-view'] --- import kbnContentManagementTabbedTableListViewObj from './kbn_content_management_tabbed_table_list_view.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list_view.mdx b/api_docs/kbn_content_management_table_list_view.mdx index da0372ad77175..80dc326e0b499 100644 --- a/api_docs/kbn_content_management_table_list_view.mdx +++ b/api_docs/kbn_content_management_table_list_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list-view title: "@kbn/content-management-table-list-view" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list-view plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view'] --- import kbnContentManagementTableListViewObj from './kbn_content_management_table_list_view.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list_view_table.mdx b/api_docs/kbn_content_management_table_list_view_table.mdx index f4f02e0699ed3..59ee0aee26e27 100644 --- a/api_docs/kbn_content_management_table_list_view_table.mdx +++ b/api_docs/kbn_content_management_table_list_view_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list-view-table title: "@kbn/content-management-table-list-view-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list-view-table plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view-table'] --- import kbnContentManagementTableListViewTableObj from './kbn_content_management_table_list_view_table.devdocs.json'; diff --git a/api_docs/kbn_content_management_utils.mdx b/api_docs/kbn_content_management_utils.mdx index 60e161f3b7873..da3749b332c70 100644 --- a/api_docs/kbn_content_management_utils.mdx +++ b/api_docs/kbn_content_management_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-utils title: "@kbn/content-management-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-utils'] --- import kbnContentManagementUtilsObj from './kbn_content_management_utils.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 0f19d1757debe..abf01e9b87809 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index 65ae7d9d5c525..eeafd6d571c92 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index fd06213dc58c4..56a4f9835d002 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 273d35d08fe5b..d62cfc6e67a36 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index 50dfec92d8cc8..c734b69e04e11 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index 2796d4b9348a5..af02ffe0dc431 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index 5343660f95030..0c91163570254 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index 514788b96f802..2b4ca8a154b87 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index bb65de3706a63..839b32626a245 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index 771ddee3d32b2..498d5d98e4a90 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index f41a779f11ecf..53cde53b4ed52 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index 877911f915135..211b46e09eabd 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_apps_server_internal.devdocs.json b/api_docs/kbn_core_apps_server_internal.devdocs.json index 56bb924457859..db65fdba1b5be 100644 --- a/api_docs/kbn_core_apps_server_internal.devdocs.json +++ b/api_docs/kbn_core_apps_server_internal.devdocs.json @@ -159,7 +159,23 @@ ], "interfaces": [], "enums": [], - "misc": [], + "misc": [ + { + "parentPluginId": "@kbn/core-apps-server-internal", + "id": "def-common.CoreAppConfigType", + "type": "Type", + "tags": [], + "label": "CoreAppConfigType", + "description": [], + "signature": [ + "{ readonly allowDynamicConfigOverrides: boolean; }" + ], + "path": "packages/core/apps/core-apps-server-internal/src/core_app_config.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], "objects": [] } } \ No newline at end of file diff --git a/api_docs/kbn_core_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index 02b9fba56b31b..14b282a273f22 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; @@ -21,10 +21,13 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 8 | 0 | 8 | 1 | +| 9 | 0 | 9 | 1 | ## Common ### Functions +### Consts, variables and types + + diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index f0146e929be94..315c7acf87a17 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index df575fdd3c903..9b9547190aa86 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 92f94a60dd217..99e3e0ed02fa2 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 86b23da543b7b..4e1e2c7844df3 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index e8bfa3479d1dd..647817f602e86 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index 51ae5d342d263..5a469f8fb6cc0 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index 2b977caea7b35..fc98432fd11db 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index ea7d5217cbabc..707a34374771c 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index f450451982d27..ebfd9644f1ffa 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index 4c0e87652249f..86c1b2a59a51c 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index 08372aed1a78e..941f7eeab85a6 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser.mdx b/api_docs/kbn_core_custom_branding_browser.mdx index 58a201eecff11..ec369f63898dd 100644 --- a/api_docs/kbn_core_custom_branding_browser.mdx +++ b/api_docs/kbn_core_custom_branding_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser title: "@kbn/core-custom-branding-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser'] --- import kbnCoreCustomBrandingBrowserObj from './kbn_core_custom_branding_browser.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_internal.mdx b/api_docs/kbn_core_custom_branding_browser_internal.mdx index fbbfd8d90c3b1..e392692c7a0ce 100644 --- a/api_docs/kbn_core_custom_branding_browser_internal.mdx +++ b/api_docs/kbn_core_custom_branding_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-internal title: "@kbn/core-custom-branding-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-internal'] --- import kbnCoreCustomBrandingBrowserInternalObj from './kbn_core_custom_branding_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_mocks.mdx b/api_docs/kbn_core_custom_branding_browser_mocks.mdx index 563028256c470..8871af521c930 100644 --- a/api_docs/kbn_core_custom_branding_browser_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-mocks title: "@kbn/core-custom-branding-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-mocks'] --- import kbnCoreCustomBrandingBrowserMocksObj from './kbn_core_custom_branding_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_common.mdx b/api_docs/kbn_core_custom_branding_common.mdx index 6f4bd9a5f598e..6b56be8c38b51 100644 --- a/api_docs/kbn_core_custom_branding_common.mdx +++ b/api_docs/kbn_core_custom_branding_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-common title: "@kbn/core-custom-branding-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-common'] --- import kbnCoreCustomBrandingCommonObj from './kbn_core_custom_branding_common.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server.mdx b/api_docs/kbn_core_custom_branding_server.mdx index c8f70dd6eaf64..e667a125a4268 100644 --- a/api_docs/kbn_core_custom_branding_server.mdx +++ b/api_docs/kbn_core_custom_branding_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server title: "@kbn/core-custom-branding-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server'] --- import kbnCoreCustomBrandingServerObj from './kbn_core_custom_branding_server.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_internal.mdx b/api_docs/kbn_core_custom_branding_server_internal.mdx index 058a0ff90fb30..9127767aba5b2 100644 --- a/api_docs/kbn_core_custom_branding_server_internal.mdx +++ b/api_docs/kbn_core_custom_branding_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-internal title: "@kbn/core-custom-branding-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-internal'] --- import kbnCoreCustomBrandingServerInternalObj from './kbn_core_custom_branding_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_mocks.mdx b/api_docs/kbn_core_custom_branding_server_mocks.mdx index 9dea0bbb5180e..8cf713c6da2f1 100644 --- a/api_docs/kbn_core_custom_branding_server_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-mocks title: "@kbn/core-custom-branding-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-mocks'] --- import kbnCoreCustomBrandingServerMocksObj from './kbn_core_custom_branding_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index 8bef97501ba14..f1ef685c162ad 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index 86c0fd24d3ef5..324b3291c428b 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index 6d63989ec8b28..bd3bb18d01281 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index c860d1ad9e33a..f72e86ef83440 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index 985ade7f9df50..4d968a10b1371 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index 7a5b1cd2bc9fe..aadb7ee12c9f5 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index 3d86a5c005edb..5891024b67a45 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index 1bcbe10f847e2..f5369679076f2 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index e6c84e04ff104..a4628267a3b23 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index 25a997e9680ed..af7d968100a5a 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index d69c2d624d041..6dffcd5bf011d 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index 6ec97363c7ac6..9f3b157a0e7ad 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index 7a35b078c5e1f..7196d5b59d0b4 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index 995420e9c5e99..dbc1eaec1fb49 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index b498071c37156..c353870e9bd1c 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index 66af48d6ebf65..1a5426df061ed 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index 5f552f25a35b9..ea6fd0531228d 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index ea588dfe4b628..bca77c3503b45 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index a737d28921aa6..376f67dce7275 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index c595b2bf4aee1..ea1da3f4749f4 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index e73c5aea252be..bf0e09898be90 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index 536185db025bf..5775b5d5f9b73 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index 039183d8996a3..9a5c8dc842acb 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index 297c976c40a97..24b454a40a72b 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index b69d9803864b9..2323310976cdf 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index 673a4b56a678d..efa9880e343cd 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index 67af69942d1d0..688cb85dc67d1 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index 82b2d27bb43f7..ed462f0b7103e 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index e9c22d6a04219..9c9a2f63eab66 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index ddb19fe2acda8..f740c651d8dcf 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index 6e50dfb1e04fc..2307f7280d904 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index b98cd8e966b79..897affe7c09b8 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index dac4ffde89734..d4fdd95134136 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index 863aff53a998f..87346e93af629 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index 259b12114cfdf..c8dcdc45a54a4 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index 0ee604bd7228d..ef1491370a696 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index 9d0dd58feda1d..f85ebe7a68529 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index 4cd12d8e150cd..018b03c72899f 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.devdocs.json b/api_docs/kbn_core_http_server.devdocs.json index c226ad218a529..646cf9fd1bb02 100644 --- a/api_docs/kbn_core_http_server.devdocs.json +++ b/api_docs/kbn_core_http_server.devdocs.json @@ -3477,11 +3477,11 @@ }, { "plugin": "actions", - "path": "x-pack/plugins/actions/server/routes/get.ts" + "path": "x-pack/plugins/actions/server/routes/connector/list_types/list_types.ts" }, { "plugin": "actions", - "path": "x-pack/plugins/actions/server/routes/connector_types.ts" + "path": "x-pack/plugins/actions/server/routes/get.ts" }, { "plugin": "actions", @@ -3623,62 +3623,6 @@ "plugin": "ruleRegistry", "path": "x-pack/plugins/rule_registry/server/routes/get_aad_fields_by_rule_type.ts" }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/find_exception_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_index/find_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/read_exception_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list/read_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/summary_exception_list_route.ts" - }, { "plugin": "savedObjectsTagging", "path": "x-pack/plugins/saved_objects_tagging/server/routes/tags/get_all_tags.ts" @@ -5027,22 +4971,6 @@ "plugin": "@kbn/core-http-router-server-mocks", "path": "packages/core/http/core-http-router-server-mocks/src/router.mock.ts" }, - { - "plugin": "actions", - "path": "x-pack/plugins/actions/server/routes/connector_types.test.ts" - }, - { - "plugin": "actions", - "path": "x-pack/plugins/actions/server/routes/connector_types.test.ts" - }, - { - "plugin": "actions", - "path": "x-pack/plugins/actions/server/routes/connector_types.test.ts" - }, - { - "plugin": "actions", - "path": "x-pack/plugins/actions/server/routes/connector_types.test.ts" - }, { "plugin": "actions", "path": "x-pack/plugins/actions/server/routes/get.test.ts" @@ -5627,6 +5555,22 @@ "plugin": "actions", "path": "x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts" }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts" + }, { "plugin": "crossClusterReplication", "path": "x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts" @@ -6469,62 +6413,6 @@ "plugin": "ruleRegistry", "path": "x-pack/plugins/rule_registry/server/routes/get_alert_summary.ts" }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/create_exception_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list/create_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/export_exception_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/import_exceptions_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list/import_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts" - }, { "plugin": "savedObjectsTagging", "path": "x-pack/plugins/saved_objects_tagging/server/routes/tags/create_tag.ts" @@ -8543,26 +8431,6 @@ "plugin": "alerting", "path": "x-pack/plugins/alerting/server/routes/update_rule.ts" }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/update_exception_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list/update_list_route.ts" - }, { "plugin": "fleet", "path": "x-pack/plugins/fleet/server/services/security/fleet_router.ts" @@ -9285,14 +9153,6 @@ "plugin": "alerting", "path": "x-pack/plugins/alerting/server/routes/bulk_disable_rules.ts" }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list/patch_list_route.ts" - }, { "plugin": "fleet", "path": "x-pack/plugins/fleet/server/services/security/fleet_router.ts" @@ -9663,30 +9523,6 @@ "plugin": "alerting", "path": "x-pack/plugins/alerting/server/routes/maintenance_window/delete_maintenance_window.ts" }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/delete_exception_list_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts" - }, - { - "plugin": "lists", - "path": "x-pack/plugins/lists/server/routes/list/delete_list_route.ts" - }, { "plugin": "savedObjectsTagging", "path": "x-pack/plugins/saved_objects_tagging/server/routes/tags/delete_tag.ts" @@ -13745,6 +13581,62 @@ "plugin": "data", "path": "src/plugins/data/server/scripts/route.ts" }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/find_exception_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_index/find_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/read_exception_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list/read_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/summary_exception_list_route.ts" + }, { "plugin": "telemetry", "path": "src/plugins/telemetry/server/routes/telemetry_config.ts" @@ -14257,10 +14149,6 @@ "plugin": "sessionView", "path": "x-pack/plugins/session_view/server/routes/get_total_io_bytes_route.ts" }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/server/routes/api/privileges.ts" - }, { "plugin": "transform", "path": "x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts" @@ -14481,6 +14369,10 @@ "deprecated": false, "trackAdoption": true, "references": [ + { + "plugin": "@kbn/core-apps-server-internal", + "path": "packages/core/apps/core-apps-server-internal/src/core_app.ts" + }, { "plugin": "dataViews", "path": "src/plugins/data_views/server/rest_api_routes/public/runtime_fields/put_runtime_field.ts" @@ -14505,6 +14397,26 @@ "plugin": "data", "path": "src/plugins/data/server/query/routes.ts" }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/update_exception_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list/update_list_route.ts" + }, { "plugin": "telemetry", "path": "src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts" @@ -14620,6 +14532,14 @@ { "plugin": "canvas", "path": "x-pack/plugins/canvas/server/routes/workpad/update.test.ts" + }, + { + "plugin": "@kbn/core-apps-server-internal", + "path": "packages/core/apps/core-apps-server-internal/src/core_app.test.ts" + }, + { + "plugin": "@kbn/core-apps-server-internal", + "path": "packages/core/apps/core-apps-server-internal/src/core_app.test.ts" } ], "returnComment": [], @@ -14773,6 +14693,62 @@ "plugin": "unifiedSearch", "path": "src/plugins/unified_search/server/autocomplete/value_suggestions_route.ts" }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/create_exception_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list/create_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/export_exception_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/import_exceptions_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list/import_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts" + }, { "plugin": "telemetry", "path": "src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts" @@ -15417,6 +15393,14 @@ "deprecated": false, "trackAdoption": true, "references": [ + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list/patch_list_route.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts" @@ -15537,6 +15521,30 @@ "plugin": "data", "path": "src/plugins/data/server/query/routes.ts" }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/delete_exception_list_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts" + }, + { + "plugin": "lists", + "path": "x-pack/plugins/lists/server/routes/list/delete_list_route.ts" + }, { "plugin": "ml", "path": "x-pack/plugins/ml/server/routes/annotations.ts" diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index 79d3d870af63e..d75b9c88f0d8c 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index f7b2e4cfa3a12..17ec295e2b28a 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index ae1e34308bd69..af7530ebe2780 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index cb8e8a6f86d1f..8c8bf1891f9f1 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 100ae9f06a288..b28afa660b41c 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index 167dbaa327158..e26626cf1aa43 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index ce0a40721a1b1..1dde8083861cc 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index 173477df8cba2..4c96d38b98734 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index f5ebcf356933e..c95da94e81dd9 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index 30bd8215d0f93..313d1a1cbc7ce 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index eaf7505c170a0..b270138d4cde2 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index 1076d85474f01..bfad413d0dbae 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index 45d7b6cd01d36..a3d274221d1f7 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index b6a4f8983cc0a..b96e16ddd9e4e 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index 52d3bc6e3141d..4ea507f43b8d8 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index 20f895dc34886..e663c9a363275 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index dcf3e6e39b6d2..c1e5800d3fc35 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 46e71a104d3de..377505265a597 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index 60de046e3cc02..08e6c40d3b799 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index b1f39308253e7..374fd7a4c3600 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index 5e8b3e5a62314..789f56c123186 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index 8a3de8307b1f5..9ce1103581bb2 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index ed3f47b751106..f65dc13cd22a7 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index b1fe1cff8e149..d9393f985758f 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 612250f053548..2a7dcbd5d1af9 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index 60b3d9c3b391b..8701f13ca30dc 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index 8e5ea4c19615e..8d37a662156d1 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index b79735594ffb8..4cc7d28317be7 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index 4ebc8d71ba5a2..48905d30f9cfe 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index 2e1cd32fcb249..afca185707beb 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index 37061c402bef7..f6a66c5963efc 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index 84ae73ec14b4b..147187be6169d 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index b59bbb842eca9..831b0fa16040c 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index 62aec73fc0457..13f3e351a4e1b 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index 2b6fa411a1b72..cee4243d41ec9 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index 35eadeade734d..d0e79df85e348 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.devdocs.json b/api_docs/kbn_core_plugins_browser_mocks.devdocs.json index 5671c40bc1f8c..38a7c61a027ac 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.devdocs.json +++ b/api_docs/kbn_core_plugins_browser_mocks.devdocs.json @@ -92,7 +92,9 @@ "label": "createPluginInitializerContext", "description": [], "signature": [ - "(config?: unknown) => ", + "(config?: unknown, { buildFlavor }?: { buildFlavor?: ", + "BuildFlavor", + " | undefined; }) => ", { "pluginId": "@kbn/core-plugins-browser", "scope": "common", @@ -120,6 +122,22 @@ "path": "packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-plugins-browser-mocks", + "id": "def-common.pluginsServiceMock.createPluginInitializerContext.$2", + "type": "Object", + "tags": [], + "label": "__1", + "description": [], + "signature": [ + "{ buildFlavor?: ", + "BuildFlavor", + " | undefined; }" + ], + "path": "packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts", + "deprecated": false, + "trackAdoption": false } ] } diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index d2c80db9d0da8..0efde46e225c8 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 6 | 0 | 6 | 0 | +| 7 | 0 | 7 | 0 | ## Common diff --git a/api_docs/kbn_core_plugins_server.devdocs.json b/api_docs/kbn_core_plugins_server.devdocs.json index 98b1596549758..1d895c5714572 100644 --- a/api_docs/kbn_core_plugins_server.devdocs.json +++ b/api_docs/kbn_core_plugins_server.devdocs.json @@ -436,6 +436,29 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "@kbn/core-plugins-server", + "id": "def-common.PluginConfigDescriptor.dynamicConfig", + "type": "Object", + "tags": [], + "label": "dynamicConfig", + "description": [ + "\nList of configuration properties that can be dynamically changed via the PUT /_settings API." + ], + "signature": [ + { + "pluginId": "@kbn/core-plugins-server", + "scope": "common", + "docId": "kibKbnCorePluginsServerPluginApi", + "section": "def-common.DynamicConfigDescriptor", + "text": "DynamicConfigDescriptor" + }, + " | undefined" + ], + "path": "packages/core/plugins/core-plugins-server/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "@kbn/core-plugins-server", "id": "def-common.PluginConfigDescriptor.schema", @@ -1050,6 +1073,31 @@ ], "enums": [], "misc": [ + { + "parentPluginId": "@kbn/core-plugins-server", + "id": "def-common.DynamicConfigDescriptor", + "type": "Type", + "tags": [], + "label": "DynamicConfigDescriptor", + "description": [ + "\nType defining the list of configuration properties that can be dynamically updated\nObject properties can either be fully exposed or narrowed down to specific keys.\n" + ], + "signature": [ + "{ [Key in keyof T]?: (T[Key] extends Maybe ? boolean : T[Key] extends Maybe ? boolean | ", + { + "pluginId": "@kbn/core-plugins-server", + "scope": "common", + "docId": "kibKbnCorePluginsServerPluginApi", + "section": "def-common.DynamicConfigDescriptor", + "text": "DynamicConfigDescriptor" + }, + " : boolean) | undefined; }" + ], + "path": "packages/core/plugins/core-plugins-server/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/core-plugins-server", "id": "def-common.ExposedToBrowserDescriptor", @@ -1057,7 +1105,7 @@ "tags": [], "label": "ExposedToBrowserDescriptor", "description": [ - "\nType defining the list of configuration properties that will be exposed on the client-side\nObject properties can either be fully exposed\n" + "\nType defining the list of configuration properties that will be exposed on the client-side\nObject properties can either be fully exposed or narrowed down to specific keys.\n" ], "signature": [ "{ [Key in keyof T]?: (T[Key] extends Maybe ? boolean : T[Key] extends Maybe ? boolean | ", diff --git a/api_docs/kbn_core_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 72fa61e3eeab9..b9b84c8887fc5 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 58 | 0 | 26 | 0 | +| 60 | 0 | 26 | 0 | ## Common diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index e97a77130be9d..7a04d07f67c12 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index b649bec5321c4..8b8849d1e1a78 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index 677715f149a8a..bde0ba3320d89 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index e8cc53127f6cc..c5f0137d455e2 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index 688f5fa567503..b09d5759718c2 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index f1331870c7f73..71ec48a5a4eb3 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index 311ff162c0d0b..985a16b0261ea 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index eb583fc7b7d80..c3e93e7242086 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.devdocs.json b/api_docs/kbn_core_saved_objects_api_server.devdocs.json index e529c9e80e178..89f29c9e91e81 100644 --- a/api_docs/kbn_core_saved_objects_api_server.devdocs.json +++ b/api_docs/kbn_core_saved_objects_api_server.devdocs.json @@ -7942,7 +7942,7 @@ "tags": [], "label": "version", "description": [ - "\nAn opaque version number which changes on each successful write operation.\nCan be used for implementing optimistic concurrency control." + "\nAn opaque version number which changes on each successful write operation.\nCan be used for implementing optimistic concurrency control.\nUnused for multi-namespace objects" ], "signature": [ "string | undefined" @@ -8028,6 +8028,22 @@ "path": "packages/core/saved-objects/core-saved-objects-api-server/src/apis/update.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-saved-objects-api-server", + "id": "def-common.SavedObjectsUpdateOptions.migrationVersionCompatibility", + "type": "CompoundType", + "tags": [], + "label": "migrationVersionCompatibility", + "description": [ + "{@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility}" + ], + "signature": [ + "\"raw\" | \"compatible\" | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-api-server/src/apis/update.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 227d0db50d4b0..5389c790c3889 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 351 | 1 | 5 | 1 | +| 352 | 1 | 5 | 1 | ## Common diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index 2e5d85dcd548c..24d6094fecad2 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index b183c5413a5a0..3348a66f9503c 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index dcd89040b2edf..00ec9085ed5b0 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index 7863ce545e938..46cca04b8c670 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index de37144affb42..15a10088397da 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index 5c1d1bd8b2fb7..f11d1ea3fd27b 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index b254e4c33dd60..b38bf406d436f 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index f085a159d2dd3..dcc9f8b8c8e94 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index 1ae599546797c..d87aa7fa00c18 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index 6e2c6c18a2244..0243585e35df1 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index b2596036f146f..089cd4f11e138 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.devdocs.json b/api_docs/kbn_core_saved_objects_server.devdocs.json index 097e6c4edbe8c..1853a66e3f1b5 100644 --- a/api_docs/kbn_core_saved_objects_server.devdocs.json +++ b/api_docs/kbn_core_saved_objects_server.devdocs.json @@ -8647,7 +8647,9 @@ "type": "Interface", "tags": [], "label": "SavedObjectsRawDocSource", - "description": [], + "description": [ + "\nSaved object document as stored in `_source` of doc in ES index\nSimilar to SavedObjectDoc and excludes `version`, includes `references`, has `attributes` in [typeMapping]\n" + ], "path": "packages/core/saved-objects/core-saved-objects-server/src/serialization.ts", "deprecated": false, "trackAdoption": false, @@ -9619,52 +9621,6 @@ "trackAdoption": false, "children": [], "returnComment": [] - }, - { - "parentPluginId": "@kbn/core-saved-objects-server", - "id": "def-common.SavedObjectsServiceSetup.getAllIndices", - "type": "Function", - "tags": [ - "deprecated" - ], - "label": "getAllIndices", - "description": [ - "\nReturns all (aliases to) kibana system indices used for saved object storage.\n" - ], - "signature": [ - "() => string[]" - ], - "path": "packages/core/saved-objects/core-saved-objects-server/src/contracts.ts", - "deprecated": true, - "trackAdoption": false, - "references": [ - { - "plugin": "@kbn/core-saved-objects-server-internal", - "path": "packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts" - }, - { - "plugin": "@kbn/core-plugins-server-internal", - "path": "packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts" - }, - { - "plugin": "@kbn/core-plugins-server-internal", - "path": "packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts" - }, - { - "plugin": "savedObjectsTagging", - "path": "x-pack/plugins/saved_objects_tagging/server/plugin.ts" - }, - { - "plugin": "@kbn/core-saved-objects-server-mocks", - "path": "packages/core/saved-objects/core-saved-objects-server-mocks/src/saved_objects_service.mock.ts" - }, - { - "plugin": "@kbn/core-saved-objects-server-mocks", - "path": "packages/core/saved-objects/core-saved-objects-server-mocks/src/saved_objects_service.mock.ts" - } - ], - "children": [], - "returnComment": [] } ], "initialIsOpen": false @@ -10225,7 +10181,7 @@ "tags": [], "label": "getAllIndices", "description": [ - "\nReturns all (aliases to) kibana system indices used for saved object storage." + "\nReturns all (aliases to) kibana system indices used for saved object storage.\n" ], "signature": [ "() => string[]" diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index 2a128edbd4123..0d93a2c9924c8 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 548 | 1 | 122 | 4 | +| 547 | 1 | 121 | 4 | ## Common diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index 961ee502dba74..038594c4e6f50 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index 1e8fde889c422..8de8c4fc312c4 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index b9694e3000853..14872988849f1 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index 6f9dfeb432340..0707f752b2a6c 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index 0b254f28a4e39..35c63747efecb 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common-internal title: "@kbn/core-status-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index 33b50a3171712..b13279c38b32f 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index d1618d4918996..5740bf4cbada0 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index 758aa4c4bc691..3126e14f0581c 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index 84c94392b87a5..d2910acd94f73 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index 8ce8eb614334c..caca10c2fd48f 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index 9b09ce92e443b..96d43fb283968 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index 7834a214aa798..52d1fb852d224 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index 55d2d369ac626..e256f3ba6d69e 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 9bbbf78c7f64f..c883b482a9c69 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index 9eea1301252db..513db41d2f316 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index 8d3a5287f8438..38cfbeb018997 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index 85f6df26b9788..cfe6994c0bbf7 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index 03185781325d5..0bacde75c8ed3 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index d9212725166a6..f1671013301da 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 140be2bd66784..3f0b161976b96 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index f77bcf74e7190..696ae1d316070 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index 83103ed73dddc..6461a930f9926 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index b1e5842cc38be..11e95f64326de 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index 68a5256f3d9ed..2fff03abb2438 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index 6ac2cfa27b5ff..3d8450614de08 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_user_settings_server.mdx b/api_docs/kbn_core_user_settings_server.mdx index bcca65011f8e8..8a4e30bc7f65e 100644 --- a/api_docs/kbn_core_user_settings_server.mdx +++ b/api_docs/kbn_core_user_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-settings-server title: "@kbn/core-user-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-settings-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-settings-server'] --- import kbnCoreUserSettingsServerObj from './kbn_core_user_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_user_settings_server_internal.mdx b/api_docs/kbn_core_user_settings_server_internal.mdx index 1a82d2fdfcd13..bbc16de105137 100644 --- a/api_docs/kbn_core_user_settings_server_internal.mdx +++ b/api_docs/kbn_core_user_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-settings-server-internal title: "@kbn/core-user-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-settings-server-internal plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-settings-server-internal'] --- import kbnCoreUserSettingsServerInternalObj from './kbn_core_user_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_user_settings_server_mocks.mdx b/api_docs/kbn_core_user_settings_server_mocks.mdx index e0bab56d5c1db..f2235fdee4dee 100644 --- a/api_docs/kbn_core_user_settings_server_mocks.mdx +++ b/api_docs/kbn_core_user_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-settings-server-mocks title: "@kbn/core-user-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-settings-server-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-settings-server-mocks'] --- import kbnCoreUserSettingsServerMocksObj from './kbn_core_user_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index efd2c9d43c394..c14fe4df54dc4 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index f030b08f89ade..cd44c94dbb706 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_cypress_config.mdx b/api_docs/kbn_cypress_config.mdx index 12a99cec6f65f..1244ed0c0da4c 100644 --- a/api_docs/kbn_cypress_config.mdx +++ b/api_docs/kbn_cypress_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cypress-config title: "@kbn/cypress-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cypress-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cypress-config'] --- import kbnCypressConfigObj from './kbn_cypress_config.devdocs.json'; diff --git a/api_docs/kbn_data_service.mdx b/api_docs/kbn_data_service.mdx index ee54819beba99..76ecfb8d270ca 100644 --- a/api_docs/kbn_data_service.mdx +++ b/api_docs/kbn_data_service.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-service title: "@kbn/data-service" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-service plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-service'] --- import kbnDataServiceObj from './kbn_data_service.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 4efc8ace6cee1..8a115eeed4cf6 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_analytics.mdx b/api_docs/kbn_deeplinks_analytics.mdx index 4f8ab53a1157d..bda8d0b6b8ab1 100644 --- a/api_docs/kbn_deeplinks_analytics.mdx +++ b/api_docs/kbn_deeplinks_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-analytics title: "@kbn/deeplinks-analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-analytics plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-analytics'] --- import kbnDeeplinksAnalyticsObj from './kbn_deeplinks_analytics.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_devtools.mdx b/api_docs/kbn_deeplinks_devtools.mdx index a1eeb9eae6a23..023e5b73ca23d 100644 --- a/api_docs/kbn_deeplinks_devtools.mdx +++ b/api_docs/kbn_deeplinks_devtools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-devtools title: "@kbn/deeplinks-devtools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-devtools plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-devtools'] --- import kbnDeeplinksDevtoolsObj from './kbn_deeplinks_devtools.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_management.mdx b/api_docs/kbn_deeplinks_management.mdx index 9518e124a176f..247745c07f508 100644 --- a/api_docs/kbn_deeplinks_management.mdx +++ b/api_docs/kbn_deeplinks_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-management title: "@kbn/deeplinks-management" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-management plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-management'] --- import kbnDeeplinksManagementObj from './kbn_deeplinks_management.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_ml.mdx b/api_docs/kbn_deeplinks_ml.mdx index b7650c5c5dd4a..bb3308665a207 100644 --- a/api_docs/kbn_deeplinks_ml.mdx +++ b/api_docs/kbn_deeplinks_ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-ml title: "@kbn/deeplinks-ml" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-ml plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-ml'] --- import kbnDeeplinksMlObj from './kbn_deeplinks_ml.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_observability.devdocs.json b/api_docs/kbn_deeplinks_observability.devdocs.json index dbcbc0da8d191..8754a1ec6e042 100644 --- a/api_docs/kbn_deeplinks_observability.devdocs.json +++ b/api_docs/kbn_deeplinks_observability.devdocs.json @@ -30,7 +30,7 @@ "label": "AppId", "description": [], "signature": [ - "\"metrics\" | \"apm\" | \"logs\" | \"observability-overview\" | \"observabilityOnboarding\"" + "\"metrics\" | \"apm\" | \"logs\" | \"observability-overview\" | \"observabilityOnboarding\" | \"observability-log-explorer\"" ], "path": "packages/deeplinks/observability/deep_links.ts", "deprecated": false, @@ -52,7 +52,7 @@ "section": "def-common.AppId", "text": "AppId" }, - " | \"discover:log-explorer\" | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:rules\" | \"observability-overview:alerts\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"observability-overview:slos\" | \"metrics:settings\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:metrics-hosts\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:services\" | \"apm:service-groups-list\" | \"apm:storage-explorer\"" + " | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:rules\" | \"observability-overview:alerts\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"observability-overview:slos\" | \"metrics:settings\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:metrics-hosts\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:services\" | \"apm:service-groups-list\" | \"apm:storage-explorer\"" ], "path": "packages/deeplinks/observability/deep_links.ts", "deprecated": false, diff --git a/api_docs/kbn_deeplinks_observability.mdx b/api_docs/kbn_deeplinks_observability.mdx index dddc0b51d2f30..315988ced942c 100644 --- a/api_docs/kbn_deeplinks_observability.mdx +++ b/api_docs/kbn_deeplinks_observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-observability title: "@kbn/deeplinks-observability" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-observability plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-observability'] --- import kbnDeeplinksObservabilityObj from './kbn_deeplinks_observability.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_search.mdx b/api_docs/kbn_deeplinks_search.mdx index eeb4a33a08856..8777eb2ca54ef 100644 --- a/api_docs/kbn_deeplinks_search.mdx +++ b/api_docs/kbn_deeplinks_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-search title: "@kbn/deeplinks-search" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-search plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-search'] --- import kbnDeeplinksSearchObj from './kbn_deeplinks_search.devdocs.json'; diff --git a/api_docs/kbn_default_nav_analytics.mdx b/api_docs/kbn_default_nav_analytics.mdx index 26dc3c42e6de3..387d6797cd5a5 100644 --- a/api_docs/kbn_default_nav_analytics.mdx +++ b/api_docs/kbn_default_nav_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-analytics title: "@kbn/default-nav-analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-analytics plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-analytics'] --- import kbnDefaultNavAnalyticsObj from './kbn_default_nav_analytics.devdocs.json'; diff --git a/api_docs/kbn_default_nav_devtools.mdx b/api_docs/kbn_default_nav_devtools.mdx index 2944b490bc4b8..efaffaf33ca7c 100644 --- a/api_docs/kbn_default_nav_devtools.mdx +++ b/api_docs/kbn_default_nav_devtools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-devtools title: "@kbn/default-nav-devtools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-devtools plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-devtools'] --- import kbnDefaultNavDevtoolsObj from './kbn_default_nav_devtools.devdocs.json'; diff --git a/api_docs/kbn_default_nav_management.mdx b/api_docs/kbn_default_nav_management.mdx index bfa83cfa31a60..ef7beaef0f6a9 100644 --- a/api_docs/kbn_default_nav_management.mdx +++ b/api_docs/kbn_default_nav_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-management title: "@kbn/default-nav-management" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-management plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-management'] --- import kbnDefaultNavManagementObj from './kbn_default_nav_management.devdocs.json'; diff --git a/api_docs/kbn_default_nav_ml.mdx b/api_docs/kbn_default_nav_ml.mdx index b2cf182a214a2..763e602b18868 100644 --- a/api_docs/kbn_default_nav_ml.mdx +++ b/api_docs/kbn_default_nav_ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-ml title: "@kbn/default-nav-ml" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-ml plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-ml'] --- import kbnDefaultNavMlObj from './kbn_default_nav_ml.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index 959b1748c1f23..918307a7aea1f 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index 8b3c4efad36cd..8889c66a849e1 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 073bbfa04161e..787be9b13f6ff 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.devdocs.json b/api_docs/kbn_dev_utils.devdocs.json index 2d366c4839e72..6fa61e7f7047f 100644 --- a/api_docs/kbn_dev_utils.devdocs.json +++ b/api_docs/kbn_dev_utils.devdocs.json @@ -471,6 +471,34 @@ "initialIsOpen": false } ], - "objects": [] + "objects": [ + { + "parentPluginId": "@kbn/dev-utils", + "id": "def-common.kibanaDevServiceAccount", + "type": "Object", + "tags": [], + "label": "kibanaDevServiceAccount", + "description": [ + "\n`kibana-dev` service account token for connecting to ESS\nSee packages/kbn-es/src/ess_resources/README.md" + ], + "path": "packages/kbn-dev-utils/src/dev_service_account.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/dev-utils", + "id": "def-common.kibanaDevServiceAccount.token", + "type": "string", + "tags": [], + "label": "token", + "description": [], + "path": "packages/kbn-dev-utils/src/dev_service_account.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ] } } \ No newline at end of file diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 522d693aee8f3..d0134b6dd43c5 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; @@ -21,10 +21,13 @@ Contact [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kiban | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 27 | 2 | 24 | 0 | +| 29 | 2 | 25 | 0 | ## Common +### Objects + + ### Functions diff --git a/api_docs/kbn_discover_utils.devdocs.json b/api_docs/kbn_discover_utils.devdocs.json index 6ae82d7acfccd..74196fab61cdd 100644 --- a/api_docs/kbn_discover_utils.devdocs.json +++ b/api_docs/kbn_discover_utils.devdocs.json @@ -587,6 +587,41 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/discover-utils", + "id": "def-common.getFieldTypeName", + "type": "Function", + "tags": [], + "label": "getFieldTypeName", + "description": [ + "\nReturns a user-friendly name of a field type" + ], + "signature": [ + "(type: string | undefined) => string" + ], + "path": "packages/kbn-discover-utils/src/utils/get_field_type_name.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/discover-utils", + "id": "def-common.getFieldTypeName.$1", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-discover-utils/src/utils/get_field_type_name.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/discover-utils", "id": "def-common.getIgnoredReason", @@ -744,6 +779,39 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/discover-utils", + "id": "def-common.isKnownFieldType", + "type": "Function", + "tags": [], + "label": "isKnownFieldType", + "description": [], + "signature": [ + "(type?: string | undefined) => type is string" + ], + "path": "packages/kbn-discover-utils/src/utils/field_types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/discover-utils", + "id": "def-common.isKnownFieldType.$1", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-discover-utils/src/utils/field_types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/discover-utils", "id": "def-common.isNestedFieldParent", @@ -875,6 +943,20 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/discover-utils", + "id": "def-common.KNOWN_FIELD_TYPES", + "type": "Enum", + "tags": [], + "label": "KNOWN_FIELD_TYPES", + "description": [ + "\nField types for which name and description are defined" + ], + "path": "packages/kbn-discover-utils/src/utils/field_types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false } ], "misc": [ @@ -970,13 +1052,13 @@ }, { "parentPluginId": "@kbn/discover-utils", - "id": "def-common.ENABLE_SQL", + "id": "def-common.ENABLE_ESQL", "type": "string", "tags": [], - "label": "ENABLE_SQL", + "label": "ENABLE_ESQL", "description": [], "signature": [ - "\"discover:enableSql\"" + "\"discover:enableESQL\"" ], "path": "packages/kbn-discover-utils/src/constants.ts", "deprecated": false, @@ -1013,6 +1095,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/discover-utils", + "id": "def-common.KNOWN_FIELD_TYPE_LIST", + "type": "Array", + "tags": [], + "label": "KNOWN_FIELD_TYPE_LIST", + "description": [], + "signature": [ + "string[]" + ], + "path": "packages/kbn-discover-utils/src/utils/field_types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/discover-utils", "id": "def-common.MAX_DOC_FIELDS_DISPLAYED", diff --git a/api_docs/kbn_discover_utils.mdx b/api_docs/kbn_discover_utils.mdx index e273816a2cf47..1ee3655894d70 100644 --- a/api_docs/kbn_discover_utils.mdx +++ b/api_docs/kbn_discover_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-discover-utils title: "@kbn/discover-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/discover-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/discover-utils'] --- import kbnDiscoverUtilsObj from './kbn_discover_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 59 | 0 | 34 | 3 | +| 65 | 0 | 38 | 3 | ## Common diff --git a/api_docs/kbn_doc_links.devdocs.json b/api_docs/kbn_doc_links.devdocs.json index 5dd70d02389dd..724a581015e7e 100644 --- a/api_docs/kbn_doc_links.devdocs.json +++ b/api_docs/kbn_doc_links.devdocs.json @@ -644,7 +644,7 @@ "label": "apis", "description": [], "signature": [ - "{ readonly bulkIndexAlias: string; readonly byteSizeUnits: string; readonly createAutoFollowPattern: string; readonly createFollower: string; readonly createIndex: string; readonly createSnapshotLifecyclePolicy: string; readonly createRoleMapping: string; readonly createRoleMappingTemplates: string; readonly createRollupJobsRequest: string; readonly createApiKey: string; readonly createPipeline: string; readonly createTransformRequest: string; readonly cronExpressions: string; readonly executeWatchActionModes: string; readonly indexExists: string; readonly multiSearch: string; readonly openIndex: string; readonly putComponentTemplate: string; readonly painlessExecute: string; readonly painlessExecuteAPIContexts: string; readonly putComponentTemplateMetadata: string; readonly putSnapshotLifecyclePolicy: string; readonly putIndexTemplateV1: string; readonly putWatch: string; readonly restApis: string; readonly searchPreference: string; readonly securityApis: string; readonly simulatePipeline: string; readonly timeUnits: string; readonly unfreezeIndex: string; readonly updateTransform: string; }" + "{ readonly bulkIndexAlias: string; readonly indexStats: string; readonly byteSizeUnits: string; readonly createAutoFollowPattern: string; readonly createFollower: string; readonly createIndex: string; readonly createSnapshotLifecyclePolicy: string; readonly createRoleMapping: string; readonly createRoleMappingTemplates: string; readonly createRollupJobsRequest: string; readonly createApiKey: string; readonly createPipeline: string; readonly createTransformRequest: string; readonly cronExpressions: string; readonly executeWatchActionModes: string; readonly indexExists: string; readonly multiSearch: string; readonly openIndex: string; readonly putComponentTemplate: string; readonly painlessExecute: string; readonly painlessExecuteAPIContexts: string; readonly putComponentTemplateMetadata: string; readonly putSnapshotLifecyclePolicy: string; readonly putIndexTemplateV1: string; readonly putWatch: string; readonly restApis: string; readonly searchPreference: string; readonly securityApis: string; readonly simulatePipeline: string; readonly timeUnits: string; readonly unfreezeIndex: string; readonly updateTransform: string; }" ], "path": "packages/kbn-doc-links/src/types.ts", "deprecated": false, diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index c1eddc858e8b6..32a41d70a9325 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index 3f6406ba9e2ae..a276ef5f584e8 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_dom_drag_drop.mdx b/api_docs/kbn_dom_drag_drop.mdx index 6fdf4e030dd1b..bd3a9ef6ceb82 100644 --- a/api_docs/kbn_dom_drag_drop.mdx +++ b/api_docs/kbn_dom_drag_drop.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dom-drag-drop title: "@kbn/dom-drag-drop" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dom-drag-drop plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dom-drag-drop'] --- import kbnDomDragDropObj from './kbn_dom_drag_drop.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index 3c376439a4477..ae8ce838528bd 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_ecs.mdx b/api_docs/kbn_ecs.mdx index 09a939e46aa45..31679eb5a4ee6 100644 --- a/api_docs/kbn_ecs.mdx +++ b/api_docs/kbn_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs title: "@kbn/ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs'] --- import kbnEcsObj from './kbn_ecs.devdocs.json'; diff --git a/api_docs/kbn_ecs_data_quality_dashboard.mdx b/api_docs/kbn_ecs_data_quality_dashboard.mdx index 9cc7dfefd9678..6af12ec235d93 100644 --- a/api_docs/kbn_ecs_data_quality_dashboard.mdx +++ b/api_docs/kbn_ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs-data-quality-dashboard title: "@kbn/ecs-data-quality-dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs-data-quality-dashboard plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs-data-quality-dashboard'] --- import kbnEcsDataQualityDashboardObj from './kbn_ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/kbn_elastic_assistant.mdx b/api_docs/kbn_elastic_assistant.mdx index 700fc56c4a12e..057d553c9c272 100644 --- a/api_docs/kbn_elastic_assistant.mdx +++ b/api_docs/kbn_elastic_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-elastic-assistant title: "@kbn/elastic-assistant" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/elastic-assistant plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/elastic-assistant'] --- import kbnElasticAssistantObj from './kbn_elastic_assistant.devdocs.json'; diff --git a/api_docs/kbn_es.devdocs.json b/api_docs/kbn_es.devdocs.json index 7ae7cfcd31d0e..b7cfd2bbebb2e 100644 --- a/api_docs/kbn_es.devdocs.json +++ b/api_docs/kbn_es.devdocs.json @@ -19,6 +19,41 @@ "common": { "classes": [], "functions": [ + { + "parentPluginId": "@kbn/es", + "id": "def-common.getDockerFileMountPath", + "type": "Function", + "tags": [], + "label": "getDockerFileMountPath", + "description": [ + "\nRemoves REPO_ROOT from hostPath. Keep the rest to avoid filename collisions.\nReturns the path where a file will be mounted inside the ES or ESS container.\n/root/kibana/package/foo/bar.json => /usr/share/elasticsearch/files/package/foo/bar.json" + ], + "signature": [ + "(hostPath: string) => string" + ], + "path": "packages/kbn-es/src/utils/docker.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/es", + "id": "def-common.getDockerFileMountPath.$1", + "type": "string", + "tags": [], + "label": "hostPath", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-es/src/utils/docker.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es", "id": "def-common.run", @@ -56,6 +91,36 @@ "interfaces": [], "enums": [], "misc": [ + { + "parentPluginId": "@kbn/es", + "id": "def-common.ELASTIC_SERVERLESS_SUPERUSER", + "type": "string", + "tags": [], + "label": "ELASTIC_SERVERLESS_SUPERUSER", + "description": [], + "signature": [ + "\"elastic_serverless\"" + ], + "path": "packages/kbn-es/src/utils/ess_file_realm.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/es", + "id": "def-common.ELASTIC_SERVERLESS_SUPERUSER_PASSWORD", + "type": "string", + "tags": [], + "label": "ELASTIC_SERVERLESS_SUPERUSER_PASSWORD", + "description": [], + "signature": [ + "\"changeme\"" + ], + "path": "packages/kbn-es/src/utils/ess_file_realm.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es", "id": "def-common.SYSTEM_INDICES_SUPERUSER", diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index 30955056b2f47..2626b9b5e3793 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kiban | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 4 | 0 | 4 | 0 | +| 8 | 0 | 7 | 0 | ## Common diff --git a/api_docs/kbn_es_archiver.devdocs.json b/api_docs/kbn_es_archiver.devdocs.json index 65257a15f067a..61d819e2b15f9 100644 --- a/api_docs/kbn_es_archiver.devdocs.json +++ b/api_docs/kbn_es_archiver.devdocs.json @@ -439,7 +439,7 @@ "tags": [], "label": "emptyKibanaIndex", "description": [ - "\nDelete any Kibana indices, and initialize the Kibana index as Kibana would do\non startup." + "\nCleanup saved object indices, preserving the space:default saved object." ], "signature": [ "() => Promise never" ], - "path": "packages/kbn-es-query/src/es_query/es_query_sql.ts", + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -2592,7 +2592,7 @@ "text": "AggregateQuery" } ], - "path": "packages/kbn-es-query/src/es_query/es_query_sql.ts", + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -2683,6 +2683,39 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.getIndexPatternFromESQLQuery", + "type": "Function", + "tags": [], + "label": "getIndexPatternFromESQLQuery", + "description": [], + "signature": [ + "(esql: string | undefined) => string" + ], + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.getIndexPatternFromESQLQuery.$1", + "type": "string", + "tags": [], + "label": "esql", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es-query", "id": "def-common.getIndexPatternFromSQLQuery", @@ -2693,7 +2726,7 @@ "signature": [ "(sqlQuery: string | undefined) => string" ], - "path": "packages/kbn-es-query/src/es_query/es_query_sql.ts", + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -2707,7 +2740,7 @@ "signature": [ "string | undefined" ], - "path": "packages/kbn-es-query/src/es_query/es_query_sql.ts", + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", "deprecated": false, "trackAdoption": false, "isRequired": false @@ -2716,6 +2749,39 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.getLanguageDisplayName", + "type": "Function", + "tags": [], + "label": "getLanguageDisplayName", + "description": [], + "signature": [ + "(language: string) => string" + ], + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.getLanguageDisplayName.$1", + "type": "string", + "tags": [], + "label": "language", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es-query", "id": "def-common.isCombinedFilter", @@ -3152,7 +3218,7 @@ }, " | { [key: string]: any; }) => boolean" ], - "path": "packages/kbn-es-query/src/es_query/es_query_sql.ts", + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -3181,7 +3247,7 @@ }, " | { [key: string]: any; }" ], - "path": "packages/kbn-es-query/src/es_query/es_query_sql.ts", + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -3216,7 +3282,7 @@ }, " | undefined) => boolean" ], - "path": "packages/kbn-es-query/src/es_query/es_query_sql.ts", + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -3245,7 +3311,7 @@ }, " | undefined" ], - "path": "packages/kbn-es-query/src/es_query/es_query_sql.ts", + "path": "packages/kbn-es-query/src/es_query/es_aggregate_query.ts", "deprecated": false, "trackAdoption": false, "isRequired": false diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 9eeb9eb239d6f..34abd1a51f0fd 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 255 | 1 | 195 | 15 | +| 259 | 1 | 199 | 15 | ## Common diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index c02c0e1b6eb54..ca7517756e605 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index 6632351eb3699..7899f9650cd30 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_event_annotation_common.mdx b/api_docs/kbn_event_annotation_common.mdx index 8b6adb20854b8..fdd6e0c7f3fc3 100644 --- a/api_docs/kbn_event_annotation_common.mdx +++ b/api_docs/kbn_event_annotation_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-event-annotation-common title: "@kbn/event-annotation-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/event-annotation-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/event-annotation-common'] --- import kbnEventAnnotationCommonObj from './kbn_event_annotation_common.devdocs.json'; diff --git a/api_docs/kbn_event_annotation_components.mdx b/api_docs/kbn_event_annotation_components.mdx index acf47db88e44a..fc83a934bf7e1 100644 --- a/api_docs/kbn_event_annotation_components.mdx +++ b/api_docs/kbn_event_annotation_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-event-annotation-components title: "@kbn/event-annotation-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/event-annotation-components plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/event-annotation-components'] --- import kbnEventAnnotationComponentsObj from './kbn_event_annotation_components.devdocs.json'; diff --git a/api_docs/kbn_expandable_flyout.mdx b/api_docs/kbn_expandable_flyout.mdx index f6d5e017e5ece..0788687a910fe 100644 --- a/api_docs/kbn_expandable_flyout.mdx +++ b/api_docs/kbn_expandable_flyout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-expandable-flyout title: "@kbn/expandable-flyout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/expandable-flyout plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/expandable-flyout'] --- import kbnExpandableFlyoutObj from './kbn_expandable_flyout.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index f3c5be9d72749..8af6fb11bde12 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 9b304e48ad970..c99be713b8d90 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 6366a800e19ce..b911c6e1e1952 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index d9df69c0e3591..b5bda95c806cb 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_generate_console_definitions.mdx b/api_docs/kbn_generate_console_definitions.mdx index 4a9a08964a21b..e3d7321f12ea1 100644 --- a/api_docs/kbn_generate_console_definitions.mdx +++ b/api_docs/kbn_generate_console_definitions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-console-definitions title: "@kbn/generate-console-definitions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-console-definitions plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-console-definitions'] --- import kbnGenerateConsoleDefinitionsObj from './kbn_generate_console_definitions.devdocs.json'; diff --git a/api_docs/kbn_generate_csv.mdx b/api_docs/kbn_generate_csv.mdx index edb0ef008da43..65b0ac1dd0ecd 100644 --- a/api_docs/kbn_generate_csv.mdx +++ b/api_docs/kbn_generate_csv.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv title: "@kbn/generate-csv" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv'] --- import kbnGenerateCsvObj from './kbn_generate_csv.devdocs.json'; diff --git a/api_docs/kbn_generate_csv_types.mdx b/api_docs/kbn_generate_csv_types.mdx index 1477071967337..a68e429f6013e 100644 --- a/api_docs/kbn_generate_csv_types.mdx +++ b/api_docs/kbn_generate_csv_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv-types title: "@kbn/generate-csv-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv-types'] --- import kbnGenerateCsvTypesObj from './kbn_generate_csv_types.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index 8ac44ad4ac395..df6dcbf68a99c 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index b5c9b0a4c7905..7fe9a26ba9dd7 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index 8c5ee973427fa..f64d1c6af5206 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index 2d5301b748a32..8676c7107d4dd 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index 0d2035b575cf9..a0f2a90b37a14 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index aa44a7339f2e1..8024bad81452e 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 37af28638ba5f..79f69a5cc71a0 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index 1c4e9645f6cb8..cc4337a22a137 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index d7d4ec86c975e..81ae937e1aabc 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_infra_forge.mdx b/api_docs/kbn_infra_forge.mdx index 141e3bd5f5893..903ff74457bd2 100644 --- a/api_docs/kbn_infra_forge.mdx +++ b/api_docs/kbn_infra_forge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-infra-forge title: "@kbn/infra-forge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/infra-forge plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/infra-forge'] --- import kbnInfraForgeObj from './kbn_infra_forge.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index fc2149869e4ad..2d1f2dd1b4f3e 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.devdocs.json b/api_docs/kbn_io_ts_utils.devdocs.json index 06b7c2f9c5137..76c85df71d2fb 100644 --- a/api_docs/kbn_io_ts_utils.devdocs.json +++ b/api_docs/kbn_io_ts_utils.devdocs.json @@ -228,14 +228,14 @@ " | ", "ParseableType", ") => ", + "StringType", + " | ", "Type", " | ", - "StringType", + "NumberType", " | ", "BooleanType", " | ", - "NumberType", - " | ", "RecordC", "<", "Mixed", diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 2ef5fad37ec2f..d3f8fb3e152da 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 17eb9593f193a..3b5fe7bcd0bb7 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index 298d1c91f18ca..eea14a63ba6ba 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_json_ast.mdx b/api_docs/kbn_json_ast.mdx index f3f26735d6d43..5c11668ffa71e 100644 --- a/api_docs/kbn_json_ast.mdx +++ b/api_docs/kbn_json_ast.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-json-ast title: "@kbn/json-ast" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/json-ast plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/json-ast'] --- import kbnJsonAstObj from './kbn_json_ast.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 50dc441905e96..f4b3cbee7bc65 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_language_documentation_popover.mdx b/api_docs/kbn_language_documentation_popover.mdx index de335a8f1d70d..277d89eeb2666 100644 --- a/api_docs/kbn_language_documentation_popover.mdx +++ b/api_docs/kbn_language_documentation_popover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation-popover title: "@kbn/language-documentation-popover" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation-popover plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_lens_embeddable_utils.mdx b/api_docs/kbn_lens_embeddable_utils.mdx index c3d3f1dfab9f6..3121954ab3512 100644 --- a/api_docs/kbn_lens_embeddable_utils.mdx +++ b/api_docs/kbn_lens_embeddable_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-lens-embeddable-utils title: "@kbn/lens-embeddable-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/lens-embeddable-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/lens-embeddable-utils'] --- import kbnLensEmbeddableUtilsObj from './kbn_lens_embeddable_utils.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index e10f9f15513ad..6d1e806fffcf2 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index 9659851be9414..ffb235143b227 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index 18ed8a93add58..6dac7d9151945 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_management_cards_navigation.mdx b/api_docs/kbn_management_cards_navigation.mdx index d4c7f326d8abd..c13b36d6a01b5 100644 --- a/api_docs/kbn_management_cards_navigation.mdx +++ b/api_docs/kbn_management_cards_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-cards-navigation title: "@kbn/management-cards-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-cards-navigation plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-cards-navigation'] --- import kbnManagementCardsNavigationObj from './kbn_management_cards_navigation.devdocs.json'; diff --git a/api_docs/kbn_management_settings_section_registry.mdx b/api_docs/kbn_management_settings_section_registry.mdx index 072feb8e3d7fc..0643d300555bb 100644 --- a/api_docs/kbn_management_settings_section_registry.mdx +++ b/api_docs/kbn_management_settings_section_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-section-registry title: "@kbn/management-settings-section-registry" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-section-registry plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-section-registry'] --- import kbnManagementSettingsSectionRegistryObj from './kbn_management_settings_section_registry.devdocs.json'; diff --git a/api_docs/kbn_management_storybook_config.mdx b/api_docs/kbn_management_storybook_config.mdx index df5b2b135f70f..8bcf6ef9da913 100644 --- a/api_docs/kbn_management_storybook_config.mdx +++ b/api_docs/kbn_management_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-storybook-config title: "@kbn/management-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-storybook-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-storybook-config'] --- import kbnManagementStorybookConfigObj from './kbn_management_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.devdocs.json b/api_docs/kbn_mapbox_gl.devdocs.json index 5c5fb82b56b43..a719d22f94a26 100644 --- a/api_docs/kbn_mapbox_gl.devdocs.json +++ b/api_docs/kbn_mapbox_gl.devdocs.json @@ -9443,7 +9443,7 @@ "label": "MapEvent", "description": [], "signature": [ - "\"error\" | \"remove\" | \"data\" | \"render\" | \"rotate\" | \"resize\" | \"zoom\" | \"move\" | \"idle\" | \"mousedown\" | \"mouseup\" | \"mouseover\" | \"mousemove\" | \"click\" | \"dblclick\" | \"mouseenter\" | \"mouseleave\" | \"mouseout\" | \"contextmenu\" | \"wheel\" | \"touchstart\" | \"touchend\" | \"touchmove\" | \"touchcancel\" | \"movestart\" | \"moveend\" | \"dragstart\" | \"drag\" | \"dragend\" | \"zoomstart\" | \"zoomend\" | \"rotatestart\" | \"rotateend\" | \"pitchstart\" | \"pitch\" | \"pitchend\" | \"boxzoomstart\" | \"boxzoomend\" | \"boxzoomcancel\" | \"webglcontextlost\" | \"webglcontextrestored\" | \"load\" | \"styledata\" | \"sourcedata\" | \"dataloading\" | \"styledataloading\" | \"sourcedataloading\" | \"styleimagemissing\" | \"style.load\" | \"terrain\" | \"dataabort\" | \"sourcedataabort\"" + "\"error\" | \"remove\" | \"data\" | \"rotate\" | \"render\" | \"resize\" | \"zoom\" | \"move\" | \"idle\" | \"mousedown\" | \"mouseup\" | \"mouseover\" | \"mousemove\" | \"click\" | \"dblclick\" | \"mouseenter\" | \"mouseleave\" | \"mouseout\" | \"contextmenu\" | \"wheel\" | \"touchstart\" | \"touchend\" | \"touchmove\" | \"touchcancel\" | \"movestart\" | \"moveend\" | \"dragstart\" | \"drag\" | \"dragend\" | \"zoomstart\" | \"zoomend\" | \"rotatestart\" | \"rotateend\" | \"pitchstart\" | \"pitch\" | \"pitchend\" | \"boxzoomstart\" | \"boxzoomend\" | \"boxzoomcancel\" | \"webglcontextlost\" | \"webglcontextrestored\" | \"load\" | \"styledata\" | \"sourcedata\" | \"dataloading\" | \"styledataloading\" | \"sourcedataloading\" | \"styleimagemissing\" | \"style.load\" | \"terrain\" | \"dataabort\" | \"sourcedataabort\"" ], "path": "node_modules/maplibre-gl/dist/maplibre-gl.d.ts", "deprecated": false, diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index c27e4ee9909f3..268c13ee40c21 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_maps_vector_tile_utils.mdx b/api_docs/kbn_maps_vector_tile_utils.mdx index 752deb601de6b..cda3b46c2c802 100644 --- a/api_docs/kbn_maps_vector_tile_utils.mdx +++ b/api_docs/kbn_maps_vector_tile_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-maps-vector-tile-utils title: "@kbn/maps-vector-tile-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/maps-vector-tile-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/maps-vector-tile-utils'] --- import kbnMapsVectorTileUtilsObj from './kbn_maps_vector_tile_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index c72e0421d1744..d4c39692829a9 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_anomaly_utils.mdx b/api_docs/kbn_ml_anomaly_utils.mdx index 0310d80f4f4ee..edb82b7d856a9 100644 --- a/api_docs/kbn_ml_anomaly_utils.mdx +++ b/api_docs/kbn_ml_anomaly_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-anomaly-utils title: "@kbn/ml-anomaly-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-anomaly-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-anomaly-utils'] --- import kbnMlAnomalyUtilsObj from './kbn_ml_anomaly_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_category_validator.mdx b/api_docs/kbn_ml_category_validator.mdx index 12cea1e4fc52d..9ee3a48b1ed90 100644 --- a/api_docs/kbn_ml_category_validator.mdx +++ b/api_docs/kbn_ml_category_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-category-validator title: "@kbn/ml-category-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-category-validator plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-category-validator'] --- import kbnMlCategoryValidatorObj from './kbn_ml_category_validator.devdocs.json'; diff --git a/api_docs/kbn_ml_data_frame_analytics_utils.mdx b/api_docs/kbn_ml_data_frame_analytics_utils.mdx index e08dcf67b5b77..d4fcdeafed66b 100644 --- a/api_docs/kbn_ml_data_frame_analytics_utils.mdx +++ b/api_docs/kbn_ml_data_frame_analytics_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-data-frame-analytics-utils title: "@kbn/ml-data-frame-analytics-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-data-frame-analytics-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-data-frame-analytics-utils'] --- import kbnMlDataFrameAnalyticsUtilsObj from './kbn_ml_data_frame_analytics_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_data_grid.mdx b/api_docs/kbn_ml_data_grid.mdx index 7e7eb6c824698..8e0369f66c66b 100644 --- a/api_docs/kbn_ml_data_grid.mdx +++ b/api_docs/kbn_ml_data_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-data-grid title: "@kbn/ml-data-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-data-grid plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-data-grid'] --- import kbnMlDataGridObj from './kbn_ml_data_grid.devdocs.json'; diff --git a/api_docs/kbn_ml_date_picker.mdx b/api_docs/kbn_ml_date_picker.mdx index 22be4828a09b4..0a44b471adea8 100644 --- a/api_docs/kbn_ml_date_picker.mdx +++ b/api_docs/kbn_ml_date_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-picker title: "@kbn/ml-date-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-picker plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-picker'] --- import kbnMlDatePickerObj from './kbn_ml_date_picker.devdocs.json'; diff --git a/api_docs/kbn_ml_date_utils.mdx b/api_docs/kbn_ml_date_utils.mdx index 2cddfbacbe995..1a8bff8e80c51 100644 --- a/api_docs/kbn_ml_date_utils.mdx +++ b/api_docs/kbn_ml_date_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-utils title: "@kbn/ml-date-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-utils'] --- import kbnMlDateUtilsObj from './kbn_ml_date_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_error_utils.mdx b/api_docs/kbn_ml_error_utils.mdx index 115d7f3f5aebb..f376e5de94cbd 100644 --- a/api_docs/kbn_ml_error_utils.mdx +++ b/api_docs/kbn_ml_error_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-error-utils title: "@kbn/ml-error-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-error-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-error-utils'] --- import kbnMlErrorUtilsObj from './kbn_ml_error_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_in_memory_table.mdx b/api_docs/kbn_ml_in_memory_table.mdx index bce3c119c7300..5a614d4f7e906 100644 --- a/api_docs/kbn_ml_in_memory_table.mdx +++ b/api_docs/kbn_ml_in_memory_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-in-memory-table title: "@kbn/ml-in-memory-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-in-memory-table plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-in-memory-table'] --- import kbnMlInMemoryTableObj from './kbn_ml_in_memory_table.devdocs.json'; diff --git a/api_docs/kbn_ml_is_defined.mdx b/api_docs/kbn_ml_is_defined.mdx index 10e9a37cf2a1d..cc1cd6b7c29c3 100644 --- a/api_docs/kbn_ml_is_defined.mdx +++ b/api_docs/kbn_ml_is_defined.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-defined title: "@kbn/ml-is-defined" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-defined plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-defined'] --- import kbnMlIsDefinedObj from './kbn_ml_is_defined.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index c84cd32d693e5..88af0e128fe6d 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_kibana_theme.mdx b/api_docs/kbn_ml_kibana_theme.mdx index b6632ced1864f..5004023259bfa 100644 --- a/api_docs/kbn_ml_kibana_theme.mdx +++ b/api_docs/kbn_ml_kibana_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-kibana-theme title: "@kbn/ml-kibana-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-kibana-theme plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-kibana-theme'] --- import kbnMlKibanaThemeObj from './kbn_ml_kibana_theme.devdocs.json'; diff --git a/api_docs/kbn_ml_local_storage.mdx b/api_docs/kbn_ml_local_storage.mdx index d2aa2d2e11369..8ab31c0810681 100644 --- a/api_docs/kbn_ml_local_storage.mdx +++ b/api_docs/kbn_ml_local_storage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-local-storage title: "@kbn/ml-local-storage" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-local-storage plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-local-storage'] --- import kbnMlLocalStorageObj from './kbn_ml_local_storage.devdocs.json'; diff --git a/api_docs/kbn_ml_nested_property.mdx b/api_docs/kbn_ml_nested_property.mdx index 865cb28ba68d7..de7246615f050 100644 --- a/api_docs/kbn_ml_nested_property.mdx +++ b/api_docs/kbn_ml_nested_property.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-nested-property title: "@kbn/ml-nested-property" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-nested-property plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-nested-property'] --- import kbnMlNestedPropertyObj from './kbn_ml_nested_property.devdocs.json'; diff --git a/api_docs/kbn_ml_number_utils.mdx b/api_docs/kbn_ml_number_utils.mdx index 0c7c2d69773df..300345dc19e9f 100644 --- a/api_docs/kbn_ml_number_utils.mdx +++ b/api_docs/kbn_ml_number_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-number-utils title: "@kbn/ml-number-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-number-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-number-utils'] --- import kbnMlNumberUtilsObj from './kbn_ml_number_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_query_utils.mdx b/api_docs/kbn_ml_query_utils.mdx index c8b5a1f564921..65e39c56c54cf 100644 --- a/api_docs/kbn_ml_query_utils.mdx +++ b/api_docs/kbn_ml_query_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-query-utils title: "@kbn/ml-query-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-query-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-query-utils'] --- import kbnMlQueryUtilsObj from './kbn_ml_query_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_random_sampler_utils.mdx b/api_docs/kbn_ml_random_sampler_utils.mdx index d88fa51624c10..5989125209f3c 100644 --- a/api_docs/kbn_ml_random_sampler_utils.mdx +++ b/api_docs/kbn_ml_random_sampler_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-random-sampler-utils title: "@kbn/ml-random-sampler-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-random-sampler-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-random-sampler-utils'] --- import kbnMlRandomSamplerUtilsObj from './kbn_ml_random_sampler_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_route_utils.mdx b/api_docs/kbn_ml_route_utils.mdx index 334b5d977b445..8c2fe6d0f842b 100644 --- a/api_docs/kbn_ml_route_utils.mdx +++ b/api_docs/kbn_ml_route_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-route-utils title: "@kbn/ml-route-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-route-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-route-utils'] --- import kbnMlRouteUtilsObj from './kbn_ml_route_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_runtime_field_utils.mdx b/api_docs/kbn_ml_runtime_field_utils.mdx index 56bb547e1b02d..b87966a18bcc8 100644 --- a/api_docs/kbn_ml_runtime_field_utils.mdx +++ b/api_docs/kbn_ml_runtime_field_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-runtime-field-utils title: "@kbn/ml-runtime-field-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-runtime-field-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-runtime-field-utils'] --- import kbnMlRuntimeFieldUtilsObj from './kbn_ml_runtime_field_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index 7f7db30ddc077..54cb3c3ed4458 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_ml_trained_models_utils.mdx b/api_docs/kbn_ml_trained_models_utils.mdx index fa07e095326b3..032e218c58780 100644 --- a/api_docs/kbn_ml_trained_models_utils.mdx +++ b/api_docs/kbn_ml_trained_models_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-trained-models-utils title: "@kbn/ml-trained-models-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-trained-models-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-trained-models-utils'] --- import kbnMlTrainedModelsUtilsObj from './kbn_ml_trained_models_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_url_state.mdx b/api_docs/kbn_ml_url_state.mdx index a829a05d9e99f..7aed7a647acd3 100644 --- a/api_docs/kbn_ml_url_state.mdx +++ b/api_docs/kbn_ml_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-url-state title: "@kbn/ml-url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-url-state plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-url-state'] --- import kbnMlUrlStateObj from './kbn_ml_url_state.devdocs.json'; diff --git a/api_docs/kbn_monaco.devdocs.json b/api_docs/kbn_monaco.devdocs.json index 364fa6046fc99..a02324fc33509 100644 --- a/api_docs/kbn_monaco.devdocs.json +++ b/api_docs/kbn_monaco.devdocs.json @@ -373,7 +373,7 @@ "label": "getSourceIdentifiers", "description": [], "signature": [ - "CallbackFn | undefined" + "CallbackFn | undefined" ], "path": "packages/kbn-monaco/src/esql/lib/autocomplete/types.ts", "deprecated": false, @@ -387,7 +387,49 @@ "label": "getFieldsIdentifiers", "description": [], "signature": [ - "CallbackFn | undefined" + "CallbackFn | undefined" + ], + "path": "packages/kbn-monaco/src/esql/lib/autocomplete/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.ESQLCustomAutocompleteCallbacks.getPoliciesIdentifiers", + "type": "Function", + "tags": [], + "label": "getPoliciesIdentifiers", + "description": [], + "signature": [ + "CallbackFn<{ name: string; indices: string[]; }> | undefined" + ], + "path": "packages/kbn-monaco/src/esql/lib/autocomplete/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.ESQLCustomAutocompleteCallbacks.getPolicyFieldsIdentifiers", + "type": "Function", + "tags": [], + "label": "getPolicyFieldsIdentifiers", + "description": [], + "signature": [ + "CallbackFn | undefined" + ], + "path": "packages/kbn-monaco/src/esql/lib/autocomplete/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.ESQLCustomAutocompleteCallbacks.getPolicyMatchingFieldIdentifiers", + "type": "Function", + "tags": [], + "label": "getPolicyMatchingFieldIdentifiers", + "description": [], + "signature": [ + "CallbackFn | undefined" ], "path": "packages/kbn-monaco/src/esql/lib/autocomplete/types.ts", "deprecated": false, diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index f2561211fbddb..e1d4a47d80bc5 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 71 | 0 | 69 | 3 | +| 74 | 0 | 72 | 3 | ## Common diff --git a/api_docs/kbn_object_versioning.mdx b/api_docs/kbn_object_versioning.mdx index cee70296ebec6..7e85861952f55 100644 --- a/api_docs/kbn_object_versioning.mdx +++ b/api_docs/kbn_object_versioning.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-object-versioning title: "@kbn/object-versioning" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/object-versioning plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/object-versioning'] --- import kbnObjectVersioningObj from './kbn_object_versioning.devdocs.json'; diff --git a/api_docs/kbn_observability_alert_details.mdx b/api_docs/kbn_observability_alert_details.mdx index 8276e0f455d6d..3fb6a56259ba7 100644 --- a/api_docs/kbn_observability_alert_details.mdx +++ b/api_docs/kbn_observability_alert_details.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alert-details title: "@kbn/observability-alert-details" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alert-details plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alert-details'] --- import kbnObservabilityAlertDetailsObj from './kbn_observability_alert_details.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 5a55501f9467a..ab04f163b40df 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index 354fbcba634ad..b3d04d8b790ae 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index b163df01ae225..5680da729406a 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index a11c97784e509..6e12225151607 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index a94e7eb5102da..0c622fb705406 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 3a1b036ca3013..149efa170d300 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_random_sampling.mdx b/api_docs/kbn_random_sampling.mdx index a8949b64310b9..70d41bb5964de 100644 --- a/api_docs/kbn_random_sampling.mdx +++ b/api_docs/kbn_random_sampling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-random-sampling title: "@kbn/random-sampling" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/random-sampling plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/random-sampling'] --- import kbnRandomSamplingObj from './kbn_random_sampling.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 7c712164fd8cb..037d30c889f87 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_common.mdx b/api_docs/kbn_react_kibana_context_common.mdx index 2f2893afffa81..58fbeeee2d187 100644 --- a/api_docs/kbn_react_kibana_context_common.mdx +++ b/api_docs/kbn_react_kibana_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-common title: "@kbn/react-kibana-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-common'] --- import kbnReactKibanaContextCommonObj from './kbn_react_kibana_context_common.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_render.mdx b/api_docs/kbn_react_kibana_context_render.mdx index 3871fd28dfb72..959e2a057f24c 100644 --- a/api_docs/kbn_react_kibana_context_render.mdx +++ b/api_docs/kbn_react_kibana_context_render.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-render title: "@kbn/react-kibana-context-render" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-render plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-render'] --- import kbnReactKibanaContextRenderObj from './kbn_react_kibana_context_render.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_root.mdx b/api_docs/kbn_react_kibana_context_root.mdx index a0e9eafcf0b21..7613d4be794d4 100644 --- a/api_docs/kbn_react_kibana_context_root.mdx +++ b/api_docs/kbn_react_kibana_context_root.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-root title: "@kbn/react-kibana-context-root" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-root plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-root'] --- import kbnReactKibanaContextRootObj from './kbn_react_kibana_context_root.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_styled.mdx b/api_docs/kbn_react_kibana_context_styled.mdx index cca223f50e6a0..0323e7ebc62b4 100644 --- a/api_docs/kbn_react_kibana_context_styled.mdx +++ b/api_docs/kbn_react_kibana_context_styled.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-styled title: "@kbn/react-kibana-context-styled" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-styled plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-styled'] --- import kbnReactKibanaContextStyledObj from './kbn_react_kibana_context_styled.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_theme.mdx b/api_docs/kbn_react_kibana_context_theme.mdx index e3281ee6b5352..cf70a70947460 100644 --- a/api_docs/kbn_react_kibana_context_theme.mdx +++ b/api_docs/kbn_react_kibana_context_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-theme title: "@kbn/react-kibana-context-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-theme plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-theme'] --- import kbnReactKibanaContextThemeObj from './kbn_react_kibana_context_theme.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_mount.mdx b/api_docs/kbn_react_kibana_mount.mdx index 299d5f22b976d..d04209a9f3568 100644 --- a/api_docs/kbn_react_kibana_mount.mdx +++ b/api_docs/kbn_react_kibana_mount.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-mount title: "@kbn/react-kibana-mount" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-mount plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-mount'] --- import kbnReactKibanaMountObj from './kbn_react_kibana_mount.devdocs.json'; diff --git a/api_docs/kbn_repo_file_maps.mdx b/api_docs/kbn_repo_file_maps.mdx index ce8158a8183c7..c2e009a4b77fb 100644 --- a/api_docs/kbn_repo_file_maps.mdx +++ b/api_docs/kbn_repo_file_maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-file-maps title: "@kbn/repo-file-maps" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-file-maps plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-file-maps'] --- import kbnRepoFileMapsObj from './kbn_repo_file_maps.devdocs.json'; diff --git a/api_docs/kbn_repo_linter.mdx b/api_docs/kbn_repo_linter.mdx index 87efc1001f627..31eb94cd3909e 100644 --- a/api_docs/kbn_repo_linter.mdx +++ b/api_docs/kbn_repo_linter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-linter title: "@kbn/repo-linter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-linter plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-linter'] --- import kbnRepoLinterObj from './kbn_repo_linter.devdocs.json'; diff --git a/api_docs/kbn_repo_path.mdx b/api_docs/kbn_repo_path.mdx index ca4a6449f390c..0ea939bbde996 100644 --- a/api_docs/kbn_repo_path.mdx +++ b/api_docs/kbn_repo_path.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-path title: "@kbn/repo-path" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-path plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-path'] --- import kbnRepoPathObj from './kbn_repo_path.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index 2e8a262dd0d34..bd5a2b4bf7477 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_reporting_common.mdx b/api_docs/kbn_reporting_common.mdx index 9d66b484597d9..ece34b24df02d 100644 --- a/api_docs/kbn_reporting_common.mdx +++ b/api_docs/kbn_reporting_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-common title: "@kbn/reporting-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-common plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-common'] --- import kbnReportingCommonObj from './kbn_reporting_common.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index 5c01b7a0b78c6..dcec40e3a89da 100644 --- a/api_docs/kbn_rison.mdx +++ b/api_docs/kbn_rison.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rison title: "@kbn/rison" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rison plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_rrule.mdx b/api_docs/kbn_rrule.mdx index cbc74d84905af..ef9900034a6d5 100644 --- a/api_docs/kbn_rrule.mdx +++ b/api_docs/kbn_rrule.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rrule title: "@kbn/rrule" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rrule plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rrule'] --- import kbnRruleObj from './kbn_rrule.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index d5938991671d2..0393965e2ebb2 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_saved_objects_settings.mdx b/api_docs/kbn_saved_objects_settings.mdx index f82e8f71c8431..3605f1aba2368 100644 --- a/api_docs/kbn_saved_objects_settings.mdx +++ b/api_docs/kbn_saved_objects_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-saved-objects-settings title: "@kbn/saved-objects-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/saved-objects-settings plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/saved-objects-settings'] --- import kbnSavedObjectsSettingsObj from './kbn_saved_objects_settings.devdocs.json'; diff --git a/api_docs/kbn_search_api_panels.devdocs.json b/api_docs/kbn_search_api_panels.devdocs.json index 10d786f99db67..8c1ce9f259ef6 100644 --- a/api_docs/kbn_search_api_panels.devdocs.json +++ b/api_docs/kbn_search_api_panels.devdocs.json @@ -588,7 +588,7 @@ "label": "docLinks", "description": [], "signature": [ - "{ beats: string; connectors: string; logStash: string; }" + "{ beats: string; connectors: string; logstash: string; }" ], "path": "packages/kbn-search-api-panels/components/integrations_panel.tsx", "deprecated": false, @@ -912,6 +912,20 @@ "path": "packages/kbn-search-api-panels/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinitionSnippetArguments.cloudId", + "type": "string", + "tags": [], + "label": "cloudId", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_search_api_panels.mdx b/api_docs/kbn_search_api_panels.mdx index 51ae6d16ef47f..23d87424f2b07 100644 --- a/api_docs/kbn_search_api_panels.mdx +++ b/api_docs/kbn_search_api_panels.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-api-panels title: "@kbn/search-api-panels" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-api-panels plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-api-panels'] --- import kbnSearchApiPanelsObj from './kbn_search_api_panels.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 64 | 0 | 64 | 0 | +| 65 | 0 | 65 | 0 | ## Common diff --git a/api_docs/kbn_search_response_warnings.mdx b/api_docs/kbn_search_response_warnings.mdx index d658e52122353..a7f3a7c301a2f 100644 --- a/api_docs/kbn_search_response_warnings.mdx +++ b/api_docs/kbn_search_response_warnings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-response-warnings title: "@kbn/search-response-warnings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-response-warnings plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-response-warnings'] --- import kbnSearchResponseWarningsObj from './kbn_search_response_warnings.devdocs.json'; diff --git a/api_docs/kbn_security_solution_features.devdocs.json b/api_docs/kbn_security_solution_features.devdocs.json new file mode 100644 index 0000000000000..efeb01d3237a1 --- /dev/null +++ b/api_docs/kbn_security_solution_features.devdocs.json @@ -0,0 +1,487 @@ +{ + "id": "@kbn/security-solution-features", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeatureParams", + "type": "Interface", + "tags": [], + "label": "AppFeatureParams", + "description": [], + "signature": [ + { + "pluginId": "@kbn/security-solution-features", + "scope": "common", + "docId": "kibKbnSecuritySolutionFeaturesPluginApi", + "section": "def-common.AppFeatureParams", + "text": "AppFeatureParams" + }, + "" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeatureParams.baseKibanaFeature", + "type": "Object", + "tags": [], + "label": "baseKibanaFeature", + "description": [], + "signature": [ + "{ id: string; order?: number | undefined; name: string; alerting?: readonly string[] | undefined; cases?: readonly string[] | undefined; description?: string | undefined; category: ", + { + "pluginId": "@kbn/core-application-common", + "scope": "common", + "docId": "kibKbnCoreApplicationCommonPluginApi", + "section": "def-common.AppCategory", + "text": "AppCategory" + }, + "; management?: { [sectionId: string]: readonly string[]; } | undefined; privileges: { all: ", + { + "pluginId": "features", + "scope": "common", + "docId": "kibFeaturesPluginApi", + "section": "def-common.FeatureKibanaPrivileges", + "text": "FeatureKibanaPrivileges" + }, + "; read: ", + { + "pluginId": "features", + "scope": "common", + "docId": "kibFeaturesPluginApi", + "section": "def-common.FeatureKibanaPrivileges", + "text": "FeatureKibanaPrivileges" + }, + "; } | null; catalogue?: readonly string[] | undefined; excludeFromBasePrivileges?: boolean | undefined; minimumLicense?: \"basic\" | \"standard\" | \"gold\" | \"platinum\" | \"enterprise\" | \"trial\" | undefined; app: readonly string[]; privilegesTooltip?: string | undefined; reserved?: { description: string; privileges: readonly ", + "ReservedKibanaPrivilege", + "[]; } | undefined; }" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeatureParams.baseKibanaSubFeatureIds", + "type": "Array", + "tags": [], + "label": "baseKibanaSubFeatureIds", + "description": [], + "signature": [ + "T[]" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeatureParams.subFeaturesMap", + "type": "Object", + "tags": [], + "label": "subFeaturesMap", + "description": [], + "signature": [ + "Map" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeatureKeys", + "type": "Type", + "tags": [], + "label": "AppFeatureKeys", + "description": [], + "signature": [ + { + "pluginId": "@kbn/security-solution-features", + "scope": "common", + "docId": "kibKbnSecuritySolutionFeaturesPluginApi", + "section": "def-common.AppFeatureKeyType", + "text": "AppFeatureKeyType" + }, + "[]" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeatureKeyType", + "type": "Type", + "tags": [], + "label": "AppFeatureKeyType", + "description": [], + "signature": [ + "AppFeatureSecurityKey", + " | ", + "AppFeatureCasesKey", + " | ", + "AppFeatureAssistantKey" + ], + "path": "x-pack/packages/security-solution/features/src/app_features_keys.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeatureKibanaConfig", + "type": "Type", + "tags": [], + "label": "AppFeatureKibanaConfig", + "description": [], + "signature": [ + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "<", + { + "pluginId": "@kbn/security-solution-features", + "scope": "common", + "docId": "kibKbnSecuritySolutionFeaturesPluginApi", + "section": "def-common.BaseKibanaFeatureConfig", + "text": "BaseKibanaFeatureConfig" + }, + "> & { subFeatureIds?: T[] | undefined; subFeaturesPrivileges?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "<", + { + "pluginId": "features", + "scope": "common", + "docId": "kibFeaturesPluginApi", + "section": "def-common.SubFeaturePrivilegeConfig", + "text": "SubFeaturePrivilegeConfig" + }, + ">[] | undefined; }" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeaturesAssistantConfig", + "type": "Type", + "tags": [], + "label": "AppFeaturesAssistantConfig", + "description": [], + "signature": [ + "Map<", + "AppFeatureAssistantKey", + ", ", + { + "pluginId": "@kbn/security-solution-features", + "scope": "common", + "docId": "kibKbnSecuritySolutionFeaturesPluginApi", + "section": "def-common.AppFeatureKibanaConfig", + "text": "AppFeatureKibanaConfig" + }, + "<", + "AssistantSubFeatureId", + ">>" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeaturesCasesConfig", + "type": "Type", + "tags": [], + "label": "AppFeaturesCasesConfig", + "description": [], + "signature": [ + "Map<", + "AppFeatureCasesKey", + ", ", + { + "pluginId": "@kbn/security-solution-features", + "scope": "common", + "docId": "kibKbnSecuritySolutionFeaturesPluginApi", + "section": "def-common.AppFeatureKibanaConfig", + "text": "AppFeatureKibanaConfig" + }, + "<", + "CasesSubFeatureId", + ">>" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeaturesConfig", + "type": "Type", + "tags": [], + "label": "AppFeaturesConfig", + "description": [], + "signature": [ + "Map<", + { + "pluginId": "@kbn/security-solution-features", + "scope": "common", + "docId": "kibKbnSecuritySolutionFeaturesPluginApi", + "section": "def-common.AppFeatureKeyType", + "text": "AppFeatureKeyType" + }, + ", ", + { + "pluginId": "@kbn/security-solution-features", + "scope": "common", + "docId": "kibKbnSecuritySolutionFeaturesPluginApi", + "section": "def-common.AppFeatureKibanaConfig", + "text": "AppFeatureKibanaConfig" + }, + ">" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppFeaturesSecurityConfig", + "type": "Type", + "tags": [], + "label": "AppFeaturesSecurityConfig", + "description": [], + "signature": [ + "Map<", + "AppFeatureSecurityKey", + ", ", + { + "pluginId": "@kbn/security-solution-features", + "scope": "common", + "docId": "kibKbnSecuritySolutionFeaturesPluginApi", + "section": "def-common.AppFeatureKibanaConfig", + "text": "AppFeatureKibanaConfig" + }, + "<", + "SecuritySubFeatureId", + ">>" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.AppSubFeaturesMap", + "type": "Type", + "tags": [], + "label": "AppSubFeaturesMap", + "description": [], + "signature": [ + "Map" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.BaseKibanaFeatureConfig", + "type": "Type", + "tags": [], + "label": "BaseKibanaFeatureConfig", + "description": [], + "signature": [ + "{ id: string; order?: number | undefined; name: string; alerting?: readonly string[] | undefined; cases?: readonly string[] | undefined; description?: string | undefined; category: ", + { + "pluginId": "@kbn/core-application-common", + "scope": "common", + "docId": "kibKbnCoreApplicationCommonPluginApi", + "section": "def-common.AppCategory", + "text": "AppCategory" + }, + "; management?: { [sectionId: string]: readonly string[]; } | undefined; privileges: { all: ", + { + "pluginId": "features", + "scope": "common", + "docId": "kibFeaturesPluginApi", + "section": "def-common.FeatureKibanaPrivileges", + "text": "FeatureKibanaPrivileges" + }, + "; read: ", + { + "pluginId": "features", + "scope": "common", + "docId": "kibFeaturesPluginApi", + "section": "def-common.FeatureKibanaPrivileges", + "text": "FeatureKibanaPrivileges" + }, + "; } | null; catalogue?: readonly string[] | undefined; excludeFromBasePrivileges?: boolean | undefined; minimumLicense?: \"basic\" | \"standard\" | \"gold\" | \"platinum\" | \"enterprise\" | \"trial\" | undefined; app: readonly string[]; privilegesTooltip?: string | undefined; reserved?: { description: string; privileges: readonly ", + "ReservedKibanaPrivilege", + "[]; } | undefined; }" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/security-solution-features", + "id": "def-common.SubFeaturesPrivileges", + "type": "Type", + "tags": [], + "label": "SubFeaturesPrivileges", + "description": [], + "signature": [ + "{ id?: string | undefined; name?: string | undefined; includeIn?: \"none\" | \"read\" | \"all\" | undefined; minimumLicense?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "<\"basic\" | \"standard\" | \"gold\" | \"platinum\" | \"enterprise\" | \"trial\" | undefined>; alerting?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "<{ rule?: { all?: readonly string[] | undefined; read?: readonly string[] | undefined; } | undefined; alert?: { all?: readonly string[] | undefined; read?: readonly string[] | undefined; } | undefined; } | undefined>; cases?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "<{ all?: readonly string[] | undefined; push?: readonly string[] | undefined; create?: readonly string[] | undefined; read?: readonly string[] | undefined; update?: readonly string[] | undefined; delete?: readonly string[] | undefined; } | undefined>; disabled?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "; management?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "<{ [sectionId: string]: readonly string[]; } | undefined>; ui?: readonly string[] | undefined; catalogue?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "; app?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "; requireAllSpaces?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "; api?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "; savedObject?: ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "<{ all: readonly string[]; read: readonly string[]; }> | undefined; }" + ], + "path": "x-pack/packages/security-solution/features/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_security_solution_features.mdx b/api_docs/kbn_security_solution_features.mdx new file mode 100644 index 0000000000000..feff9a8532c37 --- /dev/null +++ b/api_docs/kbn_security_solution_features.mdx @@ -0,0 +1,33 @@ +--- +#### +#### 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: kibKbnSecuritySolutionFeaturesPluginApi +slug: /kibana-dev-docs/api/kbn-security-solution-features +title: "@kbn/security-solution-features" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/security-solution-features plugin +date: 2023-09-05 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-features'] +--- +import kbnSecuritySolutionFeaturesObj from './kbn_security_solution_features.devdocs.json'; + + + +Contact [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 14 | 0 | 14 | 6 | + +## Common + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_security_solution_navigation.mdx b/api_docs/kbn_security_solution_navigation.mdx index c487ad465c6d7..43b8698ec5add 100644 --- a/api_docs/kbn_security_solution_navigation.mdx +++ b/api_docs/kbn_security_solution_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-navigation title: "@kbn/security-solution-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-navigation plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-navigation'] --- import kbnSecuritySolutionNavigationObj from './kbn_security_solution_navigation.devdocs.json'; diff --git a/api_docs/kbn_security_solution_side_nav.mdx b/api_docs/kbn_security_solution_side_nav.mdx index 2afb39253c2c8..0edc300b7fbb8 100644 --- a/api_docs/kbn_security_solution_side_nav.mdx +++ b/api_docs/kbn_security_solution_side_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-side-nav title: "@kbn/security-solution-side-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-side-nav plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-side-nav'] --- import kbnSecuritySolutionSideNavObj from './kbn_security_solution_side_nav.devdocs.json'; diff --git a/api_docs/kbn_security_solution_storybook_config.mdx b/api_docs/kbn_security_solution_storybook_config.mdx index f4d968a9b8ae7..303b370aed662 100644 --- a/api_docs/kbn_security_solution_storybook_config.mdx +++ b/api_docs/kbn_security_solution_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-storybook-config title: "@kbn/security-solution-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-storybook-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-storybook-config'] --- import kbnSecuritySolutionStorybookConfigObj from './kbn_security_solution_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 160e4b33b779a..0e8e3606673c7 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_data_table.mdx b/api_docs/kbn_securitysolution_data_table.mdx index 3b4edd18420ac..39fd4b1f96d01 100644 --- a/api_docs/kbn_securitysolution_data_table.mdx +++ b/api_docs/kbn_securitysolution_data_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-data-table title: "@kbn/securitysolution-data-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-data-table plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-data-table'] --- import kbnSecuritysolutionDataTableObj from './kbn_securitysolution_data_table.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_ecs.mdx b/api_docs/kbn_securitysolution_ecs.mdx index 5d11cfdca5b7f..e1881be0c8187 100644 --- a/api_docs/kbn_securitysolution_ecs.mdx +++ b/api_docs/kbn_securitysolution_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-ecs title: "@kbn/securitysolution-ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-ecs plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-ecs'] --- import kbnSecuritysolutionEcsObj from './kbn_securitysolution_ecs.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 8404d230bf212..1c4796084d273 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.devdocs.json b/api_docs/kbn_securitysolution_exception_list_components.devdocs.json index e590d21733685..0b11d9dedc1c3 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.devdocs.json +++ b/api_docs/kbn_securitysolution_exception_list_components.devdocs.json @@ -834,7 +834,7 @@ "label": "formattedDateComponent", "description": [], "signature": [ - "\"symbol\" | \"object\" | \"big\" | \"link\" | \"small\" | \"sub\" | \"sup\" | \"source\" | \"desc\" | \"filter\" | \"text\" | \"map\" | \"head\" | React.ComponentType | \"slot\" | \"style\" | \"title\" | \"meta\" | \"pattern\" | \"summary\" | \"template\" | \"span\" | \"main\" | \"path\" | \"form\" | \"body\" | \"q\" | \"label\" | \"data\" | \"progress\" | \"legend\" | \"article\" | \"image\" | \"menu\" | \"stop\" | \"base\" | \"s\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"canvas\" | \"svg\" | \"select\" | \"output\" | \"script\" | \"time\" | \"mask\" | \"input\" | \"section\" | \"circle\" | \"code\" | \"line\" | \"area\" | \"animate\" | \"view\" | \"var\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"clipPath\" | \"textarea\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"caption\" | \"cite\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"strong\" | \"table\" | \"tbody\" | \"td\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + "\"symbol\" | \"object\" | \"big\" | \"link\" | \"small\" | \"sub\" | \"sup\" | \"source\" | \"desc\" | \"filter\" | \"text\" | \"map\" | \"head\" | React.ComponentType | \"slot\" | \"style\" | \"title\" | \"meta\" | \"pattern\" | \"summary\" | \"template\" | \"span\" | \"main\" | \"path\" | \"form\" | \"body\" | \"q\" | \"label\" | \"data\" | \"progress\" | \"legend\" | \"article\" | \"image\" | \"menu\" | \"stop\" | \"base\" | \"s\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"canvas\" | \"svg\" | \"select\" | \"output\" | \"script\" | \"time\" | \"mask\" | \"input\" | \"clipPath\" | \"section\" | \"circle\" | \"code\" | \"line\" | \"area\" | \"animate\" | \"view\" | \"var\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"textarea\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"caption\" | \"cite\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"strong\" | \"table\" | \"tbody\" | \"td\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" ], "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/index.tsx", "deprecated": false, @@ -848,7 +848,7 @@ "label": "securityLinkAnchorComponent", "description": [], "signature": [ - "\"symbol\" | \"object\" | \"big\" | \"link\" | \"small\" | \"sub\" | \"sup\" | \"source\" | \"desc\" | \"filter\" | \"text\" | \"map\" | \"head\" | React.ComponentType | \"slot\" | \"style\" | \"title\" | \"meta\" | \"pattern\" | \"summary\" | \"template\" | \"span\" | \"main\" | \"path\" | \"form\" | \"body\" | \"q\" | \"label\" | \"data\" | \"progress\" | \"legend\" | \"article\" | \"image\" | \"menu\" | \"stop\" | \"base\" | \"s\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"canvas\" | \"svg\" | \"select\" | \"output\" | \"script\" | \"time\" | \"mask\" | \"input\" | \"section\" | \"circle\" | \"code\" | \"line\" | \"area\" | \"animate\" | \"view\" | \"var\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"clipPath\" | \"textarea\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"caption\" | \"cite\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"strong\" | \"table\" | \"tbody\" | \"td\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + "\"symbol\" | \"object\" | \"big\" | \"link\" | \"small\" | \"sub\" | \"sup\" | \"source\" | \"desc\" | \"filter\" | \"text\" | \"map\" | \"head\" | React.ComponentType | \"slot\" | \"style\" | \"title\" | \"meta\" | \"pattern\" | \"summary\" | \"template\" | \"span\" | \"main\" | \"path\" | \"form\" | \"body\" | \"q\" | \"label\" | \"data\" | \"progress\" | \"legend\" | \"article\" | \"image\" | \"menu\" | \"stop\" | \"base\" | \"s\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"canvas\" | \"svg\" | \"select\" | \"output\" | \"script\" | \"time\" | \"mask\" | \"input\" | \"clipPath\" | \"section\" | \"circle\" | \"code\" | \"line\" | \"area\" | \"animate\" | \"view\" | \"var\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"textarea\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"caption\" | \"cite\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"strong\" | \"table\" | \"tbody\" | \"td\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" ], "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/index.tsx", "deprecated": false, @@ -987,7 +987,7 @@ "label": "securityLinkAnchorComponent", "description": [], "signature": [ - "\"symbol\" | \"object\" | \"big\" | \"link\" | \"small\" | \"sub\" | \"sup\" | \"source\" | \"desc\" | \"filter\" | \"text\" | \"map\" | \"head\" | React.ComponentType | \"slot\" | \"style\" | \"title\" | \"meta\" | \"pattern\" | \"summary\" | \"template\" | \"span\" | \"main\" | \"path\" | \"form\" | \"body\" | \"q\" | \"label\" | \"data\" | \"progress\" | \"legend\" | \"article\" | \"image\" | \"menu\" | \"stop\" | \"base\" | \"s\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"canvas\" | \"svg\" | \"select\" | \"output\" | \"script\" | \"time\" | \"mask\" | \"input\" | \"section\" | \"circle\" | \"code\" | \"line\" | \"area\" | \"animate\" | \"view\" | \"var\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"clipPath\" | \"textarea\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"caption\" | \"cite\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"strong\" | \"table\" | \"tbody\" | \"td\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + "\"symbol\" | \"object\" | \"big\" | \"link\" | \"small\" | \"sub\" | \"sup\" | \"source\" | \"desc\" | \"filter\" | \"text\" | \"map\" | \"head\" | React.ComponentType | \"slot\" | \"style\" | \"title\" | \"meta\" | \"pattern\" | \"summary\" | \"template\" | \"span\" | \"main\" | \"path\" | \"form\" | \"body\" | \"q\" | \"label\" | \"data\" | \"progress\" | \"legend\" | \"article\" | \"image\" | \"menu\" | \"stop\" | \"base\" | \"s\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"canvas\" | \"svg\" | \"select\" | \"output\" | \"script\" | \"time\" | \"mask\" | \"input\" | \"clipPath\" | \"section\" | \"circle\" | \"code\" | \"line\" | \"area\" | \"animate\" | \"view\" | \"var\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"textarea\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"caption\" | \"cite\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"strong\" | \"table\" | \"tbody\" | \"td\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" ], "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", "deprecated": false, @@ -1001,7 +1001,7 @@ "label": "formattedDateComponent", "description": [], "signature": [ - "\"symbol\" | \"object\" | \"big\" | \"link\" | \"small\" | \"sub\" | \"sup\" | \"source\" | \"desc\" | \"filter\" | \"text\" | \"map\" | \"head\" | React.ComponentType | \"slot\" | \"style\" | \"title\" | \"meta\" | \"pattern\" | \"summary\" | \"template\" | \"span\" | \"main\" | \"path\" | \"form\" | \"body\" | \"q\" | \"label\" | \"data\" | \"progress\" | \"legend\" | \"article\" | \"image\" | \"menu\" | \"stop\" | \"base\" | \"s\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"canvas\" | \"svg\" | \"select\" | \"output\" | \"script\" | \"time\" | \"mask\" | \"input\" | \"section\" | \"circle\" | \"code\" | \"line\" | \"area\" | \"animate\" | \"view\" | \"var\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"clipPath\" | \"textarea\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"caption\" | \"cite\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"strong\" | \"table\" | \"tbody\" | \"td\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + "\"symbol\" | \"object\" | \"big\" | \"link\" | \"small\" | \"sub\" | \"sup\" | \"source\" | \"desc\" | \"filter\" | \"text\" | \"map\" | \"head\" | React.ComponentType | \"slot\" | \"style\" | \"title\" | \"meta\" | \"pattern\" | \"summary\" | \"template\" | \"span\" | \"main\" | \"path\" | \"form\" | \"body\" | \"q\" | \"label\" | \"data\" | \"progress\" | \"legend\" | \"article\" | \"image\" | \"menu\" | \"stop\" | \"base\" | \"s\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"canvas\" | \"svg\" | \"select\" | \"output\" | \"script\" | \"time\" | \"mask\" | \"input\" | \"clipPath\" | \"section\" | \"circle\" | \"code\" | \"line\" | \"area\" | \"animate\" | \"view\" | \"var\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"textarea\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"caption\" | \"cite\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"strong\" | \"table\" | \"tbody\" | \"td\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" ], "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", "deprecated": false, diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index 928cafcdeddab..057fd8ddd0154 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_grouping.mdx b/api_docs/kbn_securitysolution_grouping.mdx index 38cc77464c55e..bcab35e3734e8 100644 --- a/api_docs/kbn_securitysolution_grouping.mdx +++ b/api_docs/kbn_securitysolution_grouping.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-grouping title: "@kbn/securitysolution-grouping" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-grouping plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-grouping'] --- import kbnSecuritysolutionGroupingObj from './kbn_securitysolution_grouping.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index 23a4d7539f34c..6188b4f538622 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 096803d5d1dcc..ee9589bd9fdab 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index 517925775389f..55ab104bac818 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 63f8756358135..16df8b1d3143c 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 3b8453ee7140b..9d02dbe8bf408 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index e1460523bb773..47fd8f9a4d494 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.devdocs.json b/api_docs/kbn_securitysolution_list_constants.devdocs.json index f39f80212e340..c3f2279e6ccde 100644 --- a/api_docs/kbn_securitysolution_list_constants.devdocs.json +++ b/api_docs/kbn_securitysolution_list_constants.devdocs.json @@ -167,6 +167,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" @@ -431,6 +435,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" @@ -639,6 +647,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" @@ -1084,6 +1096,10 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts" + }, { "plugin": "@kbn/securitysolution-io-ts-list-types", "path": "packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_schema/index.mock.ts" diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 8352096276e14..5c0ca05384879 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index f706f8f6f4905..806945c74d6ef 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index df70e22184234..67c024298e1af 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index 621e624f7b3d0..ae9982c0af157 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index 6a4b404c44234..f9802c7fe0576 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.devdocs.json b/api_docs/kbn_securitysolution_utils.devdocs.json index 54c9d41a2d799..c755b84836844 100644 --- a/api_docs/kbn_securitysolution_utils.devdocs.json +++ b/api_docs/kbn_securitysolution_utils.devdocs.json @@ -495,7 +495,7 @@ "label": "BlocklistConditionEntryField", "description": [], "signature": [ - "\"file.path\" | \"file.hash.*\" | \"file.Ext.code_signature\" | \"file.path.caseless\"" + "\"file.path\" | \"file.Ext.code_signature\" | \"file.hash.*\" | \"file.path.caseless\"" ], "path": "packages/kbn-securitysolution-utils/src/path_validations/index.ts", "deprecated": false, diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index c458c7ab9d639..6dc1d8f5b2b13 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index d3775e0321552..1b4eefb68fc5f 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index 95acb3c677003..e106cb033c5c2 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_serverless_project_switcher.mdx b/api_docs/kbn_serverless_project_switcher.mdx index aa6da93f9c477..f9ad4f267548f 100644 --- a/api_docs/kbn_serverless_project_switcher.mdx +++ b/api_docs/kbn_serverless_project_switcher.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-project-switcher title: "@kbn/serverless-project-switcher" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-project-switcher plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-project-switcher'] --- import kbnServerlessProjectSwitcherObj from './kbn_serverless_project_switcher.devdocs.json'; diff --git a/api_docs/kbn_serverless_storybook_config.mdx b/api_docs/kbn_serverless_storybook_config.mdx index 11f040fbf0ade..8f8629d869e68 100644 --- a/api_docs/kbn_serverless_storybook_config.mdx +++ b/api_docs/kbn_serverless_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-storybook-config title: "@kbn/serverless-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-storybook-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-storybook-config'] --- import kbnServerlessStorybookConfigObj from './kbn_serverless_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index 6b612c9b52e3c..82f04c8c1f8f3 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index 0f2876009bf6d..e4ec0a2a0470d 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index 7e36237dedc4b..332ee7b559786 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index db9395e88f664..b342b28412dd3 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index b0f90559e6ae0..ea0dee0f20096 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index e09d1bb60fb53..bed943cf01244 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index f4e62f597966a..298b26586d47f 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index 69199b1416456..f8e4d0340abe9 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_chrome_navigation.mdx b/api_docs/kbn_shared_ux_chrome_navigation.mdx index cf16454368cd0..a9cfd49bd9006 100644 --- a/api_docs/kbn_shared_ux_chrome_navigation.mdx +++ b/api_docs/kbn_shared_ux_chrome_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-chrome-navigation title: "@kbn/shared-ux-chrome-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-chrome-navigation plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-chrome-navigation'] --- import kbnSharedUxChromeNavigationObj from './kbn_shared_ux_chrome_navigation.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index 23f9d0a2811e3..571aa579476e0 100644 --- a/api_docs/kbn_shared_ux_file_context.mdx +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-context title: "@kbn/shared-ux-file-context" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-context plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] --- import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index 830594a259441..d320a6fe79159 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index 5b1ac2d313724..7a01e2db59806 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx index 92f4185ba9485..05b8167d72a32 100644 --- a/api_docs/kbn_shared_ux_file_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks title: "@kbn/shared-ux-file-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] --- import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_picker.mdx b/api_docs/kbn_shared_ux_file_picker.mdx index 856f141ffb8ed..7fe50b1e6efa5 100644 --- a/api_docs/kbn_shared_ux_file_picker.mdx +++ b/api_docs/kbn_shared_ux_file_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-picker title: "@kbn/shared-ux-file-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-picker plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-picker'] --- import kbnSharedUxFilePickerObj from './kbn_shared_ux_file_picker.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_types.mdx b/api_docs/kbn_shared_ux_file_types.mdx index 8b30f58d1014e..341baae0eddc6 100644 --- a/api_docs/kbn_shared_ux_file_types.mdx +++ b/api_docs/kbn_shared_ux_file_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-types title: "@kbn/shared-ux-file-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-types'] --- import kbnSharedUxFileTypesObj from './kbn_shared_ux_file_types.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_upload.mdx b/api_docs/kbn_shared_ux_file_upload.mdx index ee118d8d30131..b5bdafbe3406e 100644 --- a/api_docs/kbn_shared_ux_file_upload.mdx +++ b/api_docs/kbn_shared_ux_file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-upload title: "@kbn/shared-ux-file-upload" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-upload plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-upload'] --- import kbnSharedUxFileUploadObj from './kbn_shared_ux_file_upload.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index 4d209eb741ade..bd9f0e5e054b5 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index e6ad6ff96ef6f..bdb21965fe310 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index 7a58af1dd23c1..f8166ef3df380 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index cb8c346fe03d4..034c8d773754d 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index 4704d8ff04654..47ecb7343d741 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index 8f137c1b19bae..c5369259e7b4d 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index 43b35150ca54f..b9d0e9b498902 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index 76beef22dab87..2bd6c3050ac8f 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index 7287acc7bf6d6..35e91bb002744 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index 7a4c6b9f0ca79..c1cf40e7bc764 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index 42191d0e64159..86fa9e7c1891f 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index 274d7abec9027..3c8e084113bee 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index 8ad594e4af757..28f32b9dc5257 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index 17435ce9d3f78..997a796e86d22 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index 875216415544c..f470445bd947b 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index 957c1bb26ac28..33873f09e4810 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index 21228fc56e777..274db8e8bfa85 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index f9d069f8e210f..9a775023878d6 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx index ce4ddbf658b33..aed9adc9f36ba 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.mdx +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found title: "@kbn/shared-ux-prompt-not-found" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-not-found plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] --- import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index 71e814e3be0d8..07c8ac15e2ccb 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index d37edc70374c6..129749e6731df 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 18e25c6928684..d36240ef6c53e 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index a1cd940e9f883..cf78e32a622ed 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index e3ace32ceff37..d3a1875b6ce61 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_slo_schema.mdx b/api_docs/kbn_slo_schema.mdx index fa014ce11dddb..0839df57edc00 100644 --- a/api_docs/kbn_slo_schema.mdx +++ b/api_docs/kbn_slo_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-slo-schema title: "@kbn/slo-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/slo-schema plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/slo-schema'] --- import kbnSloSchemaObj from './kbn_slo_schema.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index 5e91eb8810ae4..407b4ab1645d0 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index e5ab024d5bf00..8a7b3d8d10eba 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index 257d1a20a394c..8af618fd3428f 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index 5b1672a76e614..dbffcc85a47b8 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index 4f3af81ac9d76..6b1ecb4fc1a93 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.devdocs.json b/api_docs/kbn_test.devdocs.json index 862d934e85acc..86f6572ae1162 100644 --- a/api_docs/kbn_test.devdocs.json +++ b/api_docs/kbn_test.devdocs.json @@ -2089,6 +2089,41 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/test", + "id": "def-common.getDockerFileMountPath", + "type": "Function", + "tags": [], + "label": "getDockerFileMountPath", + "description": [ + "\nRemoves REPO_ROOT from hostPath. Keep the rest to avoid filename collisions.\nReturns the path where a file will be mounted inside the ES or ESS container.\n/root/kibana/package/foo/bar.json => /usr/share/elasticsearch/files/package/foo/bar.json" + ], + "signature": [ + "(hostPath: string) => string" + ], + "path": "packages/kbn-es/src/utils/docker.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/test", + "id": "def-common.getDockerFileMountPath.$1", + "type": "string", + "tags": [], + "label": "hostPath", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-es/src/utils/docker.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/test", "id": "def-common.getKibanaCliArg", @@ -2684,7 +2719,7 @@ "section": "def-common.EsVersion", "text": "EsVersion" }, - "; bail: boolean; dryRun: boolean; updateBaselines: boolean; updateSnapshots: boolean; logsDir: string | undefined; esFrom: \"source\" | \"snapshot\"; installDir: string | undefined; grep: string | undefined; suiteTags: { include: string[]; exclude: string[]; }; suiteFilters: { include: string[]; exclude: string[]; }; }) => Promise" + "; bail: boolean; dryRun: boolean; updateBaselines: boolean; updateSnapshots: boolean; logsDir: string | undefined; esFrom: \"source\" | \"snapshot\" | \"serverless\" | undefined; installDir: string | undefined; grep: string | undefined; suiteTags: { include: string[]; exclude: string[]; }; suiteFilters: { include: string[]; exclude: string[]; }; }) => Promise" ], "path": "packages/kbn-test/src/functional_tests/run_tests/run_tests.ts", "deprecated": false, @@ -2727,7 +2762,7 @@ "section": "def-common.EsVersion", "text": "EsVersion" }, - "; bail: boolean; dryRun: boolean; updateBaselines: boolean; updateSnapshots: boolean; logsDir: string | undefined; esFrom: \"source\" | \"snapshot\"; installDir: string | undefined; grep: string | undefined; suiteTags: { include: string[]; exclude: string[]; }; suiteFilters: { include: string[]; exclude: string[]; }; }" + "; bail: boolean; dryRun: boolean; updateBaselines: boolean; updateSnapshots: boolean; logsDir: string | undefined; esFrom: \"source\" | \"snapshot\" | \"serverless\" | undefined; installDir: string | undefined; grep: string | undefined; suiteTags: { include: string[]; exclude: string[]; }; suiteFilters: { include: string[]; exclude: string[]; }; }" ], "path": "packages/kbn-test/src/functional_tests/run_tests/run_tests.ts", "deprecated": false, @@ -2819,7 +2854,7 @@ "section": "def-common.ToolingLog", "text": "ToolingLog" }, - ", options: { config: string; esFrom: \"source\" | \"snapshot\" | undefined; esVersion: ", + ", options: { config: string; esFrom: \"source\" | \"snapshot\" | \"serverless\" | undefined; esVersion: ", { "pluginId": "@kbn/test", "scope": "common", @@ -2862,7 +2897,7 @@ "label": "options", "description": [], "signature": [ - "{ config: string; esFrom: \"source\" | \"snapshot\" | undefined; esVersion: ", + "{ config: string; esFrom: \"source\" | \"snapshot\" | \"serverless\" | undefined; esVersion: ", { "pluginId": "@kbn/test", "scope": "common", @@ -3254,6 +3289,38 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "@kbn/test", + "id": "def-common.CreateTestEsClusterOptions.serverless", + "type": "CompoundType", + "tags": [], + "label": "serverless", + "description": [ + "\nIs this a serverless project" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-test/src/es/test_es_cluster.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/test", + "id": "def-common.CreateTestEsClusterOptions.files", + "type": "Array", + "tags": [], + "label": "files", + "description": [ + "\nFiles to mount inside ES containers" + ], + "signature": [ + "string[] | undefined" + ], + "path": "packages/kbn-test/src/es/test_es_cluster.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -5238,6 +5305,42 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/test", + "id": "def-common.kibanaTestSuperuserServerless", + "type": "Object", + "tags": [], + "label": "kibanaTestSuperuserServerless", + "description": [], + "path": "packages/kbn-test/src/kbn/users.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/test", + "id": "def-common.kibanaTestSuperuserServerless.username", + "type": "string", + "tags": [], + "label": "username", + "description": [], + "path": "packages/kbn-test/src/kbn/users.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/test", + "id": "def-common.kibanaTestSuperuserServerless.password", + "type": "string", + "tags": [], + "label": "password", + "description": [], + "path": "packages/kbn-test/src/kbn/users.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/test", "id": "def-common.kibanaTestUser", diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 12254960a3bcd..f25b8eca3b096 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kiban | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 282 | 4 | 238 | 12 | +| 289 | 4 | 242 | 12 | ## Common diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 4d8ddb2f69963..5077de40d935f 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index e5fa037280d38..43372f3125e20 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_text_based_editor.devdocs.json b/api_docs/kbn_text_based_editor.devdocs.json index 1eb1abc6f8275..e0ba49c3e1b0e 100644 --- a/api_docs/kbn_text_based_editor.devdocs.json +++ b/api_docs/kbn_text_based_editor.devdocs.json @@ -203,6 +203,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "@kbn/text-based-editor", + "id": "def-public.TextBasedLanguagesEditorProps.warning", + "type": "string", + "tags": [], + "label": "warning", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "@kbn/text-based-editor", "id": "def-public.TextBasedLanguagesEditorProps.isDisabled", @@ -244,6 +258,20 @@ "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/text-based-editor", + "id": "def-public.TextBasedLanguagesEditorProps.hideMinimizeButton", + "type": "CompoundType", + "tags": [], + "label": "hideMinimizeButton", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_text_based_editor.mdx b/api_docs/kbn_text_based_editor.mdx index d0c71da058945..9e04bda94e0e3 100644 --- a/api_docs/kbn_text_based_editor.mdx +++ b/api_docs/kbn_text_based_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-text-based-editor title: "@kbn/text-based-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/text-based-editor plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/text-based-editor'] --- import kbnTextBasedEditorObj from './kbn_text_based_editor.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 15 | 0 | 14 | 0 | +| 17 | 0 | 16 | 0 | ## Client diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index b20f45a9c4058..871c6c2b8e718 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx index f5eabeb06ea4c..be3c8e29d5516 100644 --- a/api_docs/kbn_ts_projects.mdx +++ b/api_docs/kbn_ts_projects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ts-projects title: "@kbn/ts-projects" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ts-projects plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ts-projects'] --- import kbnTsProjectsObj from './kbn_ts_projects.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index 31ea7d67456a5..46f8c19cf89ec 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; diff --git a/api_docs/kbn_ui_actions_browser.mdx b/api_docs/kbn_ui_actions_browser.mdx index 1358a1714994e..d8b53fc2d669f 100644 --- a/api_docs/kbn_ui_actions_browser.mdx +++ b/api_docs/kbn_ui_actions_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-actions-browser title: "@kbn/ui-actions-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-actions-browser plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-actions-browser'] --- import kbnUiActionsBrowserObj from './kbn_ui_actions_browser.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index f3d423de49ff6..a08cefa0974a5 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index f22c2c15c35fc..68e956c6a9fd7 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_unified_data_table.devdocs.json b/api_docs/kbn_unified_data_table.devdocs.json new file mode 100644 index 0000000000000..ae7cb737b9638 --- /dev/null +++ b/api_docs/kbn_unified_data_table.devdocs.json @@ -0,0 +1,1584 @@ +{ + "id": "@kbn/unified-data-table", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.getDisplayedColumns", + "type": "Function", + "tags": [], + "label": "getDisplayedColumns", + "description": [ + "\nFunction to provide fallback when\n1) no columns are given\n2) Just one column is given, which is the configured timefields" + ], + "signature": [ + "(stateColumns: string[], dataView: ", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + }, + ") => string[]" + ], + "path": "packages/kbn-unified-data-table/src/utils/columns.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.getDisplayedColumns.$1", + "type": "Array", + "tags": [], + "label": "stateColumns", + "description": [], + "signature": [ + "string[]" + ], + "path": "packages/kbn-unified-data-table/src/utils/columns.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.getDisplayedColumns.$2", + "type": "Object", + "tags": [], + "label": "dataView", + "description": [], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + } + ], + "path": "packages/kbn-unified-data-table/src/utils/columns.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.getRowsPerPageOptions", + "type": "Function", + "tags": [], + "label": "getRowsPerPageOptions", + "description": [], + "signature": [ + "(currentRowsPerPage?: number | undefined) => number[]" + ], + "path": "packages/kbn-unified-data-table/src/utils/rows_per_page.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.getRowsPerPageOptions.$1", + "type": "number", + "tags": [], + "label": "currentRowsPerPage", + "description": [], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-unified-data-table/src/utils/rows_per_page.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.JSONCodeEditorCommonMemoized", + "type": "Function", + "tags": [], + "label": "JSONCodeEditorCommonMemoized", + "description": [], + "signature": [ + "React.NamedExoticComponent & { readonly type: (props: JsonCodeEditorCommonProps) => JSX.Element; }" + ], + "path": "packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor_common.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.JSONCodeEditorCommonMemoized.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.popularizeField", + "type": "Function", + "tags": [], + "label": "popularizeField", + "description": [], + "signature": [ + "(dataView: ", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + }, + ", fieldName: string, DataViewsService: ", + { + "pluginId": "dataViews", + "scope": "public", + "docId": "kibDataViewsPluginApi", + "section": "def-public.DataViewsServicePublic", + "text": "DataViewsServicePublic" + }, + ", capabilities: ", + { + "pluginId": "@kbn/core-capabilities-common", + "scope": "common", + "docId": "kibKbnCoreCapabilitiesCommonPluginApi", + "section": "def-common.Capabilities", + "text": "Capabilities" + }, + ") => Promise" + ], + "path": "packages/kbn-unified-data-table/src/utils/popularize_field.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.popularizeField.$1", + "type": "Object", + "tags": [], + "label": "dataView", + "description": [], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + } + ], + "path": "packages/kbn-unified-data-table/src/utils/popularize_field.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.popularizeField.$2", + "type": "string", + "tags": [], + "label": "fieldName", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-unified-data-table/src/utils/popularize_field.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.popularizeField.$3", + "type": "Object", + "tags": [], + "label": "DataViewsService", + "description": [], + "signature": [ + { + "pluginId": "dataViews", + "scope": "public", + "docId": "kibDataViewsPluginApi", + "section": "def-public.DataViewsServicePublic", + "text": "DataViewsServicePublic" + } + ], + "path": "packages/kbn-unified-data-table/src/utils/popularize_field.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.popularizeField.$4", + "type": "Object", + "tags": [], + "label": "capabilities", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-capabilities-common", + "scope": "common", + "docId": "kibKbnCoreCapabilitiesCommonPluginApi", + "section": "def-common.Capabilities", + "text": "Capabilities" + } + ], + "path": "packages/kbn-unified-data-table/src/utils/popularize_field.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTable", + "type": "Function", + "tags": [], + "label": "UnifiedDataTable", + "description": [], + "signature": [ + "({ ariaLabelledBy, columns, controlColumnIds, dataView, loadingState, onFilter, onResize, onSetColumns, onSort, rows, sampleSize, searchDescription, searchTitle, settings, showTimeCol, showFullScreenButton, sort, useNewFieldsApi, isSortEnabled, isPaginationEnabled, cellActionsTriggerId, className, rowHeightState, onUpdateRowHeight, isPlainRecord, rowsPerPageState, onUpdateRowsPerPage, onFieldEdited, services, renderCustomGridBody, trailingControlColumns, totalHits, onFetchMoreRecords, renderDocumentView, setExpandedDoc, expandedDoc, configRowHeight, showMultiFields, maxDocFieldsDisplayed, externalControlColumns, externalAdditionalControls, rowsPerPageOptions, visibleCellActions, externalCustomRenderers, consumer, componentsTourSteps, }: ", + { + "pluginId": "@kbn/unified-data-table", + "scope": "common", + "docId": "kibKbnUnifiedDataTablePluginApi", + "section": "def-common.UnifiedDataTableProps", + "text": "UnifiedDataTableProps" + }, + ") => JSX.Element" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTable.$1", + "type": "Object", + "tags": [], + "label": "{\n ariaLabelledBy,\n columns,\n controlColumnIds = CONTROL_COLUMN_IDS_DEFAULT,\n dataView,\n loadingState,\n onFilter,\n onResize,\n onSetColumns,\n onSort,\n rows,\n sampleSize,\n searchDescription,\n searchTitle,\n settings,\n showTimeCol,\n showFullScreenButton = true,\n sort,\n useNewFieldsApi,\n isSortEnabled = true,\n isPaginationEnabled = true,\n cellActionsTriggerId,\n className,\n rowHeightState,\n onUpdateRowHeight,\n isPlainRecord = false,\n rowsPerPageState,\n onUpdateRowsPerPage,\n onFieldEdited,\n services,\n renderCustomGridBody,\n trailingControlColumns,\n totalHits,\n onFetchMoreRecords,\n renderDocumentView,\n setExpandedDoc,\n expandedDoc,\n configRowHeight,\n showMultiFields = true,\n maxDocFieldsDisplayed = 50,\n externalControlColumns,\n externalAdditionalControls,\n rowsPerPageOptions,\n visibleCellActions,\n externalCustomRenderers,\n consumer = 'discover',\n componentsTourSteps,\n}", + "description": [], + "signature": [ + { + "pluginId": "@kbn/unified-data-table", + "scope": "common", + "docId": "kibKbnUnifiedDataTablePluginApi", + "section": "def-common.UnifiedDataTableProps", + "text": "UnifiedDataTableProps" + } + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.useColumns", + "type": "Function", + "tags": [], + "label": "useColumns", + "description": [], + "signature": [ + "({ capabilities, dataView, dataViews, setAppState, useNewFieldsApi, columns, sort, defaultOrder, }: UseColumnsProps) => { columns: string[]; onAddColumn: (columnName: string) => void; onRemoveColumn: (columnName: string) => void; onMoveColumn: (columnName: string, newIndex: number) => void; onSetColumns: (nextColumns: string[], hideTimeColumn: boolean) => void; }" + ], + "path": "packages/kbn-unified-data-table/src/hooks/use_data_grid_columns.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.useColumns.$1", + "type": "Object", + "tags": [], + "label": "{\n capabilities,\n dataView,\n dataViews,\n setAppState,\n useNewFieldsApi,\n columns,\n sort,\n defaultOrder = 'desc',\n}", + "description": [], + "signature": [ + "UseColumnsProps" + ], + "path": "packages/kbn-unified-data-table/src/hooks/use_data_grid_columns.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps", + "type": "Interface", + "tags": [], + "label": "UnifiedDataTableProps", + "description": [], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.ariaLabelledBy", + "type": "string", + "tags": [], + "label": "ariaLabelledBy", + "description": [ + "\nDetermines which element labels the grid for ARIA" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.className", + "type": "string", + "tags": [], + "label": "className", + "description": [ + "\nOptional class name to apply" + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.columns", + "type": "Array", + "tags": [], + "label": "columns", + "description": [ + "\nDetermines ids of the columns which are displayed" + ], + "signature": [ + "string[]" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.expandedDoc", + "type": "Object", + "tags": [], + "label": "expandedDoc", + "description": [ + "\nIf set, the given document is displayed in a flyout" + ], + "signature": [ + "DataTableRecord", + " | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.dataView", + "type": "Object", + "tags": [], + "label": "dataView", + "description": [ + "\nThe used data view" + ], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + } + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.loadingState", + "type": "Enum", + "tags": [], + "label": "loadingState", + "description": [ + "\nDetermines if data is currently loaded" + ], + "signature": [ + { + "pluginId": "@kbn/unified-data-table", + "scope": "common", + "docId": "kibKbnUnifiedDataTablePluginApi", + "section": "def-common.DataLoadingState", + "text": "DataLoadingState" + } + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onFilter", + "type": "Function", + "tags": [], + "label": "onFilter", + "description": [ + "\nFunction to add a filter in the grid cell or document flyout" + ], + "signature": [ + "(mapping: string | ", + "FieldMapping", + " | undefined, value: unknown, mode: \"+\" | \"-\") => void" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onFilter.$1", + "type": "CompoundType", + "tags": [], + "label": "mapping", + "description": [], + "signature": [ + "string | ", + "FieldMapping", + " | undefined" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onFilter.$2", + "type": "Unknown", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "unknown" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onFilter.$3", + "type": "CompoundType", + "tags": [], + "label": "mode", + "description": [], + "signature": [ + "\"+\" | \"-\"" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/types.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onResize", + "type": "Function", + "tags": [], + "label": "onResize", + "description": [ + "\nFunction triggered when a column is resized by the user" + ], + "signature": [ + "((colSettings: { columnId: string; width: number; }) => void) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onResize.$1", + "type": "Object", + "tags": [], + "label": "colSettings", + "description": [], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onResize.$1.columnId", + "type": "string", + "tags": [], + "label": "columnId", + "description": [], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onResize.$1.width", + "type": "number", + "tags": [], + "label": "width", + "description": [], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onSetColumns", + "type": "Function", + "tags": [], + "label": "onSetColumns", + "description": [ + "\nFunction to set all columns" + ], + "signature": [ + "(columns: string[], hideTimeColumn: boolean) => void" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onSetColumns.$1", + "type": "Array", + "tags": [], + "label": "columns", + "description": [], + "signature": [ + "string[]" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onSetColumns.$2", + "type": "boolean", + "tags": [], + "label": "hideTimeColumn", + "description": [], + "signature": [ + "boolean" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onSort", + "type": "Function", + "tags": [], + "label": "onSort", + "description": [ + "\nfunction to change sorting of the documents, skipped when isSortEnabled is set to false" + ], + "signature": [ + "((sort: string[][]) => void) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onSort.$1", + "type": "Array", + "tags": [], + "label": "sort", + "description": [], + "signature": [ + "string[][]" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.rows", + "type": "Array", + "tags": [], + "label": "rows", + "description": [ + "\nArray of documents provided by Elasticsearch" + ], + "signature": [ + "DataTableRecord", + "[] | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.sampleSize", + "type": "number", + "tags": [], + "label": "sampleSize", + "description": [ + "\nThe max size of the documents returned by Elasticsearch" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.setExpandedDoc", + "type": "Function", + "tags": [], + "label": "setExpandedDoc", + "description": [ + "\nFunction to set the expanded document, which is displayed in a flyout" + ], + "signature": [ + "((doc?: ", + "DataTableRecord", + " | undefined) => void) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.setExpandedDoc.$1", + "type": "Object", + "tags": [], + "label": "doc", + "description": [], + "signature": [ + "DataTableRecord", + " | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.settings", + "type": "Object", + "tags": [], + "label": "settings", + "description": [ + "\nGrid display settings persisted in Elasticsearch (e.g. column width)" + ], + "signature": [ + { + "pluginId": "@kbn/unified-data-table", + "scope": "common", + "docId": "kibKbnUnifiedDataTablePluginApi", + "section": "def-common.UnifiedDataTableSettings", + "text": "UnifiedDataTableSettings" + }, + " | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.searchDescription", + "type": "string", + "tags": [], + "label": "searchDescription", + "description": [ + "\nSearch description" + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.searchTitle", + "type": "string", + "tags": [], + "label": "searchTitle", + "description": [ + "\nSearch title" + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.showTimeCol", + "type": "boolean", + "tags": [], + "label": "showTimeCol", + "description": [ + "\nDetermines whether the time columns should be displayed (legacy settings)" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.showFullScreenButton", + "type": "CompoundType", + "tags": [], + "label": "showFullScreenButton", + "description": [ + "\nDetermines whether the full screen button should be displayed" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.isSortEnabled", + "type": "CompoundType", + "tags": [], + "label": "isSortEnabled", + "description": [ + "\nManage user sorting control" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.sort", + "type": "Array", + "tags": [], + "label": "sort", + "description": [ + "\nCurrent sort setting" + ], + "signature": [ + "SortOrder", + "[]" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.useNewFieldsApi", + "type": "boolean", + "tags": [], + "label": "useNewFieldsApi", + "description": [ + "\nHow the data is fetched" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.isPaginationEnabled", + "type": "CompoundType", + "tags": [], + "label": "isPaginationEnabled", + "description": [ + "\nManage pagination control" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.controlColumnIds", + "type": "Array", + "tags": [], + "label": "controlColumnIds", + "description": [ + "\nList of used control columns (available: 'openDetails', 'select')" + ], + "signature": [ + "string[] | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.rowHeightState", + "type": "number", + "tags": [], + "label": "rowHeightState", + "description": [ + "\nRow height from state" + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onUpdateRowHeight", + "type": "Function", + "tags": [], + "label": "onUpdateRowHeight", + "description": [ + "\nUpdate row height state" + ], + "signature": [ + "((rowHeight: number) => void) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onUpdateRowHeight.$1", + "type": "number", + "tags": [], + "label": "rowHeight", + "description": [], + "signature": [ + "number" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.isPlainRecord", + "type": "CompoundType", + "tags": [], + "label": "isPlainRecord", + "description": [ + "\nIs text base lang mode enabled" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.rowsPerPageState", + "type": "number", + "tags": [], + "label": "rowsPerPageState", + "description": [ + "\nCurrent state value for rowsPerPage" + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onUpdateRowsPerPage", + "type": "Function", + "tags": [], + "label": "onUpdateRowsPerPage", + "description": [ + "\nUpdate rows per page state" + ], + "signature": [ + "((rowsPerPage: number) => void) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onUpdateRowsPerPage.$1", + "type": "number", + "tags": [], + "label": "rowsPerPage", + "description": [], + "signature": [ + "number" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onFieldEdited", + "type": "Function", + "tags": [], + "label": "onFieldEdited", + "description": [ + "\nCallback to execute on edit runtime field" + ], + "signature": [ + "(() => void) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.cellActionsTriggerId", + "type": "string", + "tags": [], + "label": "cellActionsTriggerId", + "description": [ + "\nOptional triggerId to retrieve the column cell actions that will override the default ones" + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.services", + "type": "Object", + "tags": [], + "label": "services", + "description": [ + "\nService dependencies" + ], + "signature": [ + "{ theme: ", + { + "pluginId": "@kbn/react-kibana-context-common", + "scope": "common", + "docId": "kibKbnReactKibanaContextCommonPluginApi", + "section": "def-common.ThemeServiceStart", + "text": "ThemeServiceStart" + }, + "; fieldFormats: ", + { + "pluginId": "fieldFormats", + "scope": "public", + "docId": "kibFieldFormatsPluginApi", + "section": "def-public.FieldFormatsStart", + "text": "FieldFormatsStart" + }, + "; uiSettings: ", + { + "pluginId": "@kbn/core-ui-settings-browser", + "scope": "common", + "docId": "kibKbnCoreUiSettingsBrowserPluginApi", + "section": "def-common.IUiSettingsClient", + "text": "IUiSettingsClient" + }, + "; dataViewFieldEditor: ", + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.PluginStart", + "text": "PluginStart" + }, + "; toastNotifications: ", + { + "pluginId": "@kbn/core-notifications-browser", + "scope": "common", + "docId": "kibKbnCoreNotificationsBrowserPluginApi", + "section": "def-common.IToasts", + "text": "IToasts" + }, + "; storage: ", + { + "pluginId": "kibanaUtils", + "scope": "public", + "docId": "kibKibanaUtilsPluginApi", + "section": "def-public.Storage", + "text": "Storage" + }, + "; data: ", + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginStart", + "text": "DataPublicPluginStart" + }, + "; }" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.renderDocumentView", + "type": "Function", + "tags": [], + "label": "renderDocumentView", + "description": [ + "\nCallback to render DocumentView when the document is expanded" + ], + "signature": [ + "((hit: ", + "DataTableRecord", + ", displayedRows: ", + "DataTableRecord", + "[], displayedColumns: string[]) => JSX.Element | undefined) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.renderDocumentView.$1", + "type": "Object", + "tags": [], + "label": "hit", + "description": [], + "signature": [ + "DataTableRecord" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.renderDocumentView.$2", + "type": "Array", + "tags": [], + "label": "displayedRows", + "description": [], + "signature": [ + "DataTableRecord", + "[]" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.renderDocumentView.$3", + "type": "Array", + "tags": [], + "label": "displayedColumns", + "description": [], + "signature": [ + "string[]" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.configRowHeight", + "type": "number", + "tags": [], + "label": "configRowHeight", + "description": [ + "\nOptional value for providing configuration setting for UnifiedDataTable rows height" + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.showMultiFields", + "type": "CompoundType", + "tags": [], + "label": "showMultiFields", + "description": [ + "\nOptional value for providing configuration setting for enabling to display the complex fields in the table. Default is true." + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.maxDocFieldsDisplayed", + "type": "number", + "tags": [], + "label": "maxDocFieldsDisplayed", + "description": [ + "\nOptional value for providing configuration setting for maximum number of document fields to display in the table. Default is 50." + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.externalControlColumns", + "type": "Array", + "tags": [], + "label": "externalControlColumns", + "description": [ + "\nOptional value for providing EuiDataGridControlColumn list of the additional leading control columns. UnifiedDataTable includes two control columns: Open Details and Select." + ], + "signature": [ + "EuiDataGridControlColumn", + "[] | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.totalHits", + "type": "number", + "tags": [], + "label": "totalHits", + "description": [ + "\nNumber total hits from ES" + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.onFetchMoreRecords", + "type": "Function", + "tags": [], + "label": "onFetchMoreRecords", + "description": [ + "\nTo fetch more" + ], + "signature": [ + "(() => void) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.externalAdditionalControls", + "type": "CompoundType", + "tags": [], + "label": "externalAdditionalControls", + "description": [ + "\nOptional value for providing the additional controls available in the UnifiedDataTable toolbar to manage it's records or state. UnifiedDataTable includes Columns, Sorting and Bulk Actions." + ], + "signature": [ + "boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.rowsPerPageOptions", + "type": "Array", + "tags": [], + "label": "rowsPerPageOptions", + "description": [ + "\nOptional list of number type values to set custom UnifiedDataTable paging options to display the records per page." + ], + "signature": [ + "number[] | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.renderCustomGridBody", + "type": "Function", + "tags": [], + "label": "renderCustomGridBody", + "description": [ + "\nAn optional function called to completely customize and control the rendering of\nEuiDataGrid's body and cell placement. This can be used to, e.g. remove EuiDataGrid's\nvirtualization library, or roll your own.\n\nThis component is **only** meant as an escape hatch for extremely custom use cases.\n\nBehind the scenes, this function is treated as a React component,\nallowing hooks, context, and other React concepts to be used.\nIt receives #EuiDataGridCustomBodyProps as its only argument." + ], + "signature": [ + "((args: ", + "EuiDataGridCustomBodyProps", + ") => React.ReactNode) | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.renderCustomGridBody.$1", + "type": "Object", + "tags": [], + "label": "args", + "description": [], + "signature": [ + "EuiDataGridCustomBodyProps" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.trailingControlColumns", + "type": "Array", + "tags": [], + "label": "trailingControlColumns", + "description": [ + "\nAn optional list of the EuiDataGridControlColumn type for setting trailing control columns standard for EuiDataGrid." + ], + "signature": [ + "EuiDataGridControlColumn", + "[] | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.visibleCellActions", + "type": "number", + "tags": [], + "label": "visibleCellActions", + "description": [ + "\nAn optional value for a custom number of the visible cell actions in the table. By default is up to 3." + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.externalCustomRenderers", + "type": "Object", + "tags": [], + "label": "externalCustomRenderers", + "description": [ + "\nAn optional settings for a specified fields rendering like links. Applied only for the listed fields rendering." + ], + "signature": [ + "Record React.ReactNode> | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.consumer", + "type": "string", + "tags": [], + "label": "consumer", + "description": [ + "\nName of the UnifiedDataTable consumer component or application" + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.componentsTourSteps", + "type": "Object", + "tags": [], + "label": "componentsTourSteps", + "description": [ + "\nOptional key/value pairs to set guided onboarding steps ids for a data table components included to guided tour." + ], + "signature": [ + "Record | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableSettings", + "type": "Interface", + "tags": [], + "label": "UnifiedDataTableSettings", + "description": [ + "\nUser configurable state of data grid, persisted in saved search" + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableSettings.columns", + "type": "Object", + "tags": [], + "label": "columns", + "description": [], + "signature": [ + "Record | undefined" + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableSettingsColumn", + "type": "Interface", + "tags": [], + "label": "UnifiedDataTableSettingsColumn", + "description": [], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableSettingsColumn.width", + "type": "number", + "tags": [], + "label": "width", + "description": [], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.DataLoadingState", + "type": "Enum", + "tags": [], + "label": "DataLoadingState", + "description": [], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "misc": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ValueToStringConverter", + "type": "Type", + "tags": [], + "label": "ValueToStringConverter", + "description": [], + "signature": [ + "(rowIndex: number, columnId: string, options?: { compatibleWithCSV?: boolean | undefined; } | undefined) => { formattedString: string; withFormula: boolean; }" + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ValueToStringConverter.$1", + "type": "number", + "tags": [], + "label": "rowIndex", + "description": [], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ValueToStringConverter.$2", + "type": "string", + "tags": [], + "label": "columnId", + "description": [], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ValueToStringConverter.$3", + "type": "Object", + "tags": [], + "label": "options", + "description": [], + "signature": [ + "{ compatibleWithCSV?: boolean | undefined; } | undefined" + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_unified_data_table.mdx b/api_docs/kbn_unified_data_table.mdx new file mode 100644 index 0000000000000..91091dbb7a330 --- /dev/null +++ b/api_docs/kbn_unified_data_table.mdx @@ -0,0 +1,39 @@ +--- +#### +#### 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: kibKbnUnifiedDataTablePluginApi +slug: /kibana-dev-docs/api/kbn-unified-data-table +title: "@kbn/unified-data-table" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/unified-data-table plugin +date: 2023-09-05 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-data-table'] +--- +import kbnUnifiedDataTableObj from './kbn_unified_data_table.devdocs.json'; + +Contains functionality for the unified data table which can be integrated into apps + +Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 88 | 0 | 39 | 1 | + +## Common + +### Functions + + +### Interfaces + + +### Enums + + +### Consts, variables and types + + diff --git a/api_docs/kbn_unified_doc_viewer.devdocs.json b/api_docs/kbn_unified_doc_viewer.devdocs.json new file mode 100644 index 0000000000000..da1c2b438bd70 --- /dev/null +++ b/api_docs/kbn_unified_doc_viewer.devdocs.json @@ -0,0 +1,204 @@ +{ + "id": "@kbn/unified-doc-viewer", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [ + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.DocViewsRegistry", + "type": "Class", + "tags": [], + "label": "DocViewsRegistry", + "description": [], + "path": "packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.DocViewsRegistry.addDocView", + "type": "Function", + "tags": [], + "label": "addDocView", + "description": [ + "\nExtends and adds the given doc view to the registry array" + ], + "signature": [ + "(docViewRaw: ", + "DocViewInput", + " | ", + "DocViewInputFn", + ") => void" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.DocViewsRegistry.addDocView.$1", + "type": "CompoundType", + "tags": [], + "label": "docViewRaw", + "description": [], + "signature": [ + "DocViewInput", + " | ", + "DocViewInputFn" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.DocViewsRegistry.getDocViewsSorted", + "type": "Function", + "tags": [], + "label": "getDocViewsSorted", + "description": [ + "\nReturns a sorted array of doc_views for rendering tabs" + ], + "signature": [ + "(hit: ", + "DataTableRecord", + ") => ", + "DocView", + "[]" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.DocViewsRegistry.getDocViewsSorted.$1", + "type": "Object", + "tags": [], + "label": "hit", + "description": [], + "signature": [ + "DataTableRecord" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [ + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.DocViewer", + "type": "Function", + "tags": [], + "label": "DocViewer", + "description": [ + "\nRendering tabs with different views of 1 Elasticsearch hit in Discover.\nThe tabs are provided by the `docs_views` registry.\nA view can contain a React `component`, or any JS framework by using\na `render` function." + ], + "signature": [ + "({ docViews, ...renderProps }: ", + "DocViewerProps", + ") => JSX.Element | null" + ], + "path": "packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.DocViewer.$1", + "type": "Object", + "tags": [], + "label": "{ docViews, ...renderProps }", + "description": [], + "signature": [ + "DocViewerProps" + ], + "path": "packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.FieldName", + "type": "Function", + "tags": [], + "label": "FieldName", + "description": [], + "signature": [ + "({\n fieldName,\n fieldMapping,\n fieldType,\n fieldIconProps,\n scripted = false,\n highlight = '',\n}: Props) => JSX.Element" + ], + "path": "packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.FieldName.$1", + "type": "Object", + "tags": [], + "label": "{\n fieldName,\n fieldMapping,\n fieldType,\n fieldIconProps,\n scripted = false,\n highlight = '',\n}", + "description": [], + "signature": [ + "Props" + ], + "path": "packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [ + { + "parentPluginId": "@kbn/unified-doc-viewer", + "id": "def-common.ElasticRequestState", + "type": "Enum", + "tags": [], + "label": "ElasticRequestState", + "description": [], + "path": "packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_unified_doc_viewer.mdx b/api_docs/kbn_unified_doc_viewer.mdx new file mode 100644 index 0000000000000..40c628ad3325c --- /dev/null +++ b/api_docs/kbn_unified_doc_viewer.mdx @@ -0,0 +1,36 @@ +--- +#### +#### 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: kibKbnUnifiedDocViewerPluginApi +slug: /kibana-dev-docs/api/kbn-unified-doc-viewer +title: "@kbn/unified-doc-viewer" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/unified-doc-viewer plugin +date: 2023-09-05 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-doc-viewer'] +--- +import kbnUnifiedDocViewerObj from './kbn_unified_doc_viewer.devdocs.json'; + + + +Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 10 | 0 | 7 | 6 | + +## Common + +### Functions + + +### Classes + + +### Enums + + diff --git a/api_docs/kbn_unified_field_list.devdocs.json b/api_docs/kbn_unified_field_list.devdocs.json index 88f3b347e3f0b..dddaeb04aa725 100644 --- a/api_docs/kbn_unified_field_list.devdocs.json +++ b/api_docs/kbn_unified_field_list.devdocs.json @@ -796,41 +796,6 @@ "returnComment": [], "initialIsOpen": false }, - { - "parentPluginId": "@kbn/unified-field-list", - "id": "def-common.getFieldTypeName", - "type": "Function", - "tags": [], - "label": "getFieldTypeName", - "description": [ - "\nReturns a user-friendly name of a field type" - ], - "signature": [ - "(type: string | undefined) => string" - ], - "path": "packages/kbn-unified-field-list/src/utils/field_types/get_field_type_name.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/unified-field-list", - "id": "def-common.getFieldTypeName.$1", - "type": "string", - "tags": [], - "label": "type", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "packages/kbn-unified-field-list/src/utils/field_types/get_field_type_name.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "@kbn/unified-field-list", "id": "def-common.getSearchMode", @@ -5518,20 +5483,6 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/unified-field-list", - "id": "def-common.KNOWN_FIELD_TYPES", - "type": "Enum", - "tags": [], - "label": "KNOWN_FIELD_TYPES", - "description": [ - "\nField types for which name and description are defined" - ], - "path": "packages/kbn-unified-field-list/src/utils/field_types/field_types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false } ], "misc": [ @@ -5691,21 +5642,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "@kbn/unified-field-list", - "id": "def-common.FieldTypeKnown", - "type": "Type", - "tags": [], - "label": "FieldTypeKnown", - "description": [], - "signature": [ - "string" - ], - "path": "packages/kbn-unified-field-list/src/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "@kbn/unified-field-list", "id": "def-common.GetCustomFieldType", diff --git a/api_docs/kbn_unified_field_list.mdx b/api_docs/kbn_unified_field_list.mdx index 6a265e7eadae2..f22127f77ae44 100644 --- a/api_docs/kbn_unified_field_list.mdx +++ b/api_docs/kbn_unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-field-list title: "@kbn/unified-field-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unified-field-list plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-field-list'] --- import kbnUnifiedFieldListObj from './kbn_unified_field_list.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 306 | 0 | 277 | 9 | +| 302 | 0 | 275 | 9 | ## Common diff --git a/api_docs/kbn_url_state.mdx b/api_docs/kbn_url_state.mdx index 8ff7a1031b7a8..1e294e99efaba 100644 --- a/api_docs/kbn_url_state.mdx +++ b/api_docs/kbn_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-url-state title: "@kbn/url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/url-state plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/url-state'] --- import kbnUrlStateObj from './kbn_url_state.devdocs.json'; diff --git a/api_docs/kbn_use_tracked_promise.mdx b/api_docs/kbn_use_tracked_promise.mdx index 0af4f5d49265e..b8c701132cb88 100644 --- a/api_docs/kbn_use_tracked_promise.mdx +++ b/api_docs/kbn_use_tracked_promise.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-use-tracked-promise title: "@kbn/use-tracked-promise" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/use-tracked-promise plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/use-tracked-promise'] --- import kbnUseTrackedPromiseObj from './kbn_use_tracked_promise.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index 9fed54ca5d325..ed3eb341e30b8 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.devdocs.json b/api_docs/kbn_utility_types.devdocs.json index 821e2d7a40523..6d177a2aa2cf2 100644 --- a/api_docs/kbn_utility_types.devdocs.json +++ b/api_docs/kbn_utility_types.devdocs.json @@ -627,6 +627,61 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/utility-types", + "id": "def-common.RecursivePartial", + "type": "Type", + "tags": [], + "label": "RecursivePartial", + "description": [], + "signature": [ + "{ [P in keyof T]?: (T[P] extends NonAny[] ? T[P] : T[P] extends readonly NonAny[] ? T[P] : T[P] extends (infer U)[] ? ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "[] : T[P] extends readonly (infer U)[] ? readonly ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "[] : T[P] extends Set ? Set<", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + "> : T[P] extends Map ? Map> : T[P] extends NonAny ? T[P] : ", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.RecursivePartial", + "text": "RecursivePartial" + }, + ") | undefined; }" + ], + "path": "packages/kbn-utility-types/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/utility-types", "id": "def-common.RecursiveReadonly", diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index c0e666a3d573c..fabeb02cc299a 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 36 | 0 | 15 | 1 | +| 37 | 0 | 16 | 1 | ## Common diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index 430e9f209ba18..9e9cb5759dee2 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index 16987b55706d4..7b5a3ae2edbf2 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_visualization_ui_components.mdx b/api_docs/kbn_visualization_ui_components.mdx index 5d5a3f2a58c0e..0dc5954a37433 100644 --- a/api_docs/kbn_visualization_ui_components.mdx +++ b/api_docs/kbn_visualization_ui_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-visualization-ui-components title: "@kbn/visualization-ui-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/visualization-ui-components plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/visualization-ui-components'] --- import kbnVisualizationUiComponentsObj from './kbn_visualization_ui_components.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index ddee9ca854aa9..aa2fb5154468f 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index 4bd66f17c803d..9fa0c25531795 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.devdocs.json b/api_docs/kibana_react.devdocs.json index 04518800ede3e..3133e29cbacf4 100644 --- a/api_docs/kibana_react.devdocs.json +++ b/api_docs/kibana_react.devdocs.json @@ -908,6 +908,22 @@ "plugin": "savedObjects", "path": "src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx" }, + { + "plugin": "indexManagement", + "path": "x-pack/plugins/index_management/public/shared_imports.ts" + }, + { + "plugin": "indexManagement", + "path": "x-pack/plugins/index_management/public/application/index.tsx" + }, + { + "plugin": "indexManagement", + "path": "x-pack/plugins/index_management/public/application/index.tsx" + }, + { + "plugin": "indexManagement", + "path": "x-pack/plugins/index_management/public/application/index.tsx" + }, { "plugin": "serverless", "path": "x-pack/plugins/serverless/public/plugin.tsx" @@ -1576,22 +1592,6 @@ "plugin": "expressionShape", "path": "src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx" }, - { - "plugin": "indexManagement", - "path": "x-pack/plugins/index_management/public/shared_imports.ts" - }, - { - "plugin": "indexManagement", - "path": "x-pack/plugins/index_management/public/application/index.tsx" - }, - { - "plugin": "indexManagement", - "path": "x-pack/plugins/index_management/public/application/index.tsx" - }, - { - "plugin": "indexManagement", - "path": "x-pack/plugins/index_management/public/application/index.tsx" - }, { "plugin": "crossClusterReplication", "path": "x-pack/plugins/cross_cluster_replication/public/shared_imports.ts" @@ -3166,6 +3166,26 @@ "plugin": "savedObjects", "path": "src/plugins/saved_objects/public/saved_object/helpers/confirm_modal_promise.tsx" }, + { + "plugin": "runtimeFields", + "path": "x-pack/plugins/runtime_fields/public/shared_imports.ts" + }, + { + "plugin": "runtimeFields", + "path": "x-pack/plugins/runtime_fields/public/load_editor.tsx" + }, + { + "plugin": "runtimeFields", + "path": "x-pack/plugins/runtime_fields/public/load_editor.tsx" + }, + { + "plugin": "indexManagement", + "path": "x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx" + }, + { + "plugin": "indexManagement", + "path": "x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx" + }, { "plugin": "dataViewEditor", "path": "src/plugins/data_view_editor/public/shared_imports.ts" @@ -3326,14 +3346,6 @@ "plugin": "savedObjectsTagging", "path": "x-pack/plugins/saved_objects_tagging/public/components/assign_flyout/open_assign_flyout.tsx" }, - { - "plugin": "eventAnnotation", - "path": "src/plugins/event_annotation/public/get_table_list.tsx" - }, - { - "plugin": "eventAnnotation", - "path": "src/plugins/event_annotation/public/get_table_list.tsx" - }, { "plugin": "dataViewFieldEditor", "path": "src/plugins/data_view_field_editor/public/shared_imports.ts" @@ -3346,6 +3358,14 @@ "plugin": "dataViewFieldEditor", "path": "src/plugins/data_view_field_editor/public/open_delete_modal.tsx" }, + { + "plugin": "eventAnnotation", + "path": "src/plugins/event_annotation/public/get_table_list.tsx" + }, + { + "plugin": "eventAnnotation", + "path": "src/plugins/event_annotation/public/get_table_list.tsx" + }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/persistence/saved_objects_utils/confirm_modal_promise.tsx" @@ -3674,26 +3694,6 @@ "plugin": "cloudSecurityPosture", "path": "x-pack/plugins/cloud_security_posture/public/components/take_action.tsx" }, - { - "plugin": "runtimeFields", - "path": "x-pack/plugins/runtime_fields/public/shared_imports.ts" - }, - { - "plugin": "runtimeFields", - "path": "x-pack/plugins/runtime_fields/public/load_editor.tsx" - }, - { - "plugin": "runtimeFields", - "path": "x-pack/plugins/runtime_fields/public/load_editor.tsx" - }, - { - "plugin": "indexManagement", - "path": "x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx" - }, - { - "plugin": "indexManagement", - "path": "x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx" - }, { "plugin": "dashboardEnhanced", "path": "x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx" @@ -3906,122 +3906,6 @@ "plugin": "synthetics", "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/manual_test_run_mode/simple_test_results.tsx" }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/components/toast_notification_text.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/components/toast_notification_text.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_schedule_now_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_schedule_now_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_reauthorize_transform.tsx" - }, - { - "plugin": "transform", - "path": "x-pack/plugins/transform/public/app/hooks/use_reauthorize_transform.tsx" - }, { "plugin": "uptime", "path": "x-pack/plugins/uptime/public/legacy_uptime/lib/alert_types/alert_messages.tsx" diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index 2ee81bd38b33e..72e61b9db565c 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 2a09443de9b3c..91d17d9fcde72 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index e1da6b763fa0d..eeb74883b2962 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 595a33dd849bf..7640d66033c05 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index d39ec76e6e0b6..51aee902356a6 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index 181c51f6a7f60..6d5783871cdc4 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 2e7ed075cd6c8..8514ee21fcd37 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.devdocs.json b/api_docs/lists.devdocs.json index ef489757d203b..1dce5bcba81d2 100644 --- a/api_docs/lists.devdocs.json +++ b/api_docs/lists.devdocs.json @@ -2594,6 +2594,24 @@ "The contents of the bootstrap response from Elasticsearch" ] }, + { + "parentPluginId": "lists", + "id": "def-server.ListClient.deleteLegacyListTemplateIfExists", + "type": "Function", + "tags": [], + "label": "deleteLegacyListTemplateIfExists", + "description": [ + "\nChecks if legacy lists template exists and delete it" + ], + "signature": [ + "() => Promise" + ], + "path": "x-pack/plugins/lists/server/services/lists/list_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "lists", "id": "def-server.ListClient.deleteLegacyListItemTemplate", @@ -2614,6 +2632,24 @@ "The contents of the bootstrap response from Elasticsearch" ] }, + { + "parentPluginId": "lists", + "id": "def-server.ListClient.deleteLegacyListItemTemplateIfExists", + "type": "Function", + "tags": [], + "label": "deleteLegacyListItemTemplateIfExists", + "description": [ + "\nChecks if legacy list item template exists and delete it" + ], + "signature": [ + "() => Promise" + ], + "path": "x-pack/plugins/lists/server/services/lists/list_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "lists", "id": "def-server.ListClient.deleteListItem", diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index ff691940dc751..7d88a913416d4 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-detection-engine](https://github.com/orgs/elastic/tea | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 222 | 0 | 96 | 51 | +| 224 | 0 | 96 | 51 | ## Client diff --git a/api_docs/log_explorer.devdocs.json b/api_docs/log_explorer.devdocs.json new file mode 100644 index 0000000000000..742f9c0f72bfa --- /dev/null +++ b/api_docs/log_explorer.devdocs.json @@ -0,0 +1,76 @@ +{ + "id": "logExplorer", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "logExplorer", + "id": "def-public.LogExplorerPluginSetup", + "type": "Type", + "tags": [], + "label": "LogExplorerPluginSetup", + "description": [], + "signature": [ + "void" + ], + "path": "x-pack/plugins/log_explorer/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "logExplorer", + "id": "def-public.LogExplorerPluginStart", + "type": "Interface", + "tags": [], + "label": "LogExplorerPluginStart", + "description": [], + "path": "x-pack/plugins/log_explorer/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "logExplorer", + "id": "def-public.LogExplorerPluginStart.LogExplorer", + "type": "CompoundType", + "tags": [], + "label": "LogExplorer", + "description": [], + "signature": [ + "React.ComponentClass<", + "LogExplorerProps", + ", any> | React.FunctionComponent<", + "LogExplorerProps", + ">" + ], + "path": "x-pack/plugins/log_explorer/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/log_explorer.mdx b/api_docs/log_explorer.mdx new file mode 100644 index 0000000000000..88da234919d93 --- /dev/null +++ b/api_docs/log_explorer.mdx @@ -0,0 +1,33 @@ +--- +#### +#### 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: kibLogExplorerPluginApi +slug: /kibana-dev-docs/api/logExplorer +title: "logExplorer" +image: https://source.unsplash.com/400x175/?github +description: API docs for the logExplorer plugin +date: 2023-09-05 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logExplorer'] +--- +import logExplorerObj from './log_explorer.devdocs.json'; + +This plugin provides a LogExplorer component using the Discover customization framework, offering several affordances specifically designed for log consumption. + +Contact [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3 | 0 | 3 | 1 | + +## Client + +### Setup + + +### Start + + diff --git a/api_docs/logs_shared.mdx b/api_docs/logs_shared.mdx index 8ec3e570acee0..6a569ad488335 100644 --- a/api_docs/logs_shared.mdx +++ b/api_docs/logs_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsShared title: "logsShared" image: https://source.unsplash.com/400x175/?github description: API docs for the logsShared plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logsShared'] --- import logsSharedObj from './logs_shared.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 851e89f0450f6..506e3bdaa1684 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index c7dd1d3279bc4..415d96478e799 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index 04030f115b578..a76d920efbd05 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index a389fc5a3d29e..87679e97ee10f 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index e955022d97f6f..2f9c10a8d601f 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.devdocs.json b/api_docs/monitoring_collection.devdocs.json index 79950746cd8e0..76ebc220d46f2 100644 --- a/api_docs/monitoring_collection.devdocs.json +++ b/api_docs/monitoring_collection.devdocs.json @@ -54,9 +54,9 @@ "signature": [ "{ [Key in keyof Required]: Required[Key] extends (infer U)[] ? { type: \"array\"; items: ", "RecursiveMakeSchemaFrom", - "; } : ", + "; } : ", "RecursiveMakeSchemaFrom", - "[Key]>; }" + "[Key], false>; }" ], "path": "x-pack/plugins/monitoring_collection/server/plugin.ts", "deprecated": false, diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index d679b2cd903c7..1f3f4c9e84b3c 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index eea6f16c1b1f2..58358d769e1c8 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index 6b5368717a70c..5c0fda7795ffc 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/no_data_page.mdx b/api_docs/no_data_page.mdx index 6014be6b3ae9f..4b84d2688576b 100644 --- a/api_docs/no_data_page.mdx +++ b/api_docs/no_data_page.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/noDataPage title: "noDataPage" image: https://source.unsplash.com/400x175/?github description: API docs for the noDataPage plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'noDataPage'] --- import noDataPageObj from './no_data_page.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index 864910d8ef2c5..844c63784a8e7 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.devdocs.json b/api_docs/observability.devdocs.json index e910e74323d61..24d66a3f94a94 100644 --- a/api_docs/observability.devdocs.json +++ b/api_docs/observability.devdocs.json @@ -2652,6 +2652,26 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "observability", + "id": "def-public.ObservabilityPublicPluginsStart.contentManagement", + "type": "Object", + "tags": [], + "label": "contentManagement", + "description": [], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "public", + "docId": "kibContentManagementPluginApi", + "section": "def-public.ContentManagementPublicStart", + "text": "ContentManagementPublicStart" + } + ], + "path": "x-pack/plugins/observability/public/plugin.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "observability", "id": "def-public.ObservabilityPublicPluginsStart.data", diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index e08e73da6a8f1..4f5365797c3b2 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/actionable-observability](https://github.com/orgs/elastic/team | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 541 | 2 | 532 | 16 | +| 542 | 2 | 533 | 16 | ## Client diff --git a/api_docs/observability_a_i_assistant.devdocs.json b/api_docs/observability_a_i_assistant.devdocs.json index f1202bffd58d1..73a81e8571d20 100644 --- a/api_docs/observability_a_i_assistant.devdocs.json +++ b/api_docs/observability_a_i_assistant.devdocs.json @@ -385,9 +385,11 @@ "PartialC", "<{ filter: ", "StringC", - "; }>]>; }> | undefined; handler: ({}: ", + "; includeRecovered: ", + "Type", + "; }>]>; }> | undefined; handler: ({}: ", "ObservabilityAIAssistantRouteHandlerResources", - " & { params: { body: { featureIds: string[]; start: string; end: string; } & { filter?: string | undefined; }; }; }) => Promise<{ content: { total: number; alerts: OutputOf>[]; }; }>; } & ", + " & { params: { body: { featureIds: string[]; start: string; end: string; } & { filter?: string | undefined; includeRecovered?: boolean | undefined; }; }; }) => Promise<{ content: { total: number; alerts: OutputOf>[]; }; }>; } & ", "ObservabilityAIAssistantRouteCreateOptions", "; \"GET /internal/observability_ai_assistant/functions/kb_status\": { endpoint: \"GET /internal/observability_ai_assistant/functions/kb_status\"; params?: undefined; handler: ({}: ", "ObservabilityAIAssistantRouteHandlerResources", @@ -397,7 +399,7 @@ "ObservabilityAIAssistantRouteHandlerResources", ") => Promise<{}>; } & ", "ObservabilityAIAssistantRouteCreateOptions", - "; \"POST /internal/observability_ai_assistant/functions/summarise\": { endpoint: \"POST /internal/observability_ai_assistant/functions/summarise\"; params?: ", + "; \"POST /internal/observability_ai_assistant/functions/summarize\": { endpoint: \"POST /internal/observability_ai_assistant/functions/summarize\"; params?: ", "TypeC", "<{ body: ", "TypeC", @@ -748,9 +750,11 @@ "PartialC", "<{ filter: ", "StringC", - "; }>]>; }> | undefined; handler: ({}: ", + "; includeRecovered: ", + "Type", + "; }>]>; }> | undefined; handler: ({}: ", "ObservabilityAIAssistantRouteHandlerResources", - " & { params: { body: { featureIds: string[]; start: string; end: string; } & { filter?: string | undefined; }; }; }) => Promise<{ content: { total: number; alerts: OutputOf>[]; }; }>; } & ", + " & { params: { body: { featureIds: string[]; start: string; end: string; } & { filter?: string | undefined; includeRecovered?: boolean | undefined; }; }; }) => Promise<{ content: { total: number; alerts: OutputOf>[]; }; }>; } & ", "ObservabilityAIAssistantRouteCreateOptions", "; \"GET /internal/observability_ai_assistant/functions/kb_status\": { endpoint: \"GET /internal/observability_ai_assistant/functions/kb_status\"; params?: undefined; handler: ({}: ", "ObservabilityAIAssistantRouteHandlerResources", @@ -760,7 +764,7 @@ "ObservabilityAIAssistantRouteHandlerResources", ") => Promise<{}>; } & ", "ObservabilityAIAssistantRouteCreateOptions", - "; \"POST /internal/observability_ai_assistant/functions/summarise\": { endpoint: \"POST /internal/observability_ai_assistant/functions/summarise\"; params?: ", + "; \"POST /internal/observability_ai_assistant/functions/summarize\": { endpoint: \"POST /internal/observability_ai_assistant/functions/summarize\"; params?: ", "TypeC", "<{ body: ", "TypeC", @@ -1101,7 +1105,7 @@ "label": "ObservabilityAIAssistantAPIEndpoint", "description": [], "signature": [ - "\"POST /internal/observability_ai_assistant/chat\" | \"GET /internal/observability_ai_assistant/conversation/{conversationId}\" | \"POST /internal/observability_ai_assistant/conversations\" | \"POST /internal/observability_ai_assistant/conversation\" | \"PUT /internal/observability_ai_assistant/conversation/{conversationId}\" | \"PUT /internal/observability_ai_assistant/conversation/{conversationId}/auto_title\" | \"PUT /internal/observability_ai_assistant/conversation/{conversationId}/title\" | \"DELETE /internal/observability_ai_assistant/conversation/{conversationId}\" | \"GET /internal/observability_ai_assistant/connectors\" | \"POST /internal/observability_ai_assistant/functions/elasticsearch\" | \"POST /internal/observability_ai_assistant/functions/recall\" | \"POST /internal/observability_ai_assistant/functions/summarise\" | \"POST /internal/observability_ai_assistant/functions/setup_kb\" | \"GET /internal/observability_ai_assistant/functions/kb_status\" | \"POST /internal/observability_ai_assistant/functions/alerts\"" + "\"POST /internal/observability_ai_assistant/chat\" | \"GET /internal/observability_ai_assistant/conversation/{conversationId}\" | \"POST /internal/observability_ai_assistant/conversations\" | \"POST /internal/observability_ai_assistant/conversation\" | \"PUT /internal/observability_ai_assistant/conversation/{conversationId}\" | \"PUT /internal/observability_ai_assistant/conversation/{conversationId}/auto_title\" | \"PUT /internal/observability_ai_assistant/conversation/{conversationId}/title\" | \"DELETE /internal/observability_ai_assistant/conversation/{conversationId}\" | \"GET /internal/observability_ai_assistant/connectors\" | \"POST /internal/observability_ai_assistant/functions/elasticsearch\" | \"POST /internal/observability_ai_assistant/functions/recall\" | \"POST /internal/observability_ai_assistant/functions/summarize\" | \"POST /internal/observability_ai_assistant/functions/setup_kb\" | \"GET /internal/observability_ai_assistant/functions/kb_status\" | \"POST /internal/observability_ai_assistant/functions/alerts\"" ], "path": "x-pack/plugins/observability_ai_assistant/public/api/index.ts", "deprecated": false, @@ -1217,9 +1221,11 @@ "PartialC", "<{ filter: ", "StringC", - "; }>]>; }> | undefined; handler: ({}: ", + "; includeRecovered: ", + "Type", + "; }>]>; }> | undefined; handler: ({}: ", "ObservabilityAIAssistantRouteHandlerResources", - " & { params: { body: { featureIds: string[]; start: string; end: string; } & { filter?: string | undefined; }; }; }) => Promise<{ content: { total: number; alerts: OutputOf>[]; }; }>; } & ", + " & { params: { body: { featureIds: string[]; start: string; end: string; } & { filter?: string | undefined; includeRecovered?: boolean | undefined; }; }; }) => Promise<{ content: { total: number; alerts: OutputOf>[]; }; }>; } & ", "ObservabilityAIAssistantRouteCreateOptions", "; \"GET /internal/observability_ai_assistant/functions/kb_status\": { endpoint: \"GET /internal/observability_ai_assistant/functions/kb_status\"; params?: undefined; handler: ({}: ", "ObservabilityAIAssistantRouteHandlerResources", @@ -1229,7 +1235,7 @@ "ObservabilityAIAssistantRouteHandlerResources", ") => Promise<{}>; } & ", "ObservabilityAIAssistantRouteCreateOptions", - "; \"POST /internal/observability_ai_assistant/functions/summarise\": { endpoint: \"POST /internal/observability_ai_assistant/functions/summarise\"; params?: ", + "; \"POST /internal/observability_ai_assistant/functions/summarize\": { endpoint: \"POST /internal/observability_ai_assistant/functions/summarize\"; params?: ", "TypeC", "<{ body: ", "TypeC", diff --git a/api_docs/observability_a_i_assistant.mdx b/api_docs/observability_a_i_assistant.mdx index f2c1ad4099954..31e4264930aee 100644 --- a/api_docs/observability_a_i_assistant.mdx +++ b/api_docs/observability_a_i_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityAIAssistant title: "observabilityAIAssistant" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityAIAssistant plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAIAssistant'] --- import observabilityAIAssistantObj from './observability_a_i_assistant.devdocs.json'; diff --git a/api_docs/observability_onboarding.mdx b/api_docs/observability_onboarding.mdx index 35299f4069006..939e6d954a9b7 100644 --- a/api_docs/observability_onboarding.mdx +++ b/api_docs/observability_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityOnboarding title: "observabilityOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityOnboarding plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityOnboarding'] --- import observabilityOnboardingObj from './observability_onboarding.devdocs.json'; diff --git a/api_docs/observability_shared.devdocs.json b/api_docs/observability_shared.devdocs.json index 0a612b6ac7f72..2bfd9c96c19c3 100644 --- a/api_docs/observability_shared.devdocs.json +++ b/api_docs/observability_shared.devdocs.json @@ -682,7 +682,7 @@ "DisambiguateSet", ", Omit, \"href\">> & Omit, \"href\">) | (", "DisambiguateSet", - ", \"href\">, React.ButtonHTMLAttributes> & React.ButtonHTMLAttributes))), \"color\" | \"onClick\" | \"rel\" | \"target\"> & { size?: \"m\" | \"s\" | \"xs\" | \"l\" | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; label: React.ReactNode; isActive?: boolean | undefined; isDisabled?: boolean | undefined; href?: string | undefined; target?: string | undefined; rel?: string | undefined; iconType?: ", + ", \"href\">, React.ButtonHTMLAttributes> & React.ButtonHTMLAttributes))), \"color\" | \"onClick\" | \"rel\" | \"target\"> & { size?: \"m\" | \"s\" | \"l\" | \"xs\" | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; label: React.ReactNode; isActive?: boolean | undefined; isDisabled?: boolean | undefined; href?: string | undefined; target?: string | undefined; rel?: string | undefined; iconType?: ", "IconType", " | undefined; iconProps?: Omit<", "EuiIconProps", @@ -721,7 +721,7 @@ "DisambiguateSet", ", Omit, \"href\">> & Omit, \"href\">) | (", "DisambiguateSet", - ", \"href\">, React.ButtonHTMLAttributes> & React.ButtonHTMLAttributes))), \"color\" | \"onClick\" | \"rel\" | \"target\"> & { size?: \"m\" | \"s\" | \"xs\" | \"l\" | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; label: React.ReactNode; isActive?: boolean | undefined; isDisabled?: boolean | undefined; href?: string | undefined; target?: string | undefined; rel?: string | undefined; iconType?: ", + ", \"href\">, React.ButtonHTMLAttributes> & React.ButtonHTMLAttributes))), \"color\" | \"onClick\" | \"rel\" | \"target\"> & { size?: \"m\" | \"s\" | \"l\" | \"xs\" | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; label: React.ReactNode; isActive?: boolean | undefined; isDisabled?: boolean | undefined; href?: string | undefined; target?: string | undefined; rel?: string | undefined; iconType?: ", "IconType", " | undefined; iconProps?: Omit<", "EuiIconProps", @@ -752,7 +752,7 @@ "CommonProps", " & Omit, \"color\"> & { bordered?: boolean | undefined; flush?: boolean | undefined; gutterSize?: \"m\" | \"none\" | \"s\" | undefined; listItems?: ", "EuiListGroupItemProps", - "[] | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; size?: \"m\" | \"s\" | \"xs\" | \"l\" | undefined; maxWidth?: boolean | ", + "[] | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; size?: \"m\" | \"s\" | \"l\" | \"xs\" | undefined; maxWidth?: boolean | ", "Property", ".MaxWidth | undefined; showToolTips?: boolean | undefined; wrapText?: boolean | undefined; ariaLabelledby?: string | undefined; }) => JSX.Element" ], @@ -772,7 +772,7 @@ "CommonProps", " & Omit, \"color\"> & { bordered?: boolean | undefined; flush?: boolean | undefined; gutterSize?: \"m\" | \"none\" | \"s\" | undefined; listItems?: ", "EuiListGroupItemProps", - "[] | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; size?: \"m\" | \"s\" | \"xs\" | \"l\" | undefined; maxWidth?: boolean | ", + "[] | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; size?: \"m\" | \"s\" | \"l\" | \"xs\" | undefined; maxWidth?: boolean | ", "Property", ".MaxWidth | undefined; showToolTips?: boolean | undefined; wrapText?: boolean | undefined; ariaLabelledby?: string | undefined; }" ], @@ -2548,7 +2548,7 @@ "DisambiguateSet", ", Omit, \"href\">> & Omit, \"href\">) | (", "DisambiguateSet", - ", \"href\">, React.ButtonHTMLAttributes> & React.ButtonHTMLAttributes))), \"color\" | \"onClick\" | \"rel\" | \"target\"> & { size?: \"m\" | \"s\" | \"xs\" | \"l\" | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; label: React.ReactNode; isActive?: boolean | undefined; isDisabled?: boolean | undefined; href?: string | undefined; target?: string | undefined; rel?: string | undefined; iconType?: ", + ", \"href\">, React.ButtonHTMLAttributes> & React.ButtonHTMLAttributes))), \"color\" | \"onClick\" | \"rel\" | \"target\"> & { size?: \"m\" | \"s\" | \"l\" | \"xs\" | undefined; color?: \"text\" | \"subdued\" | \"primary\" | undefined; label: React.ReactNode; isActive?: boolean | undefined; isDisabled?: boolean | undefined; href?: string | undefined; target?: string | undefined; rel?: string | undefined; iconType?: ", "IconType", " | undefined; iconProps?: Omit<", "EuiIconProps", diff --git a/api_docs/observability_shared.mdx b/api_docs/observability_shared.mdx index bde051c6aef8c..040d247a44432 100644 --- a/api_docs/observability_shared.mdx +++ b/api_docs/observability_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityShared title: "observabilityShared" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityShared plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityShared'] --- import observabilitySharedObj from './observability_shared.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 8833f6a1ca67b..b9a92bd0e15a9 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index 9bfdfb272f12a..8c9256f9f511c 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,29 +15,29 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 668 | 558 | 39 | +| 674 | 564 | 39 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 72283 | 226 | 61740 | 1491 | +| 72503 | 223 | 61894 | 1513 | ## Plugin Directory | Plugin name           | Maintaining team | Description | API Cnt | Any Cnt | Missing
comments | Missing
exports | |--------------|----------------|-----------|--------------|----------|---------------|--------| -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 267 | 0 | 261 | 28 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 267 | 0 | 261 | 30 | | | [@elastic/appex-sharedux @elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/appex-sharedux ) | - | 17 | 1 | 15 | 2 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 61 | 1 | 3 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 786 | 1 | 755 | 46 | -| | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 29 | 0 | 29 | 118 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 790 | 1 | 759 | 49 | +| | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 29 | 0 | 29 | 119 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 9 | 0 | 9 | 0 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | Asset manager plugin for entity assets (inventory, topology, etc) | 2 | 0 | 2 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 9 | 0 | 9 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 91 | 1 | 75 | 2 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Canvas application to Kibana | 9 | 0 | 8 | 3 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | The Case management system in Kibana | 94 | 0 | 75 | 27 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | The Case management system in Kibana | 104 | 0 | 84 | 27 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 268 | 2 | 253 | 10 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 72 | 0 | 16 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | Chat available on Elastic Cloud deployments for quicker assistance. | 3 | 0 | 2 | 0 | @@ -57,16 +57,15 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 268 | 0 | 249 | 1 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 101 | 0 | 98 | 9 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | - | 54 | 0 | 51 | 0 | -| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3308 | 33 | 2581 | 26 | +| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3311 | 33 | 2584 | 26 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin provides the ability to create data views via a modal flyout inside Kibana apps | 16 | 0 | 7 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Reusable data view field editor across Kibana | 72 | 0 | 33 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Data view management app | 2 | 0 | 2 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 1034 | 0 | 254 | 2 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index. | 31 | 3 | 25 | 1 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 12 | 0 | 10 | 3 | -| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 79 | 0 | 52 | 15 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 98 | 0 | 71 | 15 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 37 | 0 | 35 | 2 | -| discoverLogExplorer | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | This plugin exposes and registers Logs+ features. | 0 | 0 | 0 | 0 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | APIs used to assess the quality of data in Elasticsearch indexes | 2 | 0 | 0 | 0 | | | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | Server APIs for the Elastic AI Assistant | 4 | 0 | 2 | 0 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds embeddables service to Kibana | 542 | 1 | 442 | 7 | @@ -90,7 +89,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'shape' function and renderer to expressions | 148 | 0 | 146 | 0 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Expression Tagcloud plugin adds a `tagcloud` renderer and function to the expression plugin. The renderer will display the `Wordcloud` chart. | 6 | 0 | 6 | 2 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Expression XY plugin adds a `xy` renderer and function to the expression plugin. The renderer will display the `xy` chart. | 175 | 0 | 165 | 13 | -| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Adds expression runtime to Kibana | 2205 | 17 | 1746 | 5 | +| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Adds expression runtime to Kibana | 2208 | 17 | 1749 | 5 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 235 | 0 | 99 | 2 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Index pattern fields and ambiguous values formatters | 292 | 5 | 253 | 3 | | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 62 | 0 | 62 | 2 | @@ -122,7 +121,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 8 | 0 | 8 | 0 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 4 | 0 | 4 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 117 | 0 | 42 | 10 | -| | [@elastic/security-detection-engine](https://github.com/orgs/elastic/teams/security-detection-engine) | - | 222 | 0 | 96 | 51 | +| | [@elastic/security-detection-engine](https://github.com/orgs/elastic/teams/security-detection-engine) | - | 224 | 0 | 96 | 51 | +| | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | This plugin provides a LogExplorer component using the Discover customization framework, offering several affordances specifically designed for log consumption. | 3 | 0 | 3 | 1 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | Exposes the shared components and APIs to access and visualize logs. | 269 | 10 | 256 | 27 | | logstash | [@elastic/logstash](https://github.com/orgs/elastic/teams/logstash) | - | 0 | 0 | 0 | 0 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 47 | 0 | 47 | 7 | @@ -135,18 +135,20 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 3 | 0 | 3 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 2 | 1 | -| | [@elastic/actionable-observability](https://github.com/orgs/elastic/teams/actionable-observability) | - | 541 | 2 | 532 | 16 | +| | [@elastic/actionable-observability](https://github.com/orgs/elastic/teams/actionable-observability) | - | 542 | 2 | 533 | 16 | | | [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai-assistant) | - | 42 | 0 | 39 | 7 | +| observabilityLogExplorer | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | This plugin exposes and registers observability log consumption features. | 0 | 0 | 0 | 0 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 14 | 0 | 14 | 0 | | | [@elastic/observability-ui](https://github.com/orgs/elastic/teams/observability-ui) | - | 277 | 1 | 276 | 11 | | | [@elastic/security-defend-workflows](https://github.com/orgs/elastic/teams/security-defend-workflows) | - | 24 | 0 | 24 | 7 | | painlessLab | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). | 218 | 2 | 164 | 11 | -| | [@elastic/profiling-ui](https://github.com/orgs/elastic/teams/profiling-ui) | - | 19 | 1 | 19 | 3 | +| | [@elastic/profiling-ui](https://github.com/orgs/elastic/teams/profiling-ui) | - | 17 | 1 | 17 | 3 | +| | [@elastic/profiling-ui](https://github.com/orgs/elastic/teams/profiling-ui) | - | 3 | 0 | 3 | 1 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 4 | 0 | 4 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Reporting Services enables applications to feature reports that the user can automate with Watcher and download later. | 42 | 0 | 22 | 5 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 21 | 0 | 21 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 264 | 0 | 235 | 14 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 267 | 0 | 238 | 14 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 24 | 0 | 19 | 2 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 129 | 2 | 118 | 4 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 25 | 0 | 25 | 0 | @@ -158,7 +160,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-reporting-services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Kibana Screenshotting Plugin | 27 | 0 | 8 | 5 | | searchprofiler | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user. | 270 | 0 | 87 | 3 | -| | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | - | 177 | 3 | 111 | 34 | +| | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | - | 172 | 0 | 106 | 32 | | | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | ESS customizations for Security Solution. | 6 | 0 | 6 | 0 | | | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | Serverless customizations for security. | 6 | 0 | 6 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | The core Serverless plugin, providing APIs to Serverless Project plugins. | 17 | 0 | 16 | 0 | @@ -176,7 +178,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 31 | 0 | 26 | 6 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 1 | 0 | 1 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 0 | 0 | -| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 17 | 0 | 17 | 0 | +| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 19 | 0 | 19 | 0 | | | [@elastic/protections-experience](https://github.com/orgs/elastic/teams/protections-experience) | Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats | 30 | 0 | 14 | 5 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 257 | 1 | 213 | 22 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | @@ -184,8 +186,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 573 | 1 | 547 | 51 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Adds UI Actions service to Kibana | 145 | 0 | 103 | 9 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Extends UI Actions plugin with more functionality | 206 | 0 | 140 | 9 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains services reliant on the plugin lifecycle for the unified doc viewer component (see @kbn/unified-doc-viewer). | 13 | 0 | 10 | 3 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | The `unifiedHistogram` plugin provides UI components to create a layout including a resizable histogram and a main display. | 53 | 0 | 23 | 2 | -| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 142 | 2 | 104 | 22 | +| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 148 | 2 | 110 | 22 | | upgradeAssistant | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | | | [@elastic/uptime](https://github.com/orgs/elastic/teams/uptime) | This plugin visualizes data from Heartbeat, and integrates with other Observability solutions. | 1 | 0 | 1 | 0 | | urlDrilldown | [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds drilldown implementations to Kibana | 0 | 0 | 0 | 0 | @@ -216,9 +219,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 33 | 0 | 0 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 20 | 0 | 0 | 0 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 24 | 3 | 24 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 16 | 0 | 15 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 25 | 0 | 25 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 8 | 0 | 7 | 1 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 23 | 0 | 22 | 0 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 29 | 0 | 29 | 0 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 10 | 0 | 9 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 73 | 0 | 73 | 2 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 98 | 0 | 0 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 19 | 0 | 0 | 0 | @@ -262,7 +265,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 8 | 0 | 1 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 20 | 0 | 19 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2 | 0 | 2 | 0 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 8 | 0 | 8 | 1 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 3 | 0 | 3 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 13 | 0 | 4 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 7 | 2 | @@ -355,8 +358,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 1 | 0 | 1 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 3 | 0 | 3 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 15 | 0 | 11 | 0 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 6 | 0 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 58 | 0 | 26 | 0 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 7 | 0 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 60 | 0 | 26 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 5 | 0 | 5 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 5 | 0 | 0 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 6 | 0 | @@ -365,7 +368,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 4 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 25 | 1 | 24 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 111 | 1 | 0 | 0 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 351 | 1 | 5 | 1 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 352 | 1 | 5 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 11 | 0 | 11 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 89 | 0 | 61 | 10 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 4 | 0 | @@ -377,7 +380,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 4 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 125 | 0 | 91 | 46 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 12 | 0 | 12 | 0 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 548 | 1 | 122 | 4 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 547 | 1 | 121 | 4 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 69 | 0 | 69 | 4 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 14 | 0 | 14 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 0 | 6 | 0 | @@ -424,8 +427,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 9 | 1 | 9 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 101 | 0 | 85 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 15 | 0 | 9 | 0 | -| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 27 | 2 | 24 | 0 | -| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 59 | 0 | 34 | 3 | +| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 29 | 2 | 25 | 0 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 65 | 0 | 38 | 3 | | | [@elastic/docs](https://github.com/orgs/elastic/teams/docs) | - | 73 | 0 | 73 | 2 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 1 | 0 | 1 | 0 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 39 | 0 | 26 | 5 | @@ -433,10 +436,10 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 35125 | 0 | 34718 | 0 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 13 | 0 | 5 | 0 | | | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | - | 84 | 0 | 64 | 5 | -| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 4 | 0 | 4 | 0 | +| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 8 | 0 | 7 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 27 | 0 | 14 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 3 | 0 | -| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 255 | 1 | 195 | 15 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 259 | 1 | 199 | 15 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 12 | 0 | 12 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 1 | 0 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 39 | 0 | 39 | 0 | @@ -497,7 +500,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 2 | 0 | 1 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 8 | 0 | 8 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 31 | 1 | 24 | 1 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 71 | 0 | 69 | 3 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 74 | 0 | 72 | 3 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 55 | 1 | 50 | 0 | | | [@elastic/actionable-observability](https://github.com/orgs/elastic/teams/actionable-observability) | - | 13 | 0 | 13 | 3 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 45 | 0 | 45 | 10 | @@ -523,8 +526,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 16 | 0 | 16 | 1 | | | [@elastic/security-detections-response](https://github.com/orgs/elastic/teams/security-detections-response) | - | 107 | 0 | 104 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 2 | 0 | -| | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | - | 64 | 0 | 64 | 0 | +| | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | - | 65 | 0 | 65 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 16 | 0 | 8 | 0 | +| | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 14 | 0 | 14 | 6 | | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 50 | 0 | 47 | 0 | | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 29 | 0 | 23 | 0 | | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 2 | 0 | 0 | 0 | @@ -596,21 +600,23 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 4 | 0 | 2 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 41 | 2 | 21 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 5 | 1 | -| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 282 | 4 | 238 | 12 | +| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 289 | 4 | 242 | 12 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 137 | 5 | 105 | 2 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 1 | 0 | -| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 15 | 0 | 14 | 0 | +| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 17 | 0 | 16 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 72 | 0 | 55 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 39 | 0 | 25 | 1 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 86 | 0 | 86 | 1 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 49 | 0 | 35 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 47 | 0 | 38 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 7 | 0 | 6 | 0 | -| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list and field stats which can be integrated into apps | 306 | 0 | 277 | 9 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the unified data table which can be integrated into apps | 88 | 0 | 39 | 1 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 10 | 0 | 7 | 6 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list and field stats which can be integrated into apps | 302 | 0 | 275 | 9 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 4 | 0 | 0 | 0 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | - | 3 | 0 | 2 | 1 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 80 | 0 | 21 | 2 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 0 | 15 | 1 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 37 | 0 | 16 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 2 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 24 | 0 | 14 | 0 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 155 | 0 | 151 | 3 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index cdc6fdb4be051..17307c56df40b 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/profiling.devdocs.json b/api_docs/profiling.devdocs.json index c6b864a1e228a..1b57fc843050b 100644 --- a/api_docs/profiling.devdocs.json +++ b/api_docs/profiling.devdocs.json @@ -185,39 +185,6 @@ "common": { "classes": [], "functions": [ - { - "parentPluginId": "profiling", - "id": "def-common.fromMapToRecord", - "type": "Function", - "tags": [], - "label": "fromMapToRecord", - "description": [], - "signature": [ - "(m: Map) => Record" - ], - "path": "x-pack/plugins/profiling/common/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "profiling", - "id": "def-common.fromMapToRecord.$1", - "type": "Object", - "tags": [], - "label": "m", - "description": [], - "signature": [ - "Map" - ], - "path": "x-pack/plugins/profiling/common/index.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "profiling", "id": "def-common.getRoutePaths", diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index b9e7c8fa13d56..c733c69c88c29 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/profiling-ui](https://github.com/orgs/elastic/teams/profiling- | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 19 | 1 | 19 | 3 | +| 17 | 1 | 17 | 3 | ## Client diff --git a/api_docs/profiling_data_access.devdocs.json b/api_docs/profiling_data_access.devdocs.json new file mode 100644 index 0000000000000..3528e7e26c101 --- /dev/null +++ b/api_docs/profiling_data_access.devdocs.json @@ -0,0 +1,76 @@ +{ + "id": "profilingDataAccess", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "parentPluginId": "profilingDataAccess", + "id": "def-server.ProfilingConfig", + "type": "Type", + "tags": [], + "label": "ProfilingConfig", + "description": [], + "signature": [ + "{ readonly elasticsearch?: Readonly<{} & { username: string; hosts: string; password: string; }> | undefined; }" + ], + "path": "x-pack/plugins/profiling_data_access/server/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "profilingDataAccess", + "id": "def-server.ProfilingDataAccessPluginStart", + "type": "Type", + "tags": [], + "label": "ProfilingDataAccessPluginStart", + "description": [], + "signature": [ + "{ services: { fetchFlamechartData: ({ esClient, rangeFrom, rangeTo, kuery }: FetchFlamechartParams) => Promise<", + "BaseFlameGraph", + ">; }; }" + ], + "path": "x-pack/plugins/profiling_data_access/server/plugin.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [], + "start": { + "parentPluginId": "profilingDataAccess", + "id": "def-server.ProfilingDataAccessPluginSetup", + "type": "Type", + "tags": [], + "label": "ProfilingDataAccessPluginSetup", + "description": [], + "signature": [ + "void" + ], + "path": "x-pack/plugins/profiling_data_access/server/plugin.ts", + "deprecated": false, + "trackAdoption": false, + "lifecycle": "start", + "initialIsOpen": true + } + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/profiling_data_access.mdx b/api_docs/profiling_data_access.mdx new file mode 100644 index 0000000000000..7dfef9cbc7f5d --- /dev/null +++ b/api_docs/profiling_data_access.mdx @@ -0,0 +1,33 @@ +--- +#### +#### 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: kibProfilingDataAccessPluginApi +slug: /kibana-dev-docs/api/profilingDataAccess +title: "profilingDataAccess" +image: https://source.unsplash.com/400x175/?github +description: API docs for the profilingDataAccess plugin +date: 2023-09-05 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profilingDataAccess'] +--- +import profilingDataAccessObj from './profiling_data_access.devdocs.json'; + + + +Contact [@elastic/profiling-ui](https://github.com/orgs/elastic/teams/profiling-ui) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3 | 0 | 3 | 1 | + +## Server + +### Start + + +### Consts, variables and types + + diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index a1f6c7a297e1a..46f7029e4c437 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index 8b4a86aeeb456..652db20d467be 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index 0b3598ae263ed..f05dfff64c6eb 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.devdocs.json b/api_docs/rule_registry.devdocs.json index 5becb8288089d..d21e3951f5f0d 100644 --- a/api_docs/rule_registry.devdocs.json +++ b/api_docs/rule_registry.devdocs.json @@ -891,6 +891,22 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.RuleDataClient.isUsingDataStreams", + "type": "Function", + "tags": [], + "label": "isUsingDataStreams", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "ruleRegistry", "id": "def-server.RuleDataClient.getReader", @@ -1719,7 +1735,7 @@ "section": "def-common.RuleTypeState", "text": "RuleTypeState" }, - " = never, InstanceState extends { [x: string]: unknown; } = never, InstanceContext extends { [x: string]: unknown; } = never, ActionGroupIds extends string = never>(wrappedExecutor: ", + " = never, InstanceState extends Record = never, InstanceContext extends { [x: string]: unknown; } = never, ActionGroupIds extends string = never>(wrappedExecutor: ", { "pluginId": "ruleRegistry", "scope": "server", @@ -1844,7 +1860,7 @@ "section": "def-common.RuleTypeParams", "text": "RuleTypeParams" }, - ", TAlertInstanceState extends { [x: string]: unknown; }, TAlertInstanceContext extends { [x: string]: unknown; }, TActionGroupIds extends string, TServices extends ", + ", TAlertInstanceState extends Record, TAlertInstanceContext extends { [x: string]: unknown; }, TActionGroupIds extends string, TServices extends ", { "pluginId": "ruleRegistry", "scope": "server", @@ -1987,7 +2003,7 @@ "section": "def-server.RuleExecutorOptions", "text": "RuleExecutorOptions" }, - ") => Promise<{ state: TState; }>; id: string; name: string; producer: string; validate: { params: ", + ", { [x: string]: unknown; }, \"default\", never>) => Promise<{ state: TState; }>; id: string; name: string; producer: string; validate: { params: ", "RuleTypeParamsValidator", "; }; cancelAlertsOnRuleTimeout?: boolean | undefined; alerts?: ", { @@ -2542,6 +2558,22 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.IRuleDataClient.isUsingDataStreams", + "type": "Function", + "tags": [], + "label": "isUsingDataStreams", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "x-pack/plugins/rule_registry/server/rule_data_client/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "ruleRegistry", "id": "def-server.IRuleDataClient.getReader", @@ -3741,6 +3773,17 @@ "path": "x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.RuleDataClientConstructorOptions.isUsingDataStreams", + "type": "boolean", + "tags": [], + "label": "isUsingDataStreams", + "description": [], + "path": "x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -3791,7 +3834,7 @@ "section": "def-server.RuleType", "text": "RuleType" }, - ", \"executor\"> & { executor: ", + ", TAlertInstanceContext, string, string, never>, \"executor\"> & { executor: ", "AlertTypeExecutor", "; }" ], @@ -3856,7 +3899,7 @@ "section": "def-server.RuleType", "text": "RuleType" }, - "" + ", { [x: string]: unknown; }, \"default\", never, never>" ], "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", "deprecated": false, @@ -4174,7 +4217,7 @@ "section": "def-server.RuleType", "text": "RuleType" }, - ", \"executor\"> & { executor: (options: ", + ", TInstanceContext, TActionGroupIds, never, never>, \"executor\"> & { executor: (options: ", { "pluginId": "alerting", "scope": "server", @@ -4182,7 +4225,7 @@ "section": "def-server.RuleExecutorOptions", "text": "RuleExecutorOptions" }, - ", TInstanceContext, ", { "pluginId": "alerting", "scope": "common", @@ -4519,7 +4562,7 @@ "section": "def-common.RuleTypeParams", "text": "RuleTypeParams" }, - ", TAlertInstanceState extends { [x: string]: unknown; }, TAlertInstanceContext extends { [x: string]: unknown; }, TActionGroupIds extends string, TServices extends ", + ", TAlertInstanceState extends Record, TAlertInstanceContext extends { [x: string]: unknown; }, TActionGroupIds extends string, TServices extends ", { "pluginId": "ruleRegistry", "scope": "server", diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 5922688ec3d5f..04ca4e478bd01 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 264 | 0 | 235 | 14 | +| 267 | 0 | 238 | 14 | ## Server diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index eafa39a8b1ec8..deb21fb5af459 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index 1ba7eadf0ef98..a389e8c341df1 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index e23535dc14647..51f2eac47f7ee 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 98c2613f77c9a..cc16ca5a190d2 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index a063eb533b714..6e99575f34bea 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index fade576fdfed1..32fbb58ee3493 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index 15e49ecd643ca..f606643f4009d 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index 8e3054654149e..fffd6ea49de6f 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index a56afa3ea5eab..ffd1930565da5 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 436d9fc2a62b3..3950548c880fb 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.devdocs.json b/api_docs/security_solution.devdocs.json index 5ebed17999232..45c4a2685ac7c 100644 --- a/api_docs/security_solution.devdocs.json +++ b/api_docs/security_solution.devdocs.json @@ -1941,6 +1941,38 @@ ], "returnComment": [] }, + { + "parentPluginId": "securitySolution", + "id": "def-public.PluginStart.setDashboardsLandingCallout", + "type": "Function", + "tags": [], + "label": "setDashboardsLandingCallout", + "description": [], + "signature": [ + "(dashboardsLandingCallout: React.ComponentType<{}>) => void" + ], + "path": "x-pack/plugins/security_solution/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "securitySolution", + "id": "def-public.PluginStart.setDashboardsLandingCallout.$1", + "type": "CompoundType", + "tags": [], + "label": "dashboardsLandingCallout", + "description": [], + "signature": [ + "React.ComponentType<{}>" + ], + "path": "x-pack/plugins/security_solution/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, { "parentPluginId": "securitySolution", "id": "def-public.PluginStart.getBreadcrumbsNav$", @@ -2792,22 +2824,16 @@ "children": [ { "parentPluginId": "securitySolution", - "id": "def-server.SecuritySolutionPluginSetup.setAppFeatures", + "id": "def-server.SecuritySolutionPluginSetup.setAppFeaturesConfigurator", "type": "Function", "tags": [], - "label": "setAppFeatures", + "label": "setAppFeaturesConfigurator", "description": [ - "\nSets the app features that are available to the Security Solution" + "\nSets the configurations for app features that are available to the Security Solution" ], "signature": [ - "(appFeatureKeys: ", - { - "pluginId": "securitySolution", - "scope": "common", - "docId": "kibSecuritySolutionPluginApi", - "section": "def-common.AppFeatureKeys", - "text": "AppFeatureKeys" - }, + "(configurator: ", + "AppFeaturesConfigurator", ") => void" ], "path": "x-pack/plugins/security_solution/server/plugin_contract.ts", @@ -2817,22 +2843,15 @@ "children": [ { "parentPluginId": "securitySolution", - "id": "def-server.SecuritySolutionPluginSetup.setAppFeatures.$1", - "type": "Array", + "id": "def-server.SecuritySolutionPluginSetup.setAppFeaturesConfigurator.$1", + "type": "Object", "tags": [], - "label": "appFeatureKeys", + "label": "configurator", "description": [], "signature": [ - { - "pluginId": "securitySolution", - "scope": "common", - "docId": "kibSecuritySolutionPluginApi", - "section": "def-common.AppFeatureKey", - "text": "AppFeatureKey" - }, - "[]" + "AppFeaturesConfigurator" ], - "path": "x-pack/plugins/security_solution/server/lib/app_features/app_features.ts", + "path": "x-pack/plugins/security_solution/server/lib/app_features_service/app_features_service.ts", "deprecated": false, "trackAdoption": false } @@ -2938,47 +2957,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "securitySolution", - "id": "def-common.AppFeatureKey", - "type": "Type", - "tags": [], - "label": "AppFeatureKey", - "description": [], - "signature": [ - "AppFeatureSecurityKey", - " | ", - "AppFeatureCasesKey", - " | ", - "AppFeatureAssistantKey" - ], - "path": "x-pack/plugins/security_solution/common/types/app_features.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "securitySolution", - "id": "def-common.AppFeatureKeys", - "type": "Type", - "tags": [], - "label": "AppFeatureKeys", - "description": [], - "signature": [ - { - "pluginId": "securitySolution", - "scope": "common", - "docId": "kibSecuritySolutionPluginApi", - "section": "def-common.AppFeatureKey", - "text": "AppFeatureKey" - }, - "[]" - ], - "path": "x-pack/plugins/security_solution/common/types/app_features.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "securitySolution", "id": "def-common.CASES_FEATURE_ID", @@ -3058,27 +3036,6 @@ } ], "objects": [ - { - "parentPluginId": "securitySolution", - "id": "def-common.ALL_APP_FEATURE_KEYS", - "type": "Object", - "tags": [], - "label": "ALL_APP_FEATURE_KEYS", - "description": [], - "signature": [ - "readonly (", - "AppFeatureSecurityKey", - " | ", - "AppFeatureCasesKey", - ".casesConnectors | ", - "AppFeatureAssistantKey", - ".assistant)[]" - ], - "path": "x-pack/plugins/security_solution/common/types/app_features.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "securitySolution", "id": "def-common.allowedExperimentalValues", @@ -3095,62 +3052,6 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false - }, - { - "parentPluginId": "securitySolution", - "id": "def-common.AppFeatureKey", - "type": "Object", - "tags": [], - "label": "AppFeatureKey", - "description": [], - "path": "x-pack/plugins/security_solution/common/types/app_features.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "securitySolution", - "id": "def-common.AppFeatureKey.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/security_solution/common/types/app_features.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "securitySolution", - "id": "def-common.AppFeatureKey.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/security_solution/common/types/app_features.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "securitySolution", - "id": "def-common.AppFeatureKey.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/security_solution/common/types/app_features.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false } ] } diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 41c7553e1292c..56c700c517970 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution](https://github.com/orgs/elastic/teams/secur | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 177 | 3 | 111 | 34 | +| 172 | 0 | 106 | 32 | ## Client diff --git a/api_docs/security_solution_ess.mdx b/api_docs/security_solution_ess.mdx index 34e38e14067dd..98f2cb7869788 100644 --- a/api_docs/security_solution_ess.mdx +++ b/api_docs/security_solution_ess.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolutionEss title: "securitySolutionEss" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolutionEss plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolutionEss'] --- import securitySolutionEssObj from './security_solution_ess.devdocs.json'; diff --git a/api_docs/security_solution_serverless.mdx b/api_docs/security_solution_serverless.mdx index e5e61401fc375..fe15bc4706c25 100644 --- a/api_docs/security_solution_serverless.mdx +++ b/api_docs/security_solution_serverless.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolutionServerless title: "securitySolutionServerless" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolutionServerless plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolutionServerless'] --- import securitySolutionServerlessObj from './security_solution_serverless.devdocs.json'; diff --git a/api_docs/serverless.mdx b/api_docs/serverless.mdx index cd2b74a76fdf9..0528a3c42d91c 100644 --- a/api_docs/serverless.mdx +++ b/api_docs/serverless.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverless title: "serverless" image: https://source.unsplash.com/400x175/?github description: API docs for the serverless plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverless'] --- import serverlessObj from './serverless.devdocs.json'; diff --git a/api_docs/serverless_observability.mdx b/api_docs/serverless_observability.mdx index 651a305fafd4c..ca487148446ab 100644 --- a/api_docs/serverless_observability.mdx +++ b/api_docs/serverless_observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverlessObservability title: "serverlessObservability" image: https://source.unsplash.com/400x175/?github description: API docs for the serverlessObservability plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverlessObservability'] --- import serverlessObservabilityObj from './serverless_observability.devdocs.json'; diff --git a/api_docs/serverless_search.mdx b/api_docs/serverless_search.mdx index f02a9caaafbe1..2382a958e93a4 100644 --- a/api_docs/serverless_search.mdx +++ b/api_docs/serverless_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverlessSearch title: "serverlessSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the serverlessSearch plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverlessSearch'] --- import serverlessSearchObj from './serverless_search.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index b9480241e7352..0c2b0290e9db7 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 1ee73c2a226e6..6385220853e90 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index c8ca4eca7cce4..7d25378cd6f0e 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index deb0f1803fa38..54715a2f5bf55 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index 6f64356d89cea..546ab4089308b 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index b9dcc1d398047..cfe538fc4b18e 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index 892313b66db13..c110a53377d03 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index 9026cd46b9b65..7bfe57d68a2d1 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index d73c7d6246bbd..40acb98da6933 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 5e0a86eb22bfb..0df30daa7aa12 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index 000818037c4e2..7a11b41d310d4 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/text_based_languages.devdocs.json b/api_docs/text_based_languages.devdocs.json index e5f1b90b12a78..85e21f93ca908 100644 --- a/api_docs/text_based_languages.devdocs.json +++ b/api_docs/text_based_languages.devdocs.json @@ -210,6 +210,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "textBasedLanguages", + "id": "def-public.TextBasedLanguagesEditorProps.warning", + "type": "string", + "tags": [], + "label": "warning", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "textBasedLanguages", "id": "def-public.TextBasedLanguagesEditorProps.isDisabled", @@ -251,6 +265,20 @@ "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "textBasedLanguages", + "id": "def-public.TextBasedLanguagesEditorProps.hideMinimizeButton", + "type": "CompoundType", + "tags": [], + "label": "hideMinimizeButton", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/text_based_languages.mdx b/api_docs/text_based_languages.mdx index d59e94ecd47ed..0742546f60027 100644 --- a/api_docs/text_based_languages.mdx +++ b/api_docs/text_based_languages.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/textBasedLanguages title: "textBasedLanguages" image: https://source.unsplash.com/400x175/?github description: API docs for the textBasedLanguages plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'textBasedLanguages'] --- import textBasedLanguagesObj from './text_based_languages.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 17 | 0 | 17 | 0 | +| 19 | 0 | 19 | 0 | ## Client diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index f94df2ae0551f..c7145498c1a5c 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index db9408698ada4..bbc1eb06c8e39 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index c5c72936b736f..e5d3181a1d8d6 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index a87bb62ead45a..51693988db001 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 2918437a402fa..a60e9a0225c50 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index e3f159118cf9c..8acfb4007dac3 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_doc_viewer.devdocs.json b/api_docs/unified_doc_viewer.devdocs.json new file mode 100644 index 0000000000000..eb3609aa71e6d --- /dev/null +++ b/api_docs/unified_doc_viewer.devdocs.json @@ -0,0 +1,263 @@ +{ + "id": "unifiedDocViewer", + "client": { + "classes": [], + "functions": [ + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.JsonCodeEditor", + "type": "Function", + "tags": [], + "label": "JsonCodeEditor", + "description": [], + "signature": [ + "React.ForwardRefExoticComponent<", + "JsonCodeEditorProps", + " & React.RefAttributes<{}>>" + ], + "path": "src/plugins/unified_doc_viewer/public/index.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.JsonCodeEditor.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.UnifiedDocViewer", + "type": "Function", + "tags": [], + "label": "UnifiedDocViewer", + "description": [], + "signature": [ + "React.ForwardRefExoticComponent<", + "DocViewRenderProps", + " & React.RefAttributes<{}>>" + ], + "path": "src/plugins/unified_doc_viewer/public/index.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.UnifiedDocViewer.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.useEsDocSearch", + "type": "Function", + "tags": [], + "label": "useEsDocSearch", + "description": [ + "\nCustom react hook for querying a single doc in ElasticSearch" + ], + "signature": [ + "({\n id,\n index,\n dataView,\n requestSource,\n textBasedHits,\n}: ", + "EsDocSearchProps", + ") => [", + { + "pluginId": "@kbn/unified-doc-viewer", + "scope": "common", + "docId": "kibKbnUnifiedDocViewerPluginApi", + "section": "def-common.ElasticRequestState", + "text": "ElasticRequestState" + }, + ", ", + "DataTableRecord", + " | null, () => void]" + ], + "path": "src/plugins/unified_doc_viewer/public/hooks/use_es_doc_search.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.useEsDocSearch.$1", + "type": "Object", + "tags": [], + "label": "{\n id,\n index,\n dataView,\n requestSource,\n textBasedHits,\n}", + "description": [], + "signature": [ + "EsDocSearchProps" + ], + "path": "src/plugins/unified_doc_viewer/public/hooks/use_es_doc_search.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.useUnifiedDocViewerServices", + "type": "Function", + "tags": [], + "label": "useUnifiedDocViewerServices", + "description": [], + "signature": [ + "() => ", + "UnifiedDocViewerServices" + ], + "path": "src/plugins/unified_doc_viewer/public/hooks/use_doc_viewer_services.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.UnifiedDocViewerSetup", + "type": "Interface", + "tags": [], + "label": "UnifiedDocViewerSetup", + "description": [], + "path": "src/plugins/unified_doc_viewer/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.UnifiedDocViewerSetup.addDocView", + "type": "Function", + "tags": [], + "label": "addDocView", + "description": [], + "signature": [ + "(docViewRaw: ", + "DocViewInput", + " | ", + "DocViewInputFn", + ") => void" + ], + "path": "src/plugins/unified_doc_viewer/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.UnifiedDocViewerSetup.addDocView.$1", + "type": "CompoundType", + "tags": [], + "label": "docViewRaw", + "description": [], + "signature": [ + "DocViewInput", + " | ", + "DocViewInputFn" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.UnifiedDocViewerStart", + "type": "Interface", + "tags": [], + "label": "UnifiedDocViewerStart", + "description": [], + "path": "src/plugins/unified_doc_viewer/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.UnifiedDocViewerStart.getDocViews", + "type": "Function", + "tags": [], + "label": "getDocViews", + "description": [], + "signature": [ + "(hit: ", + "DataTableRecord", + ") => ", + "DocView", + "[]" + ], + "path": "src/plugins/unified_doc_viewer/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "unifiedDocViewer", + "id": "def-public.UnifiedDocViewerStart.getDocViews.$1", + "type": "Object", + "tags": [], + "label": "hit", + "description": [], + "signature": [ + "DataTableRecord" + ], + "path": "packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/unified_doc_viewer.mdx b/api_docs/unified_doc_viewer.mdx new file mode 100644 index 0000000000000..603f48d60f203 --- /dev/null +++ b/api_docs/unified_doc_viewer.mdx @@ -0,0 +1,36 @@ +--- +#### +#### 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: kibUnifiedDocViewerPluginApi +slug: /kibana-dev-docs/api/unifiedDocViewer +title: "unifiedDocViewer" +image: https://source.unsplash.com/400x175/?github +description: API docs for the unifiedDocViewer plugin +date: 2023-09-05 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedDocViewer'] +--- +import unifiedDocViewerObj from './unified_doc_viewer.devdocs.json'; + +This plugin contains services reliant on the plugin lifecycle for the unified doc viewer component (see @kbn/unified-doc-viewer). + +Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 13 | 0 | 10 | 3 | + +## Client + +### Setup + + +### Start + + +### Functions + + diff --git a/api_docs/unified_histogram.devdocs.json b/api_docs/unified_histogram.devdocs.json index 5b5b6d13ef809..afa74e73e7990 100644 --- a/api_docs/unified_histogram.devdocs.json +++ b/api_docs/unified_histogram.devdocs.json @@ -468,7 +468,7 @@ }, " | undefined; } & Pick<", "UnifiedHistogramLayoutProps", - ", \"children\" | \"className\" | \"query\" | \"filters\" | \"columns\" | \"timeRange\" | \"dataView\" | \"services\" | \"relativeTimeRange\" | \"resizeRef\" | \"appendHitsCounter\"> & React.RefAttributes<", + ", \"children\" | \"className\" | \"query\" | \"filters\" | \"columns\" | \"onBrushEnd\" | \"disabledActions\" | \"timeRange\" | \"services\" | \"dataView\" | \"relativeTimeRange\" | \"resizeRef\" | \"appendHitsCounter\" | \"onFilter\" | \"withDefaultActions\"> & React.RefAttributes<", { "pluginId": "unifiedHistogram", "scope": "public", @@ -1178,7 +1178,7 @@ }, " | undefined; } & Pick<", "UnifiedHistogramLayoutProps", - ", \"children\" | \"className\" | \"query\" | \"filters\" | \"columns\" | \"timeRange\" | \"dataView\" | \"services\" | \"relativeTimeRange\" | \"resizeRef\" | \"appendHitsCounter\">" + ", \"children\" | \"className\" | \"query\" | \"filters\" | \"columns\" | \"onBrushEnd\" | \"disabledActions\" | \"timeRange\" | \"services\" | \"dataView\" | \"relativeTimeRange\" | \"resizeRef\" | \"appendHitsCounter\" | \"onFilter\" | \"withDefaultActions\">" ], "path": "src/plugins/unified_histogram/public/container/container.tsx", "deprecated": false, diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index 50929095b7b86..7b1d9e1b20dad 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; diff --git a/api_docs/unified_search.devdocs.json b/api_docs/unified_search.devdocs.json index a9e18958ae822..db79545b07381 100644 --- a/api_docs/unified_search.devdocs.json +++ b/api_docs/unified_search.devdocs.json @@ -3,6 +3,157 @@ "client": { "classes": [], "functions": [ + { + "parentPluginId": "unifiedSearch", + "id": "def-public.createFilterAction", + "type": "Function", + "tags": [], + "label": "createFilterAction", + "description": [], + "signature": [ + "(filterManager: ", + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataQueryPluginApi", + "section": "def-public.FilterManager", + "text": "FilterManager" + }, + ", timeFilter: ", + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataQueryPluginApi", + "section": "def-public.TimefilterContract", + "text": "TimefilterContract" + }, + ", theme: ", + { + "pluginId": "@kbn/core-theme-browser", + "scope": "common", + "docId": "kibKbnCoreThemeBrowserPluginApi", + "section": "def-common.ThemeServiceSetup", + "text": "ThemeServiceSetup" + }, + ", id: string, type: string) => ", + { + "pluginId": "uiActions", + "scope": "public", + "docId": "kibUiActionsPluginApi", + "section": "def-public.ActionDefinition", + "text": "ActionDefinition" + }, + "<", + { + "pluginId": "unifiedSearch", + "scope": "public", + "docId": "kibUnifiedSearchPluginApi", + "section": "def-public.ApplyGlobalFilterActionContext", + "text": "ApplyGlobalFilterActionContext" + }, + ">" + ], + "path": "src/plugins/unified_search/public/actions/apply_filter_action.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedSearch", + "id": "def-public.createFilterAction.$1", + "type": "Object", + "tags": [], + "label": "filterManager", + "description": [], + "signature": [ + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataQueryPluginApi", + "section": "def-public.FilterManager", + "text": "FilterManager" + } + ], + "path": "src/plugins/unified_search/public/actions/apply_filter_action.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.createFilterAction.$2", + "type": "Object", + "tags": [], + "label": "timeFilter", + "description": [], + "signature": [ + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataQueryPluginApi", + "section": "def-public.TimefilterContract", + "text": "TimefilterContract" + } + ], + "path": "src/plugins/unified_search/public/actions/apply_filter_action.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.createFilterAction.$3", + "type": "Object", + "tags": [], + "label": "theme", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-theme-browser", + "scope": "common", + "docId": "kibKbnCoreThemeBrowserPluginApi", + "section": "def-common.ThemeServiceSetup", + "text": "ThemeServiceSetup" + } + ], + "path": "src/plugins/unified_search/public/actions/apply_filter_action.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.createFilterAction.$4", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/unified_search/public/actions/apply_filter_action.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.createFilterAction.$5", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/unified_search/public/actions/apply_filter_action.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "unifiedSearch", "id": "def-public.createSearchBar", @@ -275,7 +426,7 @@ "section": "def-public.FilterItemsProps", "text": "FilterItemsProps" }, - ", \"filters\" | \"indexPatterns\" | \"readOnly\" | \"filtersForSuggestions\" | \"suggestionsAbstraction\" | \"onFiltersUpdated\" | \"hiddenPanelOptions\" | \"timeRangeForSuggestionsOverride\"> & React.RefAttributes & React.RefAttributes, any, any>>) => JSX.Element" + ", \"filters\" | \"indexPatterns\" | \"filtersForSuggestions\" | \"suggestionsAbstraction\" | \"onFiltersUpdated\" | \"hiddenPanelOptions\" | \"readOnly\" | \"timeRangeForSuggestionsOverride\">, any, any>>) => JSX.Element" ], "path": "src/plugins/unified_search/public/filter_bar/index.tsx", "deprecated": false, @@ -305,7 +456,7 @@ "section": "def-public.FilterItemsProps", "text": "FilterItemsProps" }, - ", \"filters\" | \"indexPatterns\" | \"readOnly\" | \"filtersForSuggestions\" | \"suggestionsAbstraction\" | \"onFiltersUpdated\" | \"hiddenPanelOptions\" | \"timeRangeForSuggestionsOverride\"> & React.RefAttributes & React.RefAttributes, any, any>>" + ", \"filters\" | \"indexPatterns\" | \"filtersForSuggestions\" | \"suggestionsAbstraction\" | \"onFiltersUpdated\" | \"hiddenPanelOptions\" | \"readOnly\" | \"timeRangeForSuggestionsOverride\">, any, any>>" ], "path": "src/plugins/unified_search/public/filter_bar/index.tsx", "deprecated": false, @@ -550,7 +701,7 @@ "section": "def-public.DataViewPickerProps", "text": "DataViewPickerProps" }, - " | undefined; textBasedLanguageModeErrors?: Error[] | undefined; onTextBasedSavedAndExit?: (({ onSave }: ", + " | undefined; textBasedLanguageModeErrors?: Error[] | undefined; textBasedLanguageModeWarning?: string | undefined; onTextBasedSavedAndExit?: (({ onSave }: ", "OnSaveTextLanguageQueryProps", ") => void) | undefined; showSubmitButton?: boolean | undefined; submitButtonStyle?: \"full\" | \"auto\" | \"iconOnly\" | undefined; suggestionsSize?: ", "SuggestionsListSize", diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 2af9c4d1aee5a..fd7beddabd867 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 142 | 2 | 104 | 22 | +| 148 | 2 | 110 | 22 | ## Client diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index abbd20438c48c..a72a03bf4fac3 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 142 | 2 | 104 | 22 | +| 148 | 2 | 110 | 22 | ## Client diff --git a/api_docs/uptime.mdx b/api_docs/uptime.mdx index 2c463d45f48b3..62c01dfb2cc5f 100644 --- a/api_docs/uptime.mdx +++ b/api_docs/uptime.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uptime title: "uptime" image: https://source.unsplash.com/400x175/?github description: API docs for the uptime plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uptime'] --- import uptimeObj from './uptime.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index 595e139f1e17c..11d978023890f 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.devdocs.json b/api_docs/usage_collection.devdocs.json index d3e9b42e13140..2def62795ca5f 100644 --- a/api_docs/usage_collection.devdocs.json +++ b/api_docs/usage_collection.devdocs.json @@ -1942,7 +1942,7 @@ "section": "def-server.MakeSchemaFrom", "text": "MakeSchemaFrom" }, - " | undefined; fetch: ", + " | undefined; fetch: ", { "pluginId": "usageCollection", "scope": "server", @@ -1981,9 +1981,9 @@ "signature": [ "{ [Key in keyof Required]: Required[Key] extends (infer U)[] ? { type: \"array\"; items: ", "RecursiveMakeSchemaFrom", - "; } : ", + "; } : ", "RecursiveMakeSchemaFrom", - "[Key]>; }" + "[Key], RequireMeta>; }" ], "path": "src/plugins/usage_collection/server/collector/types.ts", "deprecated": false, @@ -2025,7 +2025,7 @@ "section": "def-server.MakeSchemaFrom", "text": "MakeSchemaFrom" }, - " | undefined; fetch: ", + " | undefined; fetch: ", { "pluginId": "usageCollection", "scope": "server", @@ -2041,7 +2041,7 @@ "section": "def-server.MakeSchemaFrom", "text": "MakeSchemaFrom" }, - " | undefined; fetch: ", + " | undefined; fetch: ", { "pluginId": "usageCollection", "scope": "server", diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 91bde275ef5b2..960eb658ca681 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index bc82604a8e09d..15e9fd60eafed 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index d789cfa9d6ddd..59c3f306ced16 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index b7fbc17a2559b..0010c70cfdbde 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index e58463315b9c5..6173477d3c7a5 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index 6501a0fb4648e..fc76ac9c6fc64 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index d4313e88fa7ac..ce5bbdddd2e36 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index f59755691b08d..c44173caff05c 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 05e3730a4d31a..d61f09f713d1b 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index b103c87abbb3c..bde589e42f805 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index 0b3e8fc1048ed..78696e8ff2b7d 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index 420f95794b21e..a98046d0fe69a 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.devdocs.json b/api_docs/visualizations.devdocs.json index 02291d78d807b..5b78f2be3fd99 100644 --- a/api_docs/visualizations.devdocs.json +++ b/api_docs/visualizations.devdocs.json @@ -2249,7 +2249,7 @@ "section": "def-common.DatatableRow", "text": "DatatableRow" }, - "[]; }" + "[]; warning?: string | undefined; }" ], "path": "src/plugins/visualizations/common/utils/prepare_log_table.ts", "deprecated": false, @@ -8133,7 +8133,7 @@ "section": "def-common.DatatableRow", "text": "DatatableRow" }, - "[]; }" + "[]; warning?: string | undefined; }" ], "path": "src/plugins/visualizations/common/utils/prepare_log_table.ts", "deprecated": false, diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index 2f2b9042c4947..bc11233812a61 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2023-08-29 +date: 2023-09-05 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; diff --git a/catalog-info.yaml b/catalog-info.yaml index d37b33eb0ebff..00637fb1a039b 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -123,4 +123,40 @@ spec: lifecycle: production system: control-plane +--- +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: kibana-serverless-release + description: Definition of the kibana release pipeline + links: + - title: Pipeline + url: https://buildkite.com/elastic/kibana-serverless-release +spec: + type: buildkite-pipeline + owner: group:kibana-operations + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana-serverless-release + description: Pipeline that releases kibana by triggering the release flow through qa, staging, and production + spec: + repository: elastic/kibana + pipeline_file: ./.buildkite/pipelines/pipeline.kibana-serverless-release.yaml + provider_settings: + build_branches: false + build_pull_request_forks: false + build_tags: true + # https://regex101.com/r/tY52jo/1 + filter_condition: 'build.tag =~ /^deploy@\d+$/' + filter_enabled: true + teams: + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + everyone: + access_level: READ_ONLY diff --git a/config/node.options b/config/node.options index d5799f2c2068a..abcb40a5c19d4 100644 --- a/config/node.options +++ b/config/node.options @@ -10,3 +10,6 @@ ## restore < Node 16 default DNS lookup behavior --dns-result-order=ipv4first + +## enable OpenSSL 3 legacy provider +--openssl-legacy-provider diff --git a/config/serverless.oblt.yml b/config/serverless.oblt.yml index 44d26351b5380..5e6b9a58cbc35 100644 --- a/config/serverless.oblt.yml +++ b/config/serverless.oblt.yml @@ -10,7 +10,6 @@ xpack.serverless.observability.enabled: true ## Configure plugins xpack.infra.logs.app_target: discover -xpack.discoverLogExplorer.featureFlags.deepLinkVisible: true ## Set the home route uiSettings.overrides.defaultRoute: /app/observability/landing diff --git a/config/serverless.yml b/config/serverless.yml index c76e622d67547..1349f71d019e6 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -17,11 +17,8 @@ migrations.algorithm: zdt # A longer migration time is acceptable due to the ZDT algorithm. migrations.batchSize: 250 -# temporarily allow to run the migration on UI nodes -# until the controller is able to spawn the migrator job/pod migrations.zdt: metaPickupSyncDelaySec: 5 - runOnRoles: ['ui'] # Ess plugins xpack.securitySolutionEss.enabled: false @@ -93,6 +90,9 @@ elasticsearch.requestHeadersWhitelist: ["authorization", "es-client-authenticati # Limit maxSockets to 800 as we do in ESS, which improves reliability under high loads. elasticsearch.maxSockets: 800 +# Enable dynamic config to be updated via the internal HTTP requests +coreApp.allowDynamicConfigOverrides: true + # Visualizations editors readonly settings vis_type_gauge.readOnly: true vis_type_heatmap.readOnly: true @@ -119,6 +119,11 @@ xpack.alerting.rules.run.ruleTypeOverrides: xpack.alerting.rules.minimumScheduleInterval.enforce: true xpack.actions.run.maxAttempts: 10 +# Disables ESQL in advanced settings (hides it from the UI) +uiSettings: + overrides: + discover:enableESQL: false + # Task Manager xpack.task_manager.allow_reading_invalid_state: false diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 898597eabce5d..f3e74bb49ab0d 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -10,6 +10,8 @@ Review important information about the {kib} 8.x releases. + +* <> * <> * <> * <> @@ -46,6 +48,40 @@ Review important information about the {kib} 8.x releases. * <> -- +[[release-notes-8.9.2]] +== {kib} 8.9.2 + +Review the following information about the {kib} 8.9.2 release. + +[float] +[[enhancement-v8.9.2]] +=== Enhancements + +Fleet:: +* Adds the configuration setting `xpack.fleet.packageVerification.gpgKeyPath` as an environment variable in the {kib} container ({kibana-pull}163783[#163783]). + +[float] +[[fixes-v8.9.2]] +=== Bug Fixes + +Dashboard:: +* Fixes missing state on short URLs could be lost on an alias match redirect ({kibana-pull}163658[#163658]). +* Fixes 'Download CSV' returning no data when panel has custom time range outside the time range of the global time picker ({kibana-pull}163887[#163887]). +* Fixes **Dashboard** getting stuck at loading in {kib} when Controls is used and mapping changed from integer to keyword ({kibana-pull}163529[#163529]). +Elastic Security:: +For the Elastic Security 8.9.2 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Lens & Visualizations:: +* Allow removing temporary data view from event annotation group in *Lens* ({kibana-pull}163976[#163976]). +Machine Learning:: +* Anomaly detection wizard: ensure custom URLs test functionality works as expected ({kibana-pull}165055[#165055]). +* Fixes anomaly detection module manifest queries for {kib} sample data sets, so cold and frozen tiers are not queried ({kibana-pull}164332[#164332]). +Management:: +* Transforms: Fixes privileges check ({kibana-pull}163687[#163687]). +Operations:: +* Fixes an issue where {kib} did not start on CentOS/RHEL 7 ({kibana-pull}165151[#165151]). +Reporting:: +* Allow custom roles to use image reporting in **Dashboard** ({kibana-pull}163873[#163873]). + [[release-notes-8.9.1]] == {kib} 8.9.1 diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index 9058b29f0cb8a..fb672b2884af2 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -11,7 +11,6 @@ Some APM app features are provided via a REST API: * <> * <> * <> -* <> * <> [float] @@ -716,219 +715,6 @@ curl -X DELETE "http://localhost:5601/api/apm/sourcemaps/apm:foo-1.0.0-644fd5a9" ******************************************************* //// -[role="xpack"] -[[android-sourcemap-api]] -=== Android source map API - -IMPORTANT: This endpoint is only compatible with the -{apm-guide-ref}/index.html[APM integration for Elastic Agent]. - -An Android source map (generated using Android's https://developer.android.com/build/shrink-code[R8 tool]) -allows obfuscated app stacktraces to be mapped back to original source code -- -allowing you to maintain the size and security of minimized code, without losing the ability to debug your application. - -For best results, uploading source maps should become a part of your deployment procedure, -and not something you only do when you see unhelpful errors. -That’s because uploading source maps after errors happen won’t make old errors magically readable -- -errors must occur again for source mapping to occur. - -The following APIs are available: - -* <> -* <> -* <> - -[float] -[[use-android-sourcemap-api]] -==== How to use APM APIs - -.Expand for required headers, privileges, and usage details -[%collapsible%closed] -====== -include::api.asciidoc[tag=using-the-APIs] -====== - -//// -******************************************************* -//// - -[[android-sourcemap-post]] -==== Create or update an Android source map - -Create or update an Android source map for a specific app and version. - -[[android-sourcemap-post-privs]] -===== Privileges - -The user accessing this endpoint requires `All` Kibana privileges for the {beat_kib_app} feature. -For more information, see <>. - -[[android-sourcemap-post-req]] -===== Request - -`POST /api/apm/androidmaps` - -[role="child_attributes"] -[[android-sourcemap-post-req-body]] -===== Request body - -`service_name`:: -(required, string) The name of the Android app that the map should apply to. - -`service_version`:: -(required, string) The version of the Android app that the map should apply to. - -`map_file`:: -(required, string or file upload) The R8-generated map. - -[[android-sourcemap-post-example]] -===== Examples - -The following example uploads a source map for a app named `foo` and a service version of `1.0.0`: - -[source,curl] --------------------------------------------------- -curl -X POST "http://localhost:5601/api/apm/androidmaps" \ --H 'Content-Type: multipart/form-data' \ --H 'kbn-xsrf: true' \ --H 'Authorization: ApiKey ${YOUR_API_KEY}' \ --F 'service_name="foo"' \ --F 'service_version="1.0.0"' \ --F 'map_file=@"/Path/to/the/file/mapping.txt"' --------------------------------------------------- - -[[android-sourcemap-post-body]] -===== Response body - -[source,js] --------------------------------------------------- -{ - "type": "sourcemap", - "identifier": "foo-1.0.0-android", - "relative_url": "/api/fleet/artifacts/foo-1.0.0-android/644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "body": "eJyFkL1OwzAUhd/Fc+MbYMuCEBIbHRjKgBgc96R16tiWr1OQqr47NwqJxEK3q/PzWccXxchnZ7E1A1SjuhjVZtF2yOxiEPlO17oWox3D3uPFeSRTjmJQARfCPeiAgGx8NTKsYdAc1T3rwaSJGcds8Sp3c1HnhfywUZ3QhMTFFGepZxqMC9oex3CS9tpk1XyozgOlmoVKuJX1DqEQZ0su7PGtLU+V/3JPKc3cL7TJ2FNDRPov4bFta3MDM4f7W69lpJjLO9qdK8bzVPhcJz3HUCQ4LbO/p5hCSC4cZPByrp/wFqOklbpefwAhzpqI", - "created": "2021-07-09T20:47:44.812Z", - "id": "apm:foo-1.0.0-android-644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "compressionAlgorithm": "zlib", - "decodedSha256": "644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "decodedSize": 441, - "encodedSha256": "024c72749c3e3dd411b103f7040ae62633558608f480bce4b108cf5b2275bd24", - "encodedSize": 237, - "encryptionAlgorithm": "none", - "packageName": "apm" -} --------------------------------------------------- - -//// -******************************************************* -//// - -[[android-sourcemap-get]] -==== Get source maps - -Returns an array of Fleet artifacts, including source map uploads. - -[[android-sourcemap-get-privs]] -===== Privileges - -The user accessing this endpoint requires `Read` or `All` Kibana privileges for the {beat_kib_app} feature. -For more information, see <>. - -[[android-sourcemap-get-req]] -===== Request - -`GET /api/apm/sourcemaps` - -[[android-sourcemap-get-example]] -===== Example - -The following example requests all uploaded source maps: - -[source,curl] --------------------------------------------------- -curl -X GET "http://localhost:5601/api/apm/sourcemaps" \ --H 'Content-Type: application/json' \ --H 'kbn-xsrf: true' \ --H 'Authorization: ApiKey ${YOUR_API_KEY}' --------------------------------------------------- - -[[android-sourcemap-get-body]] -===== Response body - -[source,js] --------------------------------------------------- -{ - "artifacts": [ - { - "type": "sourcemap", - "identifier": "foo-1.0.0-android", - "relative_url": "/api/fleet/artifacts/foo-1.0.0-android/644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "body": { - "serviceName": "foo", - "serviceVersion": "1.0.0", - "bundleFilepath": "android", - "sourceMap": "# compiler: R8\n# compiler_version: 3.2.47\n# min_api: 26\n..." - }, - "created": "2021-07-09T20:47:44.812Z", - "id": "apm:foo-1.0.0-android-644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "compressionAlgorithm": "zlib", - "decodedSha256": "644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "decodedSize": 441, - "encodedSha256": "024c72749c3e3dd411b103f7040ae62633558608f480bce4b108cf5b2275bd24", - "encodedSize": 237, - "encryptionAlgorithm": "none", - "packageName": "apm" - } - ] -} --------------------------------------------------- - -//// -******************************************************* -//// - -[[android-sourcemap-delete]] -==== Delete source map - -Delete a previously uploaded source map. - -[[android-sourcemap-delete-privs]] -===== Privileges - -The user accessing this endpoint requires `All` Kibana privileges for the {beat_kib_app} feature. -For more information, see <>. - -[[android-sourcemap-delete-req]] -===== Request - -`DELETE /api/apm/sourcemaps/:id` - -[[android-sourcemap-delete-example]] -===== Example - -The following example deletes a source map with an id of `apm:foo-1.0.0-android-644fd5a9`: - -[source,curl] --------------------------------------------------- -curl -X DELETE "http://localhost:5601/api/apm/sourcemaps/apm:foo-1.0.0-android-644fd5a9" \ --H 'Content-Type: application/json' \ --H 'kbn-xsrf: true' \ --H 'Authorization: ApiKey ${YOUR_API_KEY}' --------------------------------------------------- - -[[android-sourcemap-delete-body]] -===== Response body - -[source,js] --------------------------------------------------- -{} --------------------------------------------------- - -//// -******************************************************* -******************************************************* -//// - [role="xpack"] [[agent-key-api]] === APM agent Key API diff --git a/docs/concepts/data-views.asciidoc b/docs/concepts/data-views.asciidoc index f75012e8d69f4..c72679734b725 100644 --- a/docs/concepts/data-views.asciidoc +++ b/docs/concepts/data-views.asciidoc @@ -143,15 +143,20 @@ all clusters having a name starting with `cluster_`: `cluster_*:logstash-*,cluster_*:-logstash-old*` ``` -To exclude a cluster having a name starting with `cluster_`: +Excluding a cluster avoids sending any network calls to that cluster. +To exclude a cluster with the name `cluster_one`: ```ts -`cluster_*:logstash-*,cluster_one:-*` +`cluster_*:logstash-*,-cluster_one:*` ``` Once you configure a {data-source} to use the {ccs} syntax, all searches and aggregations using that {data-source} in {kib} take advantage of {ccs}. +For more information, refer to +{ref}/modules-cross-cluster-search.html#exclude-problematic-clusters[Excluding +clusters or indicies from cross-cluster search]. + [float] [[delete-data-view]] === Delete a {data-source} diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 632bce5883c28..9321e7499f9b4 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -336,6 +336,10 @@ In general this plugin provides: |Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. +|{kib-repo}blob/{branch}/src/plugins/unified_doc_viewer/README.md[unifiedDocViewer] +|This plugin contains services reliant on the plugin lifecycle for the unified doc viewer component (see @kbn/unified-doc-viewer). + + |{kib-repo}blob/{branch}/src/plugins/unified_histogram/README.md[unifiedHistogram] |Unified Histogram is a UX Building Block including a layout with a resizable histogram and a main display. It manages its own state and data fetching, and can easily be dropped into pages with minimal setup. @@ -520,10 +524,6 @@ Plugin server-side only. Plugin has three main functions: |Contains the enhancements to the OSS discover app. -|{kib-repo}blob/{branch}/x-pack/plugins/discover_log_explorer/README.md[discoverLogExplorer] -|This plugin registers a log-explorer profile using the Discover customization framework, offering several affordances specifically designed for log consumption. - - |{kib-repo}blob/{branch}/x-pack/plugins/ecs_data_quality_dashboard/README.md[ecsDataQualityDashboard] |This plugin implements (server) APIs used to render the content of the Data Quality dashboard. @@ -632,6 +632,10 @@ the infrastructure monitoring use-case within Kibana. using the CURL scripts in the scripts folder. +|{kib-repo}blob/{branch}/x-pack/plugins/log_explorer/README.md[logExplorer] +|This plugin provides a LogExplorer component using the Discover customization framework, offering several affordances specifically designed for log consumption. + + |{kib-repo}blob/{branch}/x-pack/plugins/logs_shared/README.md[logsShared] |Exposes the shared components and APIs to access and visualize logs. @@ -669,6 +673,10 @@ Elastic. |This document gives an overview of the features of the Observability AI Assistant at the time of writing, and how to use them. At a high level, the Observability AI Assistant offers contextual insights, and a chat functionality that we enrich with function calling, allowing the LLM to hook into the user's data. We also allow the LLM to store things it considers new information as embeddings into Elasticsearch, and query this knowledge base when it decides it needs more information, using ELSER. +|{kib-repo}blob/{branch}/x-pack/plugins/observability_log_explorer/README.md[observabilityLogExplorer] +|This plugin provides an app based on the LogExplorer component from the log_explorer plugin, but adds observability-specific affordances. + + |{kib-repo}blob/{branch}/x-pack/plugins/observability_onboarding/README.md[observabilityOnboarding] |This plugin provides an onboarding framework for observability solutions: Logs and APM. @@ -689,6 +697,10 @@ Elastic. |Universal Profiling provides fleet-wide, whole-system, continuous profiling with zero instrumentation. Get a comprehensive understanding of what lines of code are consuming compute resources throughout your entire fleet by visualizing your data in Kibana using the flamegraph, stacktraces, and top functions views. +|{kib-repo}blob/{branch}/x-pack/plugins/profiling_data_access[profilingDataAccess] +|WARNING: Missing README. + + |{kib-repo}blob/{branch}/x-pack/plugins/remote_clusters/README.md[remoteClusters] |This plugin helps users manage their remote clusters, which enable cross-cluster search and cross-cluster replication. diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 5873af1ef9cb1..e0ddf9bd3f334 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -288,8 +288,8 @@ in the current data view is used. The columns that appear by default on the *Discover* page. The default is `_source`. -[[discover:enableSql]]`discover:enableSql`:: -experimental[] Allows SQL queries for search. +[[discover:enableESQL]]`discover:enableESQL`:: +experimental[] Allows ES|QL queries for search. [[discover-max-doc-fields-displayed]]`discover:maxDocFieldsDisplayed`:: Specifies the maximum number of fields to show in the document column of the *Discover* table. diff --git a/docs/management/connectors/action-types/opsgenie.asciidoc b/docs/management/connectors/action-types/opsgenie.asciidoc index 453aa8c00b811..817acdfb135d4 100644 --- a/docs/management/connectors/action-types/opsgenie.asciidoc +++ b/docs/management/connectors/action-types/opsgenie.asciidoc @@ -28,34 +28,6 @@ URL:: The Opsgenie URL. For example, https://api.opsgenie.com or https://api.eu. NOTE: If you are using the <> setting, make sure the hostname is added to the allowed hosts. API Key:: The Opsgenie API authentication key for HTTP Basic authentication. For more details about generating Opsgenie API keys, refer to https://support.atlassian.com/opsgenie/docs/create-a-default-api-integration/[Opsgenie documentation]. -[float] -[[preconfigured-opsgenie-configuration]] -=== Create preconfigured connectors - -If you are running {kib} on-prem, you can define connectors by -adding `xpack.actions.preconfigured` settings to your `kibana.yml` file. -For example: - -[source,text] --- -xpack.actions.preconfigured: - my-opsgenie: - name: preconfigured-opsgenie-connector-type - actionTypeId: .opsgenie - config: - apiUrl: https://api.opsgenie.com - secrets: - apiKey: apikey --- - -Config defines information for the connector type. - -`apiUrl`:: A string that corresponds to *URL*. - -Secrets defines sensitive information for the connector type. - -`apiKey`:: A string that corresponds to *API Key*. - [float] [[opsgenie-action-configuration]] === Test connectors diff --git a/docs/management/connectors/action-types/server-log.asciidoc b/docs/management/connectors/action-types/server-log.asciidoc index 964d8300761a3..7399fb5456d45 100644 --- a/docs/management/connectors/action-types/server-log.asciidoc +++ b/docs/management/connectors/action-types/server-log.asciidoc @@ -3,13 +3,13 @@ ++++ Server log ++++ +:frontmatter-description: Add a connector that can write in {kib} server logs. +:frontmatter-tags-products: [kibana] +:frontmatter-tags-content-type: [how-to] +:frontmatter-tags-user-goals: [configure] A server log connector writes an entry to the {kib} server log. -You can create a server log connectors in {kib} or by using the -<>. If you are running {kib} -on-prem, you can also create preconfigured server log connectors. - [float] [[define-serverlog-ui]] === Create connectors in {kib} @@ -27,29 +27,12 @@ image::management/connectors/images/serverlog-connector.png[Server log connector Server log connectors do not have any configuration properties other than a name. -[float] -[[preconfigured-server-log-configuration]] -=== Create preconfigured connectors - -If you are running {kib} on-prem, you can define connectors by adding -`xpack.actions.preconfigured` settings to your `kibana.yml` file. For example: - -[source,text] --- -xpack.actions.preconfigured: - my-server-log: - name: preconfigured-server-log-connector-type - actionTypeId: .server-log --- - -For more information, go to <>. - [float] [[server-log-action-configuration]] === Test connectors -You can test connectors with the <> or -as you're creating or editing the connector in {kib}. For example: +You can test connectors as you're creating or editing the connector in {kib}. +For example: [role="screenshot"] image::management/connectors/images/serverlog-params-test.png[Server log connector test] diff --git a/docs/management/connectors/pre-configured-connectors.asciidoc b/docs/management/connectors/pre-configured-connectors.asciidoc index 9284a66ea9480..ceaf771a58f9e 100644 --- a/docs/management/connectors/pre-configured-connectors.asciidoc +++ b/docs/management/connectors/pre-configured-connectors.asciidoc @@ -80,9 +80,50 @@ image::images/preconfigured-connectors-managing.png[Connectors managing tab with Clicking a preconfigured connector shows the description, but not the configuration. +[float] +=== Examples + +* <> +* <> +* <> + +[float] +[[preconfigured-opsgenie-configuration]] +==== {opsgenie} connectors + +The following example creates an <>: + +[source,text] +-- +xpack.actions.preconfigured: + my-opsgenie: + name: preconfigured-opsgenie-connector-type + actionTypeId: .opsgenie + config: + apiUrl: https://api.opsgenie.com <1> + secrets: + apiKey: apikey <2> +-- +<1> The {opsgenie} URL. +<2> The {opsgenie} API authentication key for HTTP basic authentication. + +[float] +[[preconfigured-server-log-configuration]] +==== Server log connectors + +The following example creates a <>: + +[source,text] +-- +xpack.actions.preconfigured: + my-server-log: + name: preconfigured-server-log-connector-type + actionTypeId: .server-log +-- + [float] [[preconfigured-webhook-configuration]] -=== Webhook preconfigured connector example +==== Webhook connectors The following example creates a <> with basic authentication: diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 84ba3c73d3370..9ab4058d35e30 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -142,12 +142,6 @@ A list of action types that are enabled. It defaults to `["*"]`, enabling all ty + Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. -`xpack.actions.preconfiguredAlertHistoryEsIndex` {ess-icon}:: -Enables a preconfigured alert history {es} <> connector. Default: `false`. - -`xpack.actions.preconfigured`:: -Specifies preconfigured connector IDs and configs. Default: {}. - `xpack.actions.proxyUrl` {ess-icon}:: Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. + @@ -233,6 +227,56 @@ xpack.actions.run: maxAttempts: 5 -- +[float] +[[preconfigured-connector-settings]] +=== Preconfigured connector settings + +These settings vary depending on which type of <> you're adding. +For example: + +[source,yaml] +---------------------------------------- +xpack.actions.preconfigured: + my-server-log: + name: preconfigured-server-log-connector-type + actionTypeId: .server-log +---------------------------------------- + +`xpack.actions.preconfiguredAlertHistoryEsIndex` {ess-icon}:: +Enables a preconfigured alert history {es} <> connector. Default: `false`. + +`xpack.actions.preconfigured`:: +Specifies configuration details that are specific to the type of preconfigured connector. + +`xpack.actions.preconfigured..actionTypeId`:: +The type of preconfigured connector. +For example: `.email`, `.index`, `.opsgenie`, `.server-log`, `.resilient`, `.slack`, and `.webhook`. + +`xpack.actions.preconfigured..config.apiUrl`:: +A configuration URL that varies by connector: ++ +-- +* For an <>, specifies the {opsgenie} URL. For example, `https://api.opsgenie.com` or `https://api.eu.opsgenie.com`. + +NOTE: If you are using the `xpack.actions.allowedHosts` setting, make sure the hostname in the URL is added to the allowed hosts. +-- + +`xpack.actions.preconfigured..name`:: +The name of the preconfigured connector. + +`xpack.actions.preconfigured..secrets`:: +Sensitive configuration details, such as username, password, and keys, which are specific to the connector type. ++ +TIP: Sensitive properties, such as passwords, should be stored in the <>. + +`xpack.actions.preconfigured..secrets.apikey`:: +An API key secret that varies by connector: ++ +-- +* For an <>, specifies the {opsgenie} API authentication key for HTTP basic authentication. +-- + + [float] [[alert-settings]] === Alerting settings diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc index 92cb77cc401f7..e0878b4dbf849 100644 --- a/docs/user/production-considerations/production.asciidoc +++ b/docs/user/production-considerations/production.asciidoc @@ -118,3 +118,12 @@ The option accepts a limit in MB: -------- --max-old-space-size=2048 -------- + +[float] +[[openssl-legacy-provider]] +=== OpenSSL Legacy Provider + +Starting in 8.10.0, {kib} has upgraded its runtime environment, Node.js, from version 16 to version 18 and with it the underlying version of OpenSSL to version 3. +Algorithms deemed legacy by OpenSSL 3 have been re-enabled to avoid potential breaking changes in a minor version release of {kib}. +If SSL certificates configured for {kib} are not using any of the legacy algorithms mentioned in the https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html[OpenSSL legacy provider documentation], +we recommend disabling this setting by removing `--openssl-legacy-provider` in the `node.options` config file. diff --git a/examples/unified_doc_viewer/README.md b/examples/unified_doc_viewer/README.md new file mode 100644 index 0000000000000..fea17c5e9cde8 --- /dev/null +++ b/examples/unified_doc_viewer/README.md @@ -0,0 +1,3 @@ +## Unified Doc Viewer + +An example plugin showing usage of the unified doc viewer plugin (plugins/unified_doc_viewer) and package (@kbn/unified-doc-viewer). \ No newline at end of file diff --git a/examples/unified_doc_viewer/kibana.jsonc b/examples/unified_doc_viewer/kibana.jsonc new file mode 100644 index 0000000000000..6d9c4465072c6 --- /dev/null +++ b/examples/unified_doc_viewer/kibana.jsonc @@ -0,0 +1,16 @@ +{ + "type": "plugin", + "id": "@kbn/unified-doc-viewer-examples", + "owner": "@elastic/kibana-core", + "description": "Examples showing usage of the unified doc viewer.", + "plugin": { + "id": "unifiedDocViewerExamples", + "server": false, + "browser": true, + "requiredPlugins": [ + "data", + "developerExamples", + "unifiedDocViewer" + ] + } +} diff --git a/examples/unified_doc_viewer/public/application.tsx b/examples/unified_doc_viewer/public/application.tsx new file mode 100644 index 0000000000000..11cf99e1ed2bb --- /dev/null +++ b/examples/unified_doc_viewer/public/application.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; +import type { AppMountParameters, CoreStart } from '@kbn/core/public'; +import { buildDataTableRecord } from '@kbn/discover-utils'; +import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { StartDeps } from './plugin'; + +export const renderApp = ( + core: CoreStart, + { data }: StartDeps, + { element }: AppMountParameters +) => { + ReactDOM.render(, element); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; + +function UnifiedDocViewerExamplesApp({ data }: { data: DataPublicPluginStart }) { + const [dataView, setDataView] = useState(); + const [hit, setHit] = useState(); + + useEffect(() => { + data.dataViews.getDefault().then((defaultDataView) => setDataView(defaultDataView)); + }, [data]); + + useEffect(() => { + const setDefaultHit = async () => { + if (!dataView?.id) return; + const response = await data.search + .search({ + params: { + index: dataView?.getIndexPattern(), + body: { + fields: ['*'], + _source: false, + }, + }, + }) + .toPromise(); + const docs = response?.rawResponse?.hits?.hits ?? []; + if (docs.length > 0) { + const record = buildDataTableRecord(docs[0], dataView); + setHit(record); + } + }; + + setDefaultHit(); + }, [data, dataView]); + + return ( + <> + {dataView?.id && hit ? ( + + ) : ( + 'Loading... (make sure you have a default data view and at least one matching document)' + )} + + ); +} diff --git a/src/plugins/discover/public/components/discover_grid/types.ts b/examples/unified_doc_viewer/public/index.ts similarity index 60% rename from src/plugins/discover/public/components/discover_grid/types.ts rename to examples/unified_doc_viewer/public/index.ts index 71d82e35126ac..98580e69ded6e 100644 --- a/src/plugins/discover/public/components/discover_grid/types.ts +++ b/examples/unified_doc_viewer/public/index.ts @@ -5,14 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { UnifiedDocViewerExamplesPlugin } from './plugin'; -/** - * User configurable state of data grid, persisted in saved search - */ -export interface DiscoverGridSettings { - columns?: Record; -} - -export interface DiscoverGridSettingsColumn { - width?: number; +export function plugin() { + return new UnifiedDocViewerExamplesPlugin(); } diff --git a/examples/unified_doc_viewer/public/plugin.tsx b/examples/unified_doc_viewer/public/plugin.tsx new file mode 100644 index 0000000000000..d12c746a06223 --- /dev/null +++ b/examples/unified_doc_viewer/public/plugin.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; + +export interface SetupDeps { + developerExamples: DeveloperExamplesSetup; +} + +export interface StartDeps { + data: DataPublicPluginStart; +} + +export class UnifiedDocViewerExamplesPlugin implements Plugin { + public setup(core: CoreSetup, deps: SetupDeps) { + // Register an application into the side navigation menu + core.application.register({ + id: 'unifiedDocViewer', + title: 'Unified Doc Viewer Examples', + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + // Render the application + return renderApp(coreStart, depsStart, params); + }, + }); + + // This section is only needed to get this example plugin to show up in our Developer Examples. + deps.developerExamples.register({ + appId: 'unifiedDocViewer', + title: 'Unified Doc Viewer Examples', + description: 'Examples showcasing the unified doc viewer.', + }); + } + + public start(core: CoreStart) { + return {}; + } + + public stop() {} +} diff --git a/examples/unified_doc_viewer/tsconfig.json b/examples/unified_doc_viewer/tsconfig.json new file mode 100644 index 0000000000000..74f81731edbe8 --- /dev/null +++ b/examples/unified_doc_viewer/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../typings/**/*" + ], + "exclude": [ + "target/**/*", + ], + "kbn_references": [ + "@kbn/core", + "@kbn/data-plugin", + "@kbn/data-views-plugin", + "@kbn/developer-examples-plugin", + "@kbn/discover-utils", + "@kbn/unified-doc-viewer-plugin", + ] +} diff --git a/fleet_packages.json b/fleet_packages.json index a7278bbdd809e..a206560d48d80 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -24,13 +24,13 @@ [ { "name": "apm", - "version": "8.11.0-preview-1693211748", + "version": "8.11.0-preview-1693558513", "forceAlignStackVersion": true, "allowSyncToPrerelease": true }, { "name": "elastic_agent", - "version": "1.11.2" + "version": "1.12.0" }, { "name": "endpoint", @@ -54,7 +54,7 @@ }, { "name": "synthetics", - "version": "1.0.4" + "version": "1.0.6" }, { "name": "security_detection_engine", diff --git a/package.json b/package.json index fc076f17b7407..a26b2479e9816 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "dependencies": { "@appland/sql-parser": "^1.5.1", "@babel/runtime": "^7.21.0", + "@cfworker/json-schema": "^1.12.7", "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", "@dnd-kit/utilities": "^2.0.0", @@ -373,7 +374,6 @@ "@kbn/developer-examples-plugin": "link:examples/developer_examples", "@kbn/discover-customization-examples-plugin": "link:examples/discover_customization_examples", "@kbn/discover-enhanced-plugin": "link:x-pack/plugins/discover_enhanced", - "@kbn/discover-log-explorer-plugin": "link:x-pack/plugins/discover_log_explorer", "@kbn/discover-plugin": "link:src/plugins/discover", "@kbn/discover-utils": "link:packages/kbn-discover-utils", "@kbn/doc-links": "link:packages/kbn-doc-links", @@ -493,6 +493,7 @@ "@kbn/lists-plugin": "link:x-pack/plugins/lists", "@kbn/locator-examples-plugin": "link:examples/locator_examples", "@kbn/locator-explorer-plugin": "link:examples/locator_explorer", + "@kbn/log-explorer-plugin": "link:x-pack/plugins/log_explorer", "@kbn/logging": "link:packages/kbn-logging", "@kbn/logging-mocks": "link:packages/kbn-logging-mocks", "@kbn/logs-shared-plugin": "link:x-pack/plugins/logs_shared", @@ -542,6 +543,7 @@ "@kbn/observability-ai-assistant-plugin": "link:x-pack/plugins/observability_ai_assistant", "@kbn/observability-alert-details": "link:x-pack/packages/observability/alert_details", "@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability", + "@kbn/observability-log-explorer-plugin": "link:x-pack/plugins/observability_log_explorer", "@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_onboarding", "@kbn/observability-plugin": "link:x-pack/plugins/observability", "@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_shared", @@ -554,6 +556,7 @@ "@kbn/portable-dashboards-example": "link:examples/portable_dashboards_example", "@kbn/preboot-example-plugin": "link:examples/preboot_example", "@kbn/presentation-util-plugin": "link:src/plugins/presentation_util", + "@kbn/profiling-data-access-plugin": "link:x-pack/plugins/profiling_data_access", "@kbn/profiling-plugin": "link:x-pack/plugins/profiling", "@kbn/random-sampling": "link:x-pack/packages/kbn-random-sampling", "@kbn/react-field": "link:packages/kbn-react-field", @@ -604,6 +607,7 @@ "@kbn/searchprofiler-plugin": "link:x-pack/plugins/searchprofiler", "@kbn/security-plugin": "link:x-pack/plugins/security", "@kbn/security-solution-ess": "link:x-pack/plugins/security_solution_ess", + "@kbn/security-solution-features": "link:x-pack/packages/security-solution/features", "@kbn/security-solution-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/security_solution", "@kbn/security-solution-navigation": "link:x-pack/packages/security-solution/navigation", "@kbn/security-solution-plugin": "link:x-pack/plugins/security_solution", @@ -739,6 +743,10 @@ "@kbn/ui-shared-deps-npm": "link:packages/kbn-ui-shared-deps-npm", "@kbn/ui-shared-deps-src": "link:packages/kbn-ui-shared-deps-src", "@kbn/ui-theme": "link:packages/kbn-ui-theme", + "@kbn/unified-data-table": "link:packages/kbn-unified-data-table", + "@kbn/unified-doc-viewer": "link:packages/kbn-unified-doc-viewer", + "@kbn/unified-doc-viewer-examples": "link:examples/unified_doc_viewer", + "@kbn/unified-doc-viewer-plugin": "link:src/plugins/unified_doc_viewer", "@kbn/unified-field-list": "link:packages/kbn-unified-field-list", "@kbn/unified-field-list-examples-plugin": "link:examples/unified_field_list_examples", "@kbn/unified-histogram-plugin": "link:src/plugins/unified_histogram", @@ -833,6 +841,7 @@ "core-js": "^3.31.0", "cronstrue": "^1.51.0", "css-box-model": "^1.2.1", + "css.escape": "^1.5.1", "cuid": "^2.1.8", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", @@ -1006,6 +1015,7 @@ "suricata-sid-db": "^1.0.2", "symbol-observable": "^1.2.0", "tar": "^6.1.15", + "textarea-caret": "^3.1.0", "tinycolor2": "1.4.1", "tinygradient": "0.4.3", "ts-easing": "^0.2.0", @@ -1066,6 +1076,7 @@ "@elastic/synthetics": "^1.3.0", "@emotion/babel-preset-css-prop": "^11.11.0", "@emotion/jest": "^11.11.0", + "@frsource/cypress-plugin-visual-regression-diff": "^3.3.10", "@istanbuljs/nyc-config-typescript": "^1.0.2", "@istanbuljs/schema": "^0.1.2", "@jest/console": "^29.6.1", @@ -1367,6 +1378,7 @@ "@types/tar": "^6.1.5", "@types/tempy": "^0.2.0", "@types/testing-library__jest-dom": "^5.14.7", + "@types/textarea-caret": "^3.0.1", "@types/tinycolor2": "^1.4.1", "@types/tough-cookie": "^4.0.2", "@types/type-detect": "^4.0.1", diff --git a/packages/core/apps/core-apps-server-internal/index.ts b/packages/core/apps/core-apps-server-internal/index.ts index 7a28e9f9d2f87..3fe3261446dbc 100644 --- a/packages/core/apps/core-apps-server-internal/index.ts +++ b/packages/core/apps/core-apps-server-internal/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -export { CoreAppsService } from './src'; +export { CoreAppsService, config } from './src'; export type { + CoreAppConfigType, InternalCoreAppsServiceRequestHandlerContext, InternalCoreAppsServiceRouter, } from './src'; diff --git a/packages/core/apps/core-apps-server-internal/src/core_app.test.ts b/packages/core/apps/core-apps-server-internal/src/core_app.test.ts index 13122b4b09eb7..f16abb781bbfb 100644 --- a/packages/core/apps/core-apps-server-internal/src/core_app.test.ts +++ b/packages/core/apps/core-apps-server-internal/src/core_app.test.ts @@ -17,6 +17,7 @@ import { PluginType } from '@kbn/core-base-common'; import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; import { coreInternalLifecycleMock } from '@kbn/core-lifecycle-server-mocks'; import { CoreAppsService } from './core_app'; +import { of } from 'rxjs'; const emptyPlugins = (): UiPlugins => ({ internal: new Map(), @@ -56,10 +57,43 @@ describe('CoreApp', () => { registerBundleRoutesMock.mockReset(); }); + describe('`/internal/core/_settings` route', () => { + it('is not registered by default', async () => { + const routerMock = mockRouter.create(); + internalCoreSetup.http.createRouter.mockReturnValue(routerMock); + + const localCoreApp = new CoreAppsService(coreContext); + await localCoreApp.setup(internalCoreSetup, emptyPlugins()); + + expect(routerMock.versioned.put).not.toHaveBeenCalledWith( + expect.objectContaining({ + path: '/internal/core/_settings', + }) + ); + }); + + it('is registered when enabled', async () => { + const routerMock = mockRouter.create(); + internalCoreSetup.http.createRouter.mockReturnValue(routerMock); + + coreContext.configService.atPath.mockReturnValue(of({ allowDynamicConfigOverrides: true })); + const localCoreApp = new CoreAppsService(coreContext); + await localCoreApp.setup(internalCoreSetup, emptyPlugins()); + + expect(routerMock.versioned.put).toHaveBeenCalledWith({ + path: '/internal/core/_settings', + access: 'internal', + options: { + tags: ['access:updateDynamicConfig'], + }, + }); + }); + }); + describe('`/status` route', () => { - it('is registered with `authRequired: false` is the status page is anonymous', () => { + it('is registered with `authRequired: false` is the status page is anonymous', async () => { internalCoreSetup.status.isStatusPageAnonymous.mockReturnValue(true); - coreApp.setup(internalCoreSetup, emptyPlugins()); + await coreApp.setup(internalCoreSetup, emptyPlugins()); expect(httpResourcesRegistrar.register).toHaveBeenCalledWith( { @@ -73,9 +107,9 @@ describe('CoreApp', () => { ); }); - it('is registered with `authRequired: true` is the status page is not anonymous', () => { + it('is registered with `authRequired: true` is the status page is not anonymous', async () => { internalCoreSetup.status.isStatusPageAnonymous.mockReturnValue(false); - coreApp.setup(internalCoreSetup, emptyPlugins()); + await coreApp.setup(internalCoreSetup, emptyPlugins()); expect(httpResourcesRegistrar.register).toHaveBeenCalledWith( { @@ -185,8 +219,8 @@ describe('CoreApp', () => { }); describe('`/app/{id}/{any*}` route', () => { - it('is registered with the correct parameters', () => { - coreApp.setup(internalCoreSetup, emptyPlugins()); + it('is registered with the correct parameters', async () => { + await coreApp.setup(internalCoreSetup, emptyPlugins()); expect(httpResourcesRegistrar.register).toHaveBeenCalledWith( { @@ -201,9 +235,9 @@ describe('CoreApp', () => { }); }); - it('`setup` calls `registerBundleRoutes` with the correct options', () => { + it('`setup` calls `registerBundleRoutes` with the correct options', async () => { const uiPlugins = emptyPlugins(); - coreApp.setup(internalCoreSetup, uiPlugins); + await coreApp.setup(internalCoreSetup, uiPlugins); expect(registerBundleRoutesMock).toHaveBeenCalledTimes(1); expect(registerBundleRoutesMock).toHaveBeenCalledWith({ diff --git a/packages/core/apps/core-apps-server-internal/src/core_app.ts b/packages/core/apps/core-apps-server-internal/src/core_app.ts index 6ef61c8571c6f..4d303d24d37a1 100644 --- a/packages/core/apps/core-apps-server-internal/src/core_app.ts +++ b/packages/core/apps/core-apps-server-internal/src/core_app.ts @@ -7,8 +7,8 @@ */ import { stringify } from 'querystring'; -import { Env } from '@kbn/config'; -import { schema } from '@kbn/config-schema'; +import { Env, IConfigService } from '@kbn/config'; +import { schema, ValidationError } from '@kbn/config-schema'; import { fromRoot } from '@kbn/repo-info'; import type { Logger } from '@kbn/logging'; import type { CoreContext } from '@kbn/core-base-server-internal'; @@ -22,6 +22,8 @@ import type { import type { UiPlugins } from '@kbn/core-plugins-base-server-internal'; import type { HttpResources, HttpResourcesServiceToolkit } from '@kbn/core-http-resources-server'; import type { InternalCorePreboot, InternalCoreSetup } from '@kbn/core-lifecycle-server-internal'; +import { firstValueFrom, map, type Observable } from 'rxjs'; +import { CoreAppConfig, type CoreAppConfigType, CoreAppPath } from './core_app_config'; import { registerBundleRoutes } from './bundle_routes'; import type { InternalCoreAppsServiceRequestHandlerContext } from './internal_types'; @@ -41,10 +43,16 @@ interface CommonRoutesParams { export class CoreAppsService { private readonly logger: Logger; private readonly env: Env; + private readonly configService: IConfigService; + private readonly config$: Observable; constructor(core: CoreContext) { this.logger = core.logger.get('core-app'); this.env = core.env; + this.configService = core.configService; + this.config$ = this.configService + .atPath(CoreAppPath) + .pipe(map((rawCfg) => new CoreAppConfig(rawCfg))); } preboot(corePreboot: InternalCorePreboot, uiPlugins: UiPlugins) { @@ -57,9 +65,10 @@ export class CoreAppsService { } } - setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) { + async setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) { this.logger.debug('Setting up core app.'); - this.registerDefaultRoutes(coreSetup, uiPlugins); + const config = await firstValueFrom(this.config$); + this.registerDefaultRoutes(coreSetup, uiPlugins, config); this.registerStaticDirs(coreSetup); } @@ -88,7 +97,11 @@ export class CoreAppsService { }); } - private registerDefaultRoutes(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) { + private registerDefaultRoutes( + coreSetup: InternalCoreSetup, + uiPlugins: UiPlugins, + config: CoreAppConfig + ) { const httpSetup = coreSetup.http; const router = httpSetup.createRouter(''); const resources = coreSetup.httpResources.createRegistrar(router); @@ -147,6 +160,51 @@ export class CoreAppsService { } } ); + + if (config.allowDynamicConfigOverrides) { + this.registerInternalCoreSettingsRoute(router); + } + } + + /** + * Registers the HTTP API that allows updating in-memory the settings that opted-in to be dynamically updatable. + * @param router {@link IRouter} + * @private + */ + private registerInternalCoreSettingsRoute(router: IRouter) { + router.versioned + .put({ + path: '/internal/core/_settings', + access: 'internal', + options: { + tags: ['access:updateDynamicConfig'], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: schema.recordOf(schema.string(), schema.any()), + }, + response: { + '200': { body: schema.object({ ok: schema.boolean() }) }, + }, + }, + }, + async (context, req, res) => { + try { + this.configService.setDynamicConfigOverrides(req.body); + } catch (err) { + if (err instanceof ValidationError) { + return res.badRequest({ body: err }); + } + throw err; + } + + return res.ok({ body: { ok: true } }); + } + ); } private registerCommonDefaultRoutes({ diff --git a/packages/core/apps/core-apps-server-internal/src/core_app_config.test.ts b/packages/core/apps/core-apps-server-internal/src/core_app_config.test.ts new file mode 100644 index 0000000000000..2ac60e19fd637 --- /dev/null +++ b/packages/core/apps/core-apps-server-internal/src/core_app_config.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { config, CoreAppConfig } from './core_app_config'; + +describe('CoreApp Config', () => { + test('set correct defaults', () => { + const configValue = new CoreAppConfig(config.schema.validate({})); + expect(configValue).toMatchInlineSnapshot(` + CoreAppConfig { + "allowDynamicConfigOverrides": false, + } + `); + }); +}); diff --git a/packages/core/apps/core-apps-server-internal/src/core_app_config.ts b/packages/core/apps/core-apps-server-internal/src/core_app_config.ts new file mode 100644 index 0000000000000..d98a053433683 --- /dev/null +++ b/packages/core/apps/core-apps-server-internal/src/core_app_config.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema, type TypeOf } from '@kbn/config-schema'; +import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal'; + +/** + * Validation schema for Core App config. + * @public + */ +export const configSchema = schema.object({ + allowDynamicConfigOverrides: schema.boolean({ defaultValue: false }), +}); + +export type CoreAppConfigType = TypeOf; + +export const CoreAppPath = 'coreApp'; + +export const config: ServiceConfigDescriptor = { + path: CoreAppPath, + schema: configSchema, +}; + +/** + * Wrapper of config schema. + * @internal + */ +export class CoreAppConfig implements CoreAppConfigType { + /** + * @internal + * When true, the HTTP API to dynamically extend the configuration is registered. + * + * @remarks + * You should enable this at your own risk: Settings opted-in to being dynamically + * configurable can be changed at any given point, potentially leading to unexpected behaviours. + * This feature is mostly intended for testing purposes. + */ + public readonly allowDynamicConfigOverrides: boolean; + + constructor(rawConfig: CoreAppConfig) { + this.allowDynamicConfigOverrides = rawConfig.allowDynamicConfigOverrides; + } +} diff --git a/packages/core/apps/core-apps-server-internal/src/index.ts b/packages/core/apps/core-apps-server-internal/src/index.ts index d2eca9036f40e..2792538f5f2ba 100644 --- a/packages/core/apps/core-apps-server-internal/src/index.ts +++ b/packages/core/apps/core-apps-server-internal/src/index.ts @@ -7,6 +7,7 @@ */ export { CoreAppsService } from './core_app'; +export { config, type CoreAppConfigType } from './core_app_config'; export type { InternalCoreAppsServiceRequestHandlerContext, InternalCoreAppsServiceRouter, 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 617c810109138..c265a7590d958 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 @@ -199,26 +199,38 @@ export class Router { const setupContract: jest.Mocked = { @@ -27,7 +28,10 @@ const createStartContractMock = () => { return startContract as PluginsServiceSetup; }; -const createPluginInitializerContextMock = (config: unknown = {}) => { +const createPluginInitializerContextMock = ( + config: unknown = {}, + { buildFlavor = 'serverless' }: { buildFlavor?: BuildFlavor } = {} +) => { const mock: PluginInitializerContext = { opaqueId: Symbol(), env: { @@ -43,7 +47,7 @@ const createPluginInitializerContextMock = (config: unknown = {}) => { buildSha: 'buildSha', dist: false, buildDate: new Date('2023-05-15T23:12:09.000Z'), - buildFlavor: 'serverless', + buildFlavor, }, }, logger: loggerMock.create(), diff --git a/packages/core/plugins/core-plugins-browser-mocks/tsconfig.json b/packages/core/plugins/core-plugins-browser-mocks/tsconfig.json index 6b14fa13dd8b5..6413be0063abe 100644 --- a/packages/core/plugins/core-plugins-browser-mocks/tsconfig.json +++ b/packages/core/plugins/core-plugins-browser-mocks/tsconfig.json @@ -16,6 +16,7 @@ "@kbn/logging-mocks", "@kbn/core-plugins-browser-internal", "@kbn/core-plugins-browser", + "@kbn/config", ], "exclude": [ "target/**/*", diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts index 939e34ab0968e..5ca77254e5b50 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts @@ -246,7 +246,6 @@ export function createPluginSetupContext( setSpacesExtension: deps.savedObjects.setSpacesExtension, registerType: deps.savedObjects.registerType, getDefaultIndex: deps.savedObjects.getDefaultIndex, - getAllIndices: deps.savedObjects.getAllIndices, }, status: { core$: deps.status.core$, diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts b/packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts index 280fa38c04344..d6738b4f42394 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts @@ -278,6 +278,14 @@ export class PluginsService implements CoreService value === true) + .map(([key]) => key); + if (configKeys.length > 0) { + this.coreContext.configService.addDynamicConfigPaths(plugin.configPath, configKeys); + } + } this.coreContext.configService.setSchema(plugin.configPath, configDescriptor.schema); } } diff --git a/packages/core/plugins/core-plugins-server/index.ts b/packages/core/plugins/core-plugins-server/index.ts index 47aa0d04ac87c..0c80b60c3d111 100644 --- a/packages/core/plugins/core-plugins-server/index.ts +++ b/packages/core/plugins/core-plugins-server/index.ts @@ -18,6 +18,7 @@ export type { SharedGlobalConfig, MakeUsageFromSchema, ExposedToBrowserDescriptor, + DynamicConfigDescriptor, } from './src'; export { SharedGlobalConfigKeys } from './src'; diff --git a/packages/core/plugins/core-plugins-server/src/index.ts b/packages/core/plugins/core-plugins-server/src/index.ts index 94ad27dedbf12..322a8b5a13c09 100644 --- a/packages/core/plugins/core-plugins-server/src/index.ts +++ b/packages/core/plugins/core-plugins-server/src/index.ts @@ -18,6 +18,7 @@ export type { SharedGlobalConfig, MakeUsageFromSchema, ExposedToBrowserDescriptor, + DynamicConfigDescriptor, } from './types'; export { SharedGlobalConfigKeys } from './shared_global_config'; diff --git a/packages/core/plugins/core-plugins-server/src/types.ts b/packages/core/plugins/core-plugins-server/src/types.ts index 46773971d35ef..207df71df3279 100644 --- a/packages/core/plugins/core-plugins-server/src/types.ts +++ b/packages/core/plugins/core-plugins-server/src/types.ts @@ -34,7 +34,7 @@ export type PluginConfigSchema = Type; /** * Type defining the list of configuration properties that will be exposed on the client-side - * Object properties can either be fully exposed + * Object properties can either be fully exposed or narrowed down to specific keys. * * @public */ @@ -49,6 +49,23 @@ export type ExposedToBrowserDescriptor = { boolean; }; +/** + * Type defining the list of configuration properties that can be dynamically updated + * Object properties can either be fully exposed or narrowed down to specific keys. + * + * @public + */ +export type DynamicConfigDescriptor = { + [Key in keyof T]?: T[Key] extends Maybe + ? // handles arrays as primitive values + boolean + : T[Key] extends Maybe + ? // can be nested for objects + DynamicConfigDescriptor | boolean + : // primitives + boolean; +}; + /** * Describes a plugin configuration properties. * @@ -88,6 +105,10 @@ export interface PluginConfigDescriptor { * List of configuration properties that will be available on the client-side plugin. */ exposeToBrowser?: ExposedToBrowserDescriptor; + /** + * List of configuration properties that can be dynamically changed via the PUT /_settings API. + */ + dynamicConfig?: DynamicConfigDescriptor; /** * Schema to use to validate the plugin configuration. * diff --git a/packages/core/root/core-root-server-internal/src/register_service_config.ts b/packages/core/root/core-root-server-internal/src/register_service_config.ts index f646f9e538ae8..ccb6a745b6754 100644 --- a/packages/core/root/core-root-server-internal/src/register_service_config.ts +++ b/packages/core/root/core-root-server-internal/src/register_service_config.ts @@ -16,6 +16,7 @@ import { pidConfig } from '@kbn/core-environment-server-internal'; import { executionContextConfig } from '@kbn/core-execution-context-server-internal'; import { config as httpConfig, cspConfig, externalUrlConfig } from '@kbn/core-http-server-internal'; import { config as elasticsearchConfig } from '@kbn/core-elasticsearch-server-internal'; +import { config as coreAppConfig } from '@kbn/core-apps-server-internal'; import { opsConfig } from '@kbn/core-metrics-server-internal'; import { savedObjectsConfig, @@ -37,6 +38,7 @@ export function registerServiceConfig(configService: ConfigService) { cspConfig, deprecationConfig, elasticsearchConfig, + coreAppConfig, elasticApmConfig, executionContextConfig, externalUrlConfig, diff --git a/packages/core/root/core-root-server-internal/src/server.ts b/packages/core/root/core-root-server-internal/src/server.ts index 906e4bfbe7bf8..8f8a7f185474a 100644 --- a/packages/core/root/core-root-server-internal/src/server.ts +++ b/packages/core/root/core-root-server-internal/src/server.ts @@ -350,7 +350,7 @@ export class Server { this.#pluginsInitialized = pluginsSetup.initialized; this.registerCoreContext(coreSetup); - this.coreApp.setup(coreSetup, uiPlugins); + await this.coreApp.setup(coreSetup, uiPlugins); setupTransaction?.end(); this.uptimePerStep.setup = { start: setupStartUptime, end: performance.now() }; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts index 9989b4264f28f..e7bd523c0e741 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts @@ -23,6 +23,10 @@ export { type IPreflightCheckHelper, type PreflightCheckNamespacesParams, type PreflightCheckNamespacesResult, + type PreflightDocParams, + type PreflightDocResult, + type PreflightNSParams, + type PreflightNSResult, } from './preflight_check'; export interface RepositoryHelpers { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts index 5cf2bfbf21d99..1e39761d99d6e 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts @@ -117,7 +117,6 @@ export class PreflightCheckHelper { if (!this.registry.isMultiNamespace(type)) { throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); } - const { body, statusCode, headers } = await this.client.get( { id: this.serializer.generateRawId(undefined, type, id), @@ -151,8 +150,83 @@ export class PreflightCheckHelper { }; } + /** + * Pre-flight check fetching the document regardless of its namespace type for update. + */ + public async preflightGetDocForUpdate({ + type, + id, + namespace, + }: PreflightDocParams): Promise { + const { statusCode, body, headers } = await this.client.get( + { + id: this.serializer.generateRawId(namespace, type, id), + index: this.getIndexForType(type), + }, + { ignore: [404], meta: true } + ); + + // checking if the 404 is from Elasticsearch + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } + + const indexFound = statusCode !== 404; + if (indexFound && isFoundGetResponse(body)) { + return { + checkDocFound: 'found', + rawDocSource: body, + }; + } + + return { + checkDocFound: 'not_found', + }; + } + + /** + * Pre-flight check to ensure that a multi-namespace object exists in the current namespace for update API. + */ + public preflightCheckNamespacesForUpdate({ + type, + namespace, + initialNamespaces, + preflightDocResult, + }: PreflightNSParams): PreflightNSResult { + const { checkDocFound, rawDocSource } = preflightDocResult; + if (!this.registry.isMultiNamespace(type)) { + return { + checkSkipped: true, + }; + } + + const namespaces = initialNamespaces ?? [SavedObjectsUtils.namespaceIdToString(namespace)]; + + if (checkDocFound === 'found' && rawDocSource !== undefined) { + if (!rawDocExistsInNamespaces(this.registry, rawDocSource, namespaces)) { + return { checkResult: 'found_outside_namespace', checkSkipped: false }; + } + return { + checkResult: 'found_in_namespace', + savedObjectNamespaces: + initialNamespaces ?? getSavedObjectNamespaces(namespace, rawDocSource), + rawDocSource, + checkSkipped: false, + }; + } + + return { + checkResult: 'not_found', + savedObjectNamespaces: initialNamespaces ?? getSavedObjectNamespaces(namespace), + checkSkipped: false, + }; + } + /** * Pre-flight check to ensure that an upsert which would create a new object does not result in an alias conflict. + * + * If an upsert would result in the creation of a new object, we need to check for alias conflicts too. + * This takes an extra round trip to Elasticsearch, but this won't happen often. */ public async preflightCheckForUpsertAliasConflict( type: string, @@ -189,6 +263,39 @@ export interface PreflightCheckNamespacesParams { initialNamespaces?: string[]; } +/** + * @internal + */ +export interface PreflightNSParams { + /** The object type to fetch */ + type: string; + /** The object ID to fetch */ + id: string; + /** The current space */ + namespace: string | undefined; + /** Optional; for an object that is being created, this specifies the initial namespace(s) it will exist in (overriding the current space) */ + initialNamespaces?: string[]; + /** Optional; for a pre-fetched object */ + preflightDocResult: PreflightDocResult; +} + +/** + * @internal + */ +export interface PreflightNSResult { + /** If the object exists, and whether or not it exists in the current space */ + checkResult?: 'not_found' | 'found_in_namespace' | 'found_outside_namespace'; + /** + * What namespace(s) the object should exist in, if it needs to be created; practically speaking, this will never be undefined if + * checkResult == not_found or checkResult == found_in_namespace + */ + savedObjectNamespaces?: string[]; + /** The source of the raw document, if the object already exists */ + rawDocSource?: GetResponseFound; + /** Indicates if the namespaces check is called or not. Non-multinamespace types are not shareable */ + checkSkipped?: boolean; +} + /** * @internal */ @@ -203,3 +310,30 @@ export interface PreflightCheckNamespacesResult { /** The source of the raw document, if the object already exists */ rawDocSource?: GetResponseFound; } + +/** + * @internal + */ +export interface PreflightDocParams { + /** The object type to fetch */ + type: string; + /** The object ID to fetch */ + id: string; + /** The current space */ + namespace: string | undefined; + /** + * optional migration version compatibility. + * {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} + */ + migrationVersionCompatibility?: 'compatible' | 'raw'; +} + +/** + * @internal + */ +export interface PreflightDocResult { + /** If the object exists, and whether or not it exists in the current space */ + checkDocFound: 'not_found' | 'found'; + /** The source of the raw document, if the object already exists in the server's version (unsafe to use) */ + rawDocSource?: GetResponseFound; +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.test.ts new file mode 100644 index 0000000000000..dd5c51c6b433d --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.test.ts @@ -0,0 +1,741 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/no-shadow */ + +import { mockGetCurrentTime, mockPreflightCheckForCreate } from '../repository.test.mock'; + +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + type SavedObjectUnsanitizedDoc, + type SavedObjectReference, + SavedObjectsRawDocSource, + SavedObjectsErrorHelpers, +} from '@kbn/core-saved-objects-server'; +import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; +import { SavedObjectsRepository } from '../repository'; +import { loggerMock } from '@kbn/logging-mocks'; +import { + SavedObjectsSerializer, + encodeHitVersion, +} from '@kbn/core-saved-objects-base-server-internal'; +import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; +import { kibanaMigratorMock } from '../../mocks'; +import { + NAMESPACE_AGNOSTIC_TYPE, + MULTI_NAMESPACE_ISOLATED_TYPE, + HIDDEN_TYPE, + mockVersionProps, + mockTimestampFields, + mockTimestamp, + mappings, + mockVersion, + createRegistry, + createDocumentMigrator, + getMockGetResponse, + createSpySerializer, + createBadRequestErrorPayload, + createConflictErrorPayload, + createGenericNotFoundErrorPayload, + updateSuccess, +} from '../../test_helpers/repository.test.common'; + +describe('SavedObjectsRepository', () => { + let client: ReturnType; + let repository: SavedObjectsRepository; + let migrator: ReturnType; + let logger: ReturnType; + let serializer: jest.Mocked; + + const registry = createRegistry(); + const documentMigrator = createDocumentMigrator(registry); + + const expectMigrationArgs = (args: unknown, contains = true, n = 1) => { + const obj = contains ? expect.objectContaining(args) : expect.not.objectContaining(args); + expect(migrator.migrateDocument).toHaveBeenNthCalledWith( + n, + obj, + expect.objectContaining({ + allowDowngrade: expect.any(Boolean), + }) + ); + }; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticsearchClient(); + migrator = kibanaMigratorMock.create(); + documentMigrator.prepareMigrations(); + migrator.migrateDocument = jest.fn().mockImplementation(documentMigrator.migrate); + migrator.runMigrations = jest.fn().mockResolvedValue([{ status: 'skipped' }]); + logger = loggerMock.create(); + + // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation + serializer = createSpySerializer(registry); + + const allTypes = registry.getAllTypes().map((type) => type.name); + const allowedTypes = [...new Set(allTypes.filter((type) => !registry.isHidden(type)))]; + + // @ts-expect-error must use the private constructor to use the mocked serializer + repository = new SavedObjectsRepository({ + index: '.kibana-test', + mappings, + client, + migrator, + typeRegistry: registry, + serializer, + allowedTypes, + logger, + }); + + mockGetCurrentTime.mockReturnValue(mockTimestamp); + }); + + describe('#update', () => { + const id = 'logstash-*'; + const type = 'index-pattern'; + const attributes = { title: 'Testing' }; + const namespace = 'foo-namespace'; + const references = [ + { + name: 'ref_0', + type: 'test', + id: '1', + }, + ]; + const originId = 'some-origin-id'; + const mockMigrationVersion = { foo: '2.3.4' }; + const mockMigrateDocumentForUpdate = (doc: SavedObjectUnsanitizedDoc) => { + const response = { + ...doc, + attributes: { + ...doc.attributes, + ...(doc.attributes?.title && { title: `${doc.attributes.title}!!` }), + }, + migrationVersion: mockMigrationVersion, + managed: doc.managed ?? false, + references: doc.references || [ + { + name: 'ref_0', + type: 'test', + id: '1', + }, + ], + }; + return response; + }; + + beforeEach(() => { + mockPreflightCheckForCreate.mockReset(); + mockPreflightCheckForCreate.mockImplementation(({ objects }) => { + return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default + }); + client.create.mockResponseImplementation((params) => { + return { + body: { + _id: params.id, + ...mockVersionProps, + } as estypes.CreateResponse, + }; + }); + }); + + describe('client calls', () => { + it(`should use the ES get action then index action when type is not multi-namespace for existing objects`, async () => { + const type = 'index-pattern'; + const id = 'logstash-*'; + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess(client, repository, registry, type, id, attributes, { namespace }); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.index).toHaveBeenCalledTimes(1); + }); + + it(`should use the ES get action then index action when type is multi-namespace for existing objects`, async () => { + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess( + client, + repository, + registry, + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes + ); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.index).toHaveBeenCalledTimes(1); + }); + + it(`should use the ES get action then index action when type is namespace agnostic for existing objects`, async () => { + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess(client, repository, registry, NAMESPACE_AGNOSTIC_TYPE, id, attributes); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.index).toHaveBeenCalledTimes(1); + }); + + it(`should check for alias conflicts if a new multi-namespace object before create action would be created then create action to create the object`, async () => { + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess( + client, + repository, + registry, + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes, + { upsert: true }, + { mockGetResponseAsNotFound: { found: false } as estypes.GetResponse } + ); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.create).toHaveBeenCalledTimes(1); + }); + + it(`defaults to empty array with no input references`, async () => { + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess(client, repository, registry, type, id, attributes); + expect( + (client.index.mock.calls[0][0] as estypes.CreateRequest).body! + .references + ).toEqual([]); // we're indexing a full new doc, serializer adds default if not defined + }); + + it(`accepts custom references array 1`, async () => { + const test = async (references: SavedObjectReference[]) => { + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess(client, repository, registry, type, id, attributes, { + references, + }); + expect( + (client.index.mock.calls[0][0] as estypes.CreateRequest).body! + .references + ).toEqual(references); + client.index.mockClear(); + }; + await test(references); + }); + it(`accepts custom references array 2`, async () => { + const test = async (references: SavedObjectReference[]) => { + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess(client, repository, registry, type, id, attributes, { + references, + }); + expect( + (client.index.mock.calls[0][0] as estypes.CreateRequest).body! + .references + ).toEqual(references); + client.index.mockClear(); + }; + await test([{ type: 'foo', id: '42', name: 'some ref' }]); + }); + it(`accepts custom references array 3`, async () => { + const test = async (references: SavedObjectReference[]) => { + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess(client, repository, registry, type, id, attributes, { + references, + }); + expect( + (client.index.mock.calls[0][0] as estypes.CreateRequest).body! + .references + ).toEqual(references); + client.index.mockClear(); + }; + await test([]); + }); + + it(`uses the 'upsertAttributes' option when specified for a single-namespace type that does not exist`, async () => { + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess( + client, + repository, + registry, + type, + id, + attributes, + { + upsert: { + title: 'foo', + description: 'bar', + }, + }, + { mockGetResponseAsNotFound: { found: false } as estypes.GetResponse } + ); + + const expected = { + 'index-pattern': { description: 'bar', title: 'foo' }, + type: 'index-pattern', + ...mockTimestampFields, + }; + expect( + (client.create.mock.calls[0][0] as estypes.CreateRequest).body! + ).toEqual(expected); + }); + + it(`uses the 'upsertAttributes' option when specified for a multi-namespace type that does not exist`, async () => { + const options = { upsert: { title: 'foo', description: 'bar' } }; + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess( + client, + repository, + registry, + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes, + { + upsert: { + title: 'foo', + description: 'bar', + }, + }, + { + mockGetResponseAsNotFound: { found: false } as estypes.GetResponse, + } + ); + await repository.update(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); + expect(client.get).toHaveBeenCalledTimes(2); + const expectedType = { + multiNamespaceIsolatedType: { description: 'bar', title: 'foo' }, + namespaces: ['default'], + type: 'multiNamespaceIsolatedType', + ...mockTimestampFields, + }; + expect( + (client.create.mock.calls[0][0] as estypes.CreateRequest).body! + ).toEqual(expectedType); + }); + + it(`ignores the 'upsertAttributes' option when specified for a multi-namespace type that already exists`, async () => { + // attributes don't change + const options = { upsert: { title: 'foo', description: 'bar' } }; + migrator.migrateDocument.mockImplementation((doc) => ({ ...doc, migrated: true })); + await updateSuccess( + client, + repository, + registry, + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes, + options + ); + await repository.update(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, + index: '.kibana-test_8.0.0-testing', + refresh: 'wait_for', + require_alias: true, + body: expect.objectContaining({ + multiNamespaceIsolatedType: { title: 'Testing' }, + namespaces: ['default'], + references: [], + type: 'multiNamespaceIsolatedType', + ...mockTimestampFields, + }), + }), + expect.anything() + ); + }); + + it(`doesn't accept custom references if not an array`, async () => { + const test = async (references: unknown) => { + migrator.migrateDocument.mockImplementation(mockMigrateDocumentForUpdate); + await updateSuccess(client, repository, registry, type, id, attributes, { + // @ts-expect-error references is unknown + references, + }); + expect( + (client.index.mock.calls[0][0] as estypes.CreateRequest).body! + .references + ).toEqual([]); + client.index.mockClear(); + client.create.mockClear(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + migrator.migrateDocument.mockImplementation(mockMigrateDocumentForUpdate); + await updateSuccess(client, repository, registry, type, id, { foo: 'bar' }); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ + refresh: 'wait_for', + }), + expect.anything() + ); + }); + + it(`defaults to the version of the existing document when type is multi-namespace`, async () => { + migrator.migrateDocument.mockImplementation(mockMigrateDocumentForUpdate); + await updateSuccess( + client, + repository, + registry, + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes, + { references } + ); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining(versionProperties), + expect.anything() + ); + }); + + it(`accepts version`, async () => { + migrator.migrateDocument.mockImplementation(mockMigrateDocumentForUpdate); + await updateSuccess(client, repository, registry, type, id, attributes, { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + }); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), + expect.anything() + ); + }); + + it('retries the operation in case of conflict error', async () => { + client.get.mockResponse(getMockGetResponse(registry, { type, id })); + + client.index + .mockImplementationOnce(() => { + throw SavedObjectsErrorHelpers.createConflictError(type, id, 'conflict'); + }) + .mockImplementationOnce(() => { + throw SavedObjectsErrorHelpers.createConflictError(type, id, 'conflict'); + }) + .mockResponseImplementation((params) => { + return { + body: { + _id: params.id, + _seq_no: 1, + _primary_term: 1, + }, + } as any; + }); + + await repository.update(type, id, attributes, { retryOnConflict: 3 }); + + expect(client.get).toHaveBeenCalledTimes(3); + expect(client.index).toHaveBeenCalledTimes(3); + }); + + it('retries the operation a maximum of `retryOnConflict` times', async () => { + client.get.mockResponse(getMockGetResponse(registry, { type, id })); + + client.index.mockImplementation(() => { + throw SavedObjectsErrorHelpers.createConflictError(type, id, 'conflict'); + }); + + await expect( + repository.update(type, id, attributes, { retryOnConflict: 3 }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Saved object [index-pattern/logstash-*] conflict"` + ); + + expect(client.get).toHaveBeenCalledTimes(4); + expect(client.index).toHaveBeenCalledTimes(4); + }); + + it('default to a `retry_on_conflict` setting of `0` when `version` is provided', async () => { + client.get.mockResponse(getMockGetResponse(registry, { type, id })); + + client.index.mockImplementation(() => { + throw SavedObjectsErrorHelpers.createConflictError(type, id, 'conflict'); + }); + + await expect( + repository.update(type, id, attributes, { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Saved object [index-pattern/logstash-*] conflict"` + ); + + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledTimes(1); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await updateSuccess(client, repository, registry, type, id, attributes, { namespace }); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${namespace}:${type}:${id}`) }), // namespace expected: globalType + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await updateSuccess(client, repository, registry, type, id, attributes, { references }); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), + expect.anything() + ); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await updateSuccess(client, repository, registry, type, id, attributes, { + references, + namespace: 'default', + }); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when using agnostic-namespace type`, async () => { + await updateSuccess(client, repository, registry, NAMESPACE_AGNOSTIC_TYPE, id, attributes, { + namespace, + }); + + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.stringMatching(`${NAMESPACE_AGNOSTIC_TYPE}:${id}`), + }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when using multi-namespace type`, async () => { + await updateSuccess( + client, + repository, + registry, + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes, + { namespace } + ); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.stringMatching(`${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`), + }), + expect.anything() + ); + }); + }); + + describe('errors', () => { + const expectNotFoundError = async (type: string, id: string) => { + await expect( + repository.update(type, id, {}, { migrationVersionCompatibility: 'raw' }) + ).rejects.toThrowError(createGenericNotFoundErrorPayload(type, id)); + }; + + it(`throws when options.namespace is '*'`, async () => { + await expect( + repository.update(type, id, attributes, { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestErrorPayload('"options.namespace" cannot be "*"')); + }); + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(client.index).not.toHaveBeenCalled(); + }); + + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(client.index).not.toHaveBeenCalled(); + }); + + it(`throws when id is empty`, async () => { + await expect(repository.update(type, '', attributes)).rejects.toThrowError( + createBadRequestErrorPayload('id cannot be empty') + ); + expect(client.index).not.toHaveBeenCalled(); + }); + + it(`throws when ES is unable to find the document during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false } as estypes.GetResponse, + undefined + ) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the index during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({} as estypes.GetResponse, { + statusCode: 404, + }) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse( + registry, + { type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, + namespace + ); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when there is an alias conflict from preflightCheckForCreate`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + found: false, + } as estypes.GetResponse) + ); + mockPreflightCheckForCreate.mockResolvedValue([ + { type: 'type', id: 'id', error: { type: 'aliasConflict' } }, + ]); + await expect( + repository.update( + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + { attr: 'value' }, + { + upsert: { + upsertAttr: 'val', + attr: 'value', + }, + } + ) + ).rejects.toThrowError(createConflictErrorPayload(MULTI_NAMESPACE_ISOLATED_TYPE, id)); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.index).not.toHaveBeenCalled(); + }); + + it(`does not throw when there is a different error from preflightCheckForCreate`, async () => { + mockPreflightCheckForCreate.mockResolvedValue([ + { type: 'type', id: 'id', error: { type: 'conflict' } }, + ]); + await updateSuccess( + client, + repository, + registry, + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes, + { upsert: true }, + { mockGetResponseAsNotFound: { found: false } as estypes.GetResponse } + ); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.create).toHaveBeenCalledTimes(1); + }); + + it(`does not throw when the document does not exist`, async () => { + expect(client.create).not.toHaveBeenCalled(); + await expectNotFoundError(type, id); + }); + }); + + describe('migration', () => { + it('migrates the fetched document from get', async () => { + const type = 'index-pattern'; + const id = 'logstash-*'; + migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true })); + await updateSuccess(client, repository, registry, type, id, attributes); + expect(migrator.migrateDocument).toHaveBeenCalledTimes(2); + expectMigrationArgs({ + id, + type, + }); + }); + + it('migrates the input arguments when upsert is used', async () => { + const options = { + upsert: { + title: 'foo', + description: 'bar', + }, + }; + const internalOptions = { + mockGetResponseAsNotFound: { found: false } as estypes.GetResponse, + }; + await updateSuccess( + client, + repository, + registry, + type, + id, + attributes, + options, + internalOptions + ); + expect(migrator.migrateDocument).toHaveBeenCalledTimes(1); + expectMigrationArgs({ + id, + type, + }); + }); + }); + + describe('returns', () => { + it(`returns _seq_no and _primary_term encoded as version`, async () => { + const result = await updateSuccess(client, repository, registry, type, id, attributes, { + namespace, + references, + }); + expect(result).toEqual({ + id, + type, + ...mockTimestampFields, + version: mockVersion, + attributes, + references, + namespaces: [namespace], + }); + }); + + it(`includes namespaces if type is multi-namespace`, async () => { + const result = await updateSuccess( + client, + repository, + registry, + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes + ); + expect(result).toMatchObject({ + namespaces: expect.any(Array), + }); + }); + + it(`includes namespaces if type is not multi-namespace`, async () => { + const result = await updateSuccess(client, repository, registry, type, id, attributes); + expect(result).toMatchObject({ + namespaces: ['default'], + }); + }); + + it(`includes originId property if present in cluster call response`, async () => { + const result = await updateSuccess( + client, + repository, + registry, + type, + id, + attributes, + {}, + { originId } + ); + expect(result).toMatchObject({ originId }); + }); + }); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts index 27f3d45c642de..e119d6e6303db 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts @@ -10,19 +10,21 @@ import { SavedObjectsErrorHelpers, type SavedObject, type SavedObjectSanitizedDoc, - SavedObjectsRawDoc, - SavedObjectsRawDocSource, } from '@kbn/core-saved-objects-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; -import { encodeHitVersion } from '@kbn/core-saved-objects-base-server-internal'; import { + decodeRequestVersion, + encodeHitVersion, +} from '@kbn/core-saved-objects-base-server-internal'; +import type { SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from '@kbn/core-saved-objects-api-server'; +import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from '../constants'; -import { getCurrentTime, getExpectedVersionProperties } from './utils'; -import { ApiExecutionContext } from './types'; -import { PreflightCheckNamespacesResult } from './helpers'; +import { isValidRequest } from '../utils'; +import { getCurrentTime, getSavedObjectFromSource, mergeForUpdate } from './utils'; +import type { ApiExecutionContext } from './types'; export interface PerformUpdateParams { type: string; @@ -32,84 +34,141 @@ export interface PerformUpdateParams { } export const performUpdate = async ( + updateParams: PerformUpdateParams, + apiContext: ApiExecutionContext +): Promise> => { + const { type, id, options } = updateParams; + const { allowedTypes, helpers } = apiContext; + const namespace = helpers.common.getCurrentNamespace(options.namespace); + + // check request is valid + const { validRequest, error } = isValidRequest({ allowedTypes, type, id }); + if (!validRequest && error) { + throw error; + } + + const maxAttempts = options.version ? 1 : 1 + DEFAULT_RETRY_COUNT; + + // handle retryOnConflict manually by reattempting the operation in case of conflict errors + let response: SavedObjectsUpdateResponse; + for (let currentAttempt = 1; currentAttempt <= maxAttempts; currentAttempt++) { + try { + response = await executeUpdate(updateParams, apiContext, { namespace }); + break; + } catch (e) { + if ( + SavedObjectsErrorHelpers.isConflictError(e) && + e.retryableConflict && + currentAttempt < maxAttempts + ) { + continue; + } + throw e; + } + } + + return response!; +}; + +export const executeUpdate = async ( { id, type, attributes, options }: PerformUpdateParams, - { - registry, - helpers, - allowedTypes, - client, - serializer, - migrator, - extensions = {}, - }: ApiExecutionContext + { registry, helpers, client, serializer, extensions = {}, logger }: ApiExecutionContext, + { namespace }: { namespace: string | undefined } ): Promise> => { const { common: commonHelper, encryption: encryptionHelper, preflight: preflightHelper, migration: migrationHelper, + validation: validationHelper, } = helpers; const { securityExtension } = extensions; - const namespace = commonHelper.getCurrentNamespace(options.namespace); - if (!allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - if (!id) { - throw SavedObjectsErrorHelpers.createBadRequestError('id cannot be empty'); // prevent potentially upserting a saved object with an empty ID - } - const { version, references, upsert, refresh = DEFAULT_REFRESH_SETTING, - retryOnConflict = version ? 0 : DEFAULT_RETRY_COUNT, + migrationVersionCompatibility, } = options; - let preflightResult: PreflightCheckNamespacesResult | undefined; - if (registry.isMultiNamespace(type)) { - preflightResult = await preflightHelper.preflightCheckNamespaces({ - type, - id, - namespace, - }); - } + // Preflight calls to get the doc and check namespaces for multinamespace types. + const preflightDocResult = await preflightHelper.preflightGetDocForUpdate({ + type, + id, + namespace, + }); - const existingNamespaces = preflightResult?.savedObjectNamespaces ?? []; + const preflightDocNSResult = preflightHelper.preflightCheckNamespacesForUpdate({ + type, + id, + namespace, + preflightDocResult, + }); + const existingNamespaces = preflightDocNSResult?.savedObjectNamespaces ?? []; const authorizationResult = await securityExtension?.authorizeUpdate({ namespace, object: { type, id, existingNamespaces }, }); - if ( - preflightResult?.checkResult === 'found_outside_namespace' || - (!upsert && preflightResult?.checkResult === 'not_found') - ) { + // validate if an update (directly update or create the object instead) can be done, based on if the doc exists or not + const docOutsideNamespace = preflightDocNSResult?.checkResult === 'found_outside_namespace'; + const docNotFound = + preflightDocNSResult?.checkResult === 'not_found' || + preflightDocResult.checkDocFound === 'not_found'; + + // doc not in namespace, or doc not found but we're not upserting => throw 404 + if (docOutsideNamespace || (docNotFound && !upsert)) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - if (upsert && preflightResult?.checkResult === 'not_found') { - // If an upsert would result in the creation of a new object, we need to check for alias conflicts too. - // This takes an extra round trip to Elasticsearch, but this won't happen often. - // TODO: improve performance by combining these into a single preflight check + + if (upsert && preflightDocNSResult?.checkResult === 'not_found') { + // we only need to check multi-namespace objects. Single and agnostic types do not have aliases. + // throws SavedObjectsErrorHelpers.createConflictError(type, id) if there is one await preflightHelper.preflightCheckForUpsertAliasConflict(type, id, namespace); } + + // migrate the existing doc to the current version + let migrated: SavedObject; + if (preflightDocResult.checkDocFound === 'found') { + const document = getSavedObjectFromSource( + registry, + type, + id, + preflightDocResult.rawDocSource!, + { migrationVersionCompatibility } + ); + try { + migrated = migrationHelper.migrateStorageDocument(document) as SavedObject; + } catch (migrateStorageDocError) { + throw SavedObjectsErrorHelpers.decorateGeneralError( + migrateStorageDocError, + 'Failed to migrate document to the latest version.' + ); + } + } + // END ALL PRE_CLIENT CALL CHECKS && MIGRATE EXISTING DOC; + const time = getCurrentTime(); + let updatedOrCreatedSavedObject: SavedObject; + // `upsert` option set and document was not found -> we need to perform an upsert operation + const shouldPerformUpsert = upsert && docNotFound; - let rawUpsert: SavedObjectsRawDoc | undefined; - // don't include upsert if the object already exists; ES doesn't allow upsert in combination with version properties - if (upsert && (!preflightResult || preflightResult.checkResult === 'not_found')) { - let savedObjectNamespace: string | undefined; - let savedObjectNamespaces: string[] | undefined; + let savedObjectNamespace: string | undefined; + let savedObjectNamespaces: string[] | undefined; - if (registry.isSingleNamespace(type) && namespace) { - savedObjectNamespace = namespace; - } else if (registry.isMultiNamespace(type)) { - savedObjectNamespaces = preflightResult!.savedObjectNamespaces; - } + if (namespace && registry.isSingleNamespace(type)) { + savedObjectNamespace = namespace; + } else if (registry.isMultiNamespace(type)) { + savedObjectNamespaces = preflightDocNSResult.savedObjectNamespaces; + } - const migrated = migrationHelper.migrateInputDocument({ + // UPSERT CASE START + if (shouldPerformUpsert) { + // ignore attributes if creating a new doc: only use the upsert attributes + // don't include upsert if the object already exists; ES doesn't allow upsert in combination with version properties + const migratedUpsert = migrationHelper.migrateInputDocument({ id, type, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), @@ -118,31 +177,111 @@ export const performUpdate = async ( ...(await encryptionHelper.optionallyEncryptAttributes(type, id, namespace, upsert)), }, updated_at: time, + ...(Array.isArray(references) && { references }), + }) as SavedObjectSanitizedDoc; + validationHelper.validateObjectForCreate(type, migratedUpsert); + const rawUpsert = serializer.savedObjectToRaw(migratedUpsert); + + const createRequestParams = { + id: rawUpsert._id, + index: commonHelper.getIndexForType(type), + refresh, + body: rawUpsert._source, + ...(version ? decodeRequestVersion(version) : {}), + require_alias: true, + }; + + const { + body: createDocResponseBody, + statusCode, + headers, + } = await client.create(createRequestParams, { meta: true }).catch((err) => { + if (SavedObjectsErrorHelpers.isEsUnavailableError(err)) { + throw err; + } + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + if (SavedObjectsErrorHelpers.isConflictError(err)) { + // flag the error as being caused by an update conflict + err.retryableConflict = true; + } + throw err; }); - rawUpsert = serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - } + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type); + } + // client.create doesn't return the index document. + // Use rawUpsert as the _source + const upsertedSavedObject = serializer.rawToSavedObject( + { + ...rawUpsert, + ...createDocResponseBody, + }, + { migrationVersionCompatibility } + ); + const { originId } = upsertedSavedObject ?? {}; + let namespaces: string[] = []; + if (!registry.isNamespaceAgnostic(type)) { + namespaces = upsertedSavedObject.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(upsertedSavedObject.namespace), + ]; + } - const doc = { - [type]: await encryptionHelper.optionallyEncryptAttributes(type, id, namespace, attributes), - updated_at: time, - ...(Array.isArray(references) && { references }), - }; + updatedOrCreatedSavedObject = { + id, + type, + updated_at: time, + version: encodeHitVersion(createDocResponseBody), + namespaces, + ...(originId && { originId }), + references, + attributes: upsert, // these ignore the attribute values provided in the main request body. + } as SavedObject; - const body = await client - .update({ - id: serializer.generateRawId(namespace, type, id), + // UPSERT CASE END + } else { + // UPDATE CASE START + // at this point, we already know 1. the document exists 2. we're not doing an upsert + // therefor we can safely process with the "standard" update sequence. + + const updatedAttributes = mergeForUpdate( + { ...migrated!.attributes }, + await encryptionHelper.optionallyEncryptAttributes(type, id, namespace, attributes) + ); + const migratedUpdatedSavedObjectDoc = migrationHelper.migrateInputDocument({ + ...migrated!, + id, + type, + // need to override the redacted NS values from the decrypted/migrated document + namespace: savedObjectNamespace, + namespaces: savedObjectNamespaces, + attributes: updatedAttributes, + updated_at: time, + ...(Array.isArray(references) && { references }), + }); + + const docToSend = serializer.savedObjectToRaw( + migratedUpdatedSavedObjectDoc as SavedObjectSanitizedDoc + ); + + // implement creating the call params + const indexRequestParams = { + id: docToSend._id, index: commonHelper.getIndexForType(type), - ...getExpectedVersionProperties(version), refresh, - retry_on_conflict: retryOnConflict, - body: { - doc, - ...(rawUpsert && { upsert: rawUpsert._source }), - }, - _source_includes: ['namespace', 'namespaces', 'originId'], + body: docToSend._source, + // using version from the source doc if not provided as option to avoid erasing changes in case of concurrent calls + ...decodeRequestVersion(version || migrated!.version), require_alias: true, - }) - .catch((err) => { + }; + + const { + body: indexDocResponseBody, + statusCode, + headers, + } = await client.index(indexRequestParams, { meta: true }).catch((err) => { if (SavedObjectsErrorHelpers.isEsUnavailableError(err)) { throw err; } @@ -150,31 +289,50 @@ export const performUpdate = async ( // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } + if (SavedObjectsErrorHelpers.isConflictError(err)) { + // flag the error as being caused by an update conflict + err.retryableConflict = true; + } throw err; }); + // throw if we can't verify a 404 response is from Elasticsearch + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type); + } + // client.index doesn't return the indexed document. + // Rather than making another round trip to elasticsearch to fetch the doc, we use the SO we sent + // rawToSavedObject adds references as [] if undefined + const updatedSavedObject = serializer.rawToSavedObject( + { + ...docToSend, + ...indexDocResponseBody, + }, + { migrationVersionCompatibility } + ); - const { originId } = body.get?._source ?? {}; - let namespaces: string[] = []; - if (!registry.isNamespaceAgnostic(type)) { - namespaces = body.get?._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(body.get?._source.namespace), - ]; - } + const { originId } = updatedSavedObject ?? {}; + let namespaces: string[] = []; + if (!registry.isNamespaceAgnostic(type)) { + namespaces = updatedSavedObject.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(updatedSavedObject.namespace), + ]; + } - const result = { - id, - type, - updated_at: time, - version: encodeHitVersion(body), - namespaces, - ...(originId && { originId }), - references, - attributes, - } as SavedObject; + updatedOrCreatedSavedObject = { + id, + type, + updated_at: time, + version: encodeHitVersion(indexDocResponseBody), + namespaces, + ...(originId && { originId }), + references, + attributes, + } as SavedObject; + } return encryptionHelper.optionallyDecryptAndRedactSingleResult( - result, + updatedOrCreatedSavedObject!, authorizationResult?.typeMap, - attributes + shouldPerformUpsert ? upsert : attributes ); }; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts index f3562dffb1e86..575b29d47ca0f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts @@ -23,3 +23,4 @@ export { type GetSavedObjectFromSourceOptions, } from './internal_utils'; export { type Left, type Either, type Right, isLeft, isRight, left, right } from './either'; +export { mergeForUpdate } from './merge_for_update'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/merge_for_update.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/merge_for_update.test.ts new file mode 100644 index 0000000000000..7d859f374a5e2 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/merge_for_update.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mergeForUpdate } from './merge_for_update'; + +describe('mergeForUpdate', () => { + it('merges top level properties', () => { + expect(mergeForUpdate({ foo: 'bar', hello: 'dolly' }, { baz: 42 })).toEqual({ + foo: 'bar', + hello: 'dolly', + baz: 42, + }); + }); + + it('overrides top level properties', () => { + expect(mergeForUpdate({ foo: 'bar', hello: 'dolly' }, { baz: 42, foo: '9000' })).toEqual({ + foo: '9000', + hello: 'dolly', + baz: 42, + }); + }); + + it('ignores undefined top level properties', () => { + expect(mergeForUpdate({ foo: 'bar', hello: 'dolly' }, { baz: 42, foo: undefined })).toEqual({ + foo: 'bar', + hello: 'dolly', + baz: 42, + }); + }); + + it('merges nested properties', () => { + expect( + mergeForUpdate({ nested: { foo: 'bar', hello: 'dolly' } }, { nested: { baz: 42 } }) + ).toEqual({ + nested: { + foo: 'bar', + hello: 'dolly', + baz: 42, + }, + }); + }); + + it('overrides nested properties', () => { + expect( + mergeForUpdate( + { nested: { foo: 'bar', hello: 'dolly' } }, + { nested: { baz: 42, foo: '9000' } } + ) + ).toEqual({ + nested: { + foo: '9000', + hello: 'dolly', + baz: 42, + }, + }); + }); + + it('ignores undefined nested properties', () => { + expect( + mergeForUpdate( + { nested: { foo: 'bar', hello: 'dolly' } }, + { nested: { baz: 42, foo: undefined } } + ) + ).toEqual({ + nested: { + foo: 'bar', + hello: 'dolly', + baz: 42, + }, + }); + }); + + it('functions with mixed levels of properties', () => { + expect( + mergeForUpdate( + { rootPropA: 'A', nested: { foo: 'bar', hello: 'dolly', deep: { deeper: 'we need' } } }, + { rootPropB: 'B', nested: { baz: 42, foo: '9000', deep: { deeper: 'we are' } } } + ) + ).toEqual({ + rootPropA: 'A', + rootPropB: 'B', + nested: { + foo: '9000', + hello: 'dolly', + baz: 42, + deep: { + deeper: 'we are', + }, + }, + }); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/merge_for_update.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/merge_for_update.ts new file mode 100644 index 0000000000000..a3ad081fa74d7 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/merge_for_update.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isPlainObject } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; + +export const mergeForUpdate = ( + targetAttributes: Record, + updatedAttributes: any +): Record => { + return recursiveMerge(targetAttributes, updatedAttributes, []); +}; + +const recursiveMerge = (target: Record, value: any, keys: string[] = []) => { + if (isPlainObject(value) && Object.keys(value).length > 0) { + for (const [subKey, subVal] of Object.entries(value)) { + recursiveMerge(target, subVal, [...keys, subKey]); + } + } else if (keys.length > 0 && value !== undefined) { + set(target, keys, value); + } + + return target; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.encryption_extension.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.encryption_extension.test.ts index 9c2c70e27d121..ecaefe05d65e0 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.encryption_extension.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.encryption_extension.test.ts @@ -350,7 +350,7 @@ describe('SavedObjectsRepository Encryption Extension', () => { namespace, } ); - expect(client.update).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledTimes(1); expect(mockEncryptionExt.isEncryptableType).toHaveBeenCalledTimes(2); // (no upsert) optionallyEncryptAttributes, optionallyDecryptAndRedactSingleResult expect(mockEncryptionExt.isEncryptableType).toHaveBeenCalledWith(nonEncryptedSO.type); expect(mockEncryptionExt.encryptAttributes).not.toHaveBeenCalled(); @@ -382,7 +382,7 @@ describe('SavedObjectsRepository Encryption Extension', () => { references: encryptedSO.references, } ); - expect(client.update).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledTimes(1); expect(mockEncryptionExt.isEncryptableType).toHaveBeenCalledTimes(2); // (no upsert) optionallyEncryptAttributes, optionallyDecryptAndRedactSingleResult expect(mockEncryptionExt.isEncryptableType).toHaveBeenCalledWith(encryptedSO.type); expect(mockEncryptionExt.encryptAttributes).toHaveBeenCalledTimes(1); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts index 3e81c09d1df9f..30778ef7b5f92 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts @@ -235,7 +235,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledTimes(1); expect(result).toEqual( expect.objectContaining({ id, type, attributes, namespaces: [namespace] }) ); @@ -250,7 +250,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledTimes(1); expect(result).toEqual( expect.objectContaining({ id, type, attributes, namespaces: [namespace] }) ); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts index a000384ce5236..29983177adc99 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts @@ -222,27 +222,26 @@ describe('SavedObjectsRepository Spaces Extension', () => { id, {}, { upsert: true }, - { mockGetResponseValue: { found: false } as estypes.GetResponse } + { mockGetResponseAsNotFound: { found: false } as estypes.GetResponse }, + [currentSpace.expectedNamespace ?? 'default'] ); expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1); expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined); - expect(client.update).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledWith( + expect(client.create).toHaveBeenCalledTimes(1); + expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ id: `${ currentSpace.expectedNamespace ? `${currentSpace.expectedNamespace}:` : '' }${type}:${id}`, - body: expect.objectContaining({ - upsert: expect.objectContaining( - currentSpace.expectedNamespace - ? { - namespace: currentSpace.expectedNamespace, - } - : {} - ), - }), + body: expect.objectContaining( + currentSpace.expectedNamespace + ? { + namespace: currentSpace.expectedNamespace, + } + : {} + ), }), - { maxRetries: 0 } + expect.any(Object) ); }); }); @@ -1078,7 +1077,8 @@ describe('SavedObjectsRepository Spaces Extension', () => { id, {}, { upsert: true }, - { mockGetResponseValue: { found: false } as estypes.GetResponse } + { mockGetResponseAsNotFound: { found: false } as estypes.GetResponse }, + [currentSpace] ); expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1); expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined); @@ -1354,11 +1354,12 @@ describe('SavedObjectsRepository Spaces Extension', () => { { // no namespace provided references: encryptedSO.references, - } + }, + {} ); expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1); expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined); - expect(client.update).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledTimes(1); expect(mockEncryptionExt.isEncryptableType).toHaveBeenCalledTimes(2); // (no upsert) optionallyEncryptAttributes, optionallyDecryptAndRedactSingleResult expect(mockEncryptionExt.isEncryptableType).toHaveBeenCalledWith(encryptedSO.type); expect(mockEncryptionExt.encryptAttributes).toHaveBeenCalledTimes(1); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts index ab41890d36368..fa1fda60d09fd 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts @@ -69,7 +69,6 @@ import { import { kibanaMigratorMock } from '../mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import * as esKuery from '@kbn/es-query'; -import { errors as EsErrors } from '@elastic/elasticsearch'; import { CUSTOM_INDEX_TYPE, @@ -94,8 +93,6 @@ import { getMockBulkCreateResponse, bulkGet, getMockBulkUpdateResponse, - updateSuccess, - mockUpdateResponse, expectErrorResult, expectErrorInvalidType, expectErrorNotFound, @@ -188,6 +185,7 @@ describe('SavedObjectsRepository', () => { mockGetSearchDsl.mockClear(); }); + // Setup migration mock for creating an object const mockMigrationVersion = { foo: '2.3.4' }; const mockMigrateDocument = (doc: SavedObjectUnsanitizedDoc) => ({ ...doc, @@ -4819,507 +4817,6 @@ describe('SavedObjectsRepository', () => { }); }); - describe('#update', () => { - const id = 'logstash-*'; - const type = 'index-pattern'; - const attributes = { title: 'Testing' }; - const namespace = 'foo-namespace'; - const references = [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ]; - const originId = 'some-origin-id'; - - beforeEach(() => { - mockPreflightCheckForCreate.mockReset(); - mockPreflightCheckForCreate.mockImplementation(({ objects }) => { - return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default - }); - }); - - describe('client calls', () => { - it(`should use the ES update action when type is not multi-namespace`, async () => { - await updateSuccess(client, repository, registry, type, id, attributes); - expect(client.get).not.toHaveBeenCalled(); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`should use the ES get action then update action when type is multi-namespace`, async () => { - await updateSuccess( - client, - repository, - registry, - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes - ); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`should check for alias conflicts if a new multi-namespace object would be created`, async () => { - await updateSuccess( - client, - repository, - registry, - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes, - { upsert: true }, - { mockGetResponseValue: { found: false } as estypes.GetResponse } - ); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`defaults to no references array`, async () => { - await updateSuccess(client, repository, registry, type, id, attributes); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, - }), - expect.anything() - ); - }); - - it(`accepts custom references array`, async () => { - const test = async (references: SavedObjectReference[]) => { - await updateSuccess(client, repository, registry, type, id, attributes, { references }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - body: { doc: expect.objectContaining({ references }) }, - }), - expect.anything() - ); - client.update.mockClear(); - }; - await test(references); - await test([{ type: 'foo', id: '42', name: 'some ref' }]); - await test([]); - }); - - it(`uses the 'upsertAttributes' option when specified for a single-namespace type`, async () => { - await updateSuccess(client, repository, registry, type, id, attributes, { - upsert: { - title: 'foo', - description: 'bar', - }, - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'index-pattern:logstash-*', - body: expect.objectContaining({ - upsert: expect.objectContaining({ - type: 'index-pattern', - 'index-pattern': { - title: 'foo', - description: 'bar', - }, - }), - }), - }), - expect.anything() - ); - }); - - it(`uses the 'upsertAttributes' option when specified for a multi-namespace type that does not exist`, async () => { - const options = { upsert: { title: 'foo', description: 'bar' } }; - mockUpdateResponse(client, MULTI_NAMESPACE_ISOLATED_TYPE, id, options); - await repository.update(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:logstash-*`, - body: expect.objectContaining({ - upsert: expect.objectContaining({ - type: MULTI_NAMESPACE_ISOLATED_TYPE, - [MULTI_NAMESPACE_ISOLATED_TYPE]: { - title: 'foo', - description: 'bar', - }, - }), - }), - }), - expect.anything() - ); - }); - - it(`ignores use the 'upsertAttributes' option when specified for a multi-namespace type that already exists`, async () => { - const options = { upsert: { title: 'foo', description: 'bar' } }; - await updateSuccess( - client, - repository, - registry, - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes, - options - ); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:logstash-*`, - body: expect.not.objectContaining({ - upsert: expect.anything(), - }), - }), - expect.anything() - ); - }); - - it(`doesn't accept custom references if not an array`, async () => { - const test = async (references: unknown) => { - // @ts-expect-error references is unknown - await updateSuccess(client, repository, registry, type, id, attributes, { references }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, - }), - expect.anything() - ); - client.update.mockClear(); - }; - await test('string'); - await test(123); - await test(true); - await test(null); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await updateSuccess(client, repository, registry, type, id, { foo: 'bar' }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ); - }); - - it(`does not default to the version of the existing document when type is multi-namespace`, async () => { - await updateSuccess( - client, - repository, - registry, - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes, - { references } - ); - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.update).toHaveBeenCalledWith( - expect.not.objectContaining(versionProperties), - expect.anything() - ); - }); - - it(`accepts version`, async () => { - await updateSuccess(client, repository, registry, type, id, attributes, { - version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), - expect.anything() - ); - }); - - it('default to a `retry_on_conflict` setting of `3` when `version` is not provided', async () => { - await updateSuccess(client, repository, registry, type, id, attributes, {}); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ retry_on_conflict: 3 }), - expect.anything() - ); - }); - - it('default to a `retry_on_conflict` setting of `0` when `version` is provided', async () => { - await updateSuccess(client, repository, registry, type, id, attributes, { - version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ retry_on_conflict: 0, if_seq_no: 100, if_primary_term: 200 }), - expect.anything() - ); - }); - - it('accepts a `retryOnConflict` option', async () => { - await updateSuccess(client, repository, registry, type, id, attributes, { - version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), - retryOnConflict: 42, - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ retry_on_conflict: 42, if_seq_no: 100, if_primary_term: 200 }), - expect.anything() - ); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - await updateSuccess(client, repository, registry, type, id, attributes, { namespace }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ id: expect.stringMatching(`${namespace}:${type}:${id}`) }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - await updateSuccess(client, repository, registry, type, id, attributes, { references }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), - expect.anything() - ); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - await updateSuccess(client, repository, registry, type, id, attributes, { - references, - namespace: 'default', - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - await updateSuccess(client, repository, registry, NAMESPACE_AGNOSTIC_TYPE, id, attributes, { - namespace, - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.stringMatching(`${NAMESPACE_AGNOSTIC_TYPE}:${id}`), - }), - expect.anything() - ); - - client.update.mockClear(); - await updateSuccess( - client, - repository, - registry, - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes, - { namespace } - ); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.stringMatching(`${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`), - }), - expect.anything() - ); - }); - - it(`includes _source_includes when type is multi-namespace`, async () => { - await updateSuccess( - client, - repository, - registry, - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes - ); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ _source_includes: ['namespace', 'namespaces', 'originId'] }), - expect.anything() - ); - }); - - it(`includes _source_includes when type is not multi-namespace`, async () => { - await updateSuccess(client, repository, registry, type, id, attributes); - expect(client.update).toHaveBeenLastCalledWith( - expect.objectContaining({ - _source_includes: ['namespace', 'namespaces', 'originId'], - }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type: string, id: string) => { - await expect(repository.update(type, id, {})).rejects.toThrowError( - createGenericNotFoundErrorPayload(type, id) - ); - }; - - it(`throws when options.namespace is '*'`, async () => { - await expect( - repository.update(type, id, attributes, { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestErrorPayload('"options.namespace" cannot be "*"')); - }); - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when id is empty`, async () => { - await expect(repository.update(type, '', attributes)).rejects.toThrowError( - createBadRequestErrorPayload('id cannot be empty') - ); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - { found: false } as estypes.GetResponse, - undefined - ) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({} as estypes.GetResponse, { - statusCode: 404, - }) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse( - registry, - { type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, - namespace - ); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when there is an alias conflict from preflightCheckForCreate`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - found: false, - } as estypes.GetResponse) - ); - mockPreflightCheckForCreate.mockResolvedValue([ - { type: 'type', id: 'id', error: { type: 'aliasConflict' } }, - ]); - await expect( - repository.update( - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - { attr: 'value' }, - { - upsert: { - upsertAttr: 'val', - attr: 'value', - }, - } - ) - ).rejects.toThrowError(createConflictErrorPayload(MULTI_NAMESPACE_ISOLATED_TYPE, id)); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`does not throw when there is a different error from preflightCheckForCreate`, async () => { - mockPreflightCheckForCreate.mockResolvedValue([ - { type: 'type', id: 'id', error: { type: 'conflict' } }, - ]); - await updateSuccess( - client, - repository, - registry, - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes, - { upsert: true }, - { mockGetResponseValue: { found: false } as estypes.GetResponse } - ); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during update`, async () => { - const notFoundError = new EsErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: 404, - body: { error: { type: 'es_type', reason: 'es_reason' } }, - }) - ); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createErrorTransportRequestPromise(notFoundError) - ); - await expectNotFoundError(type, id); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns _seq_no and _primary_term encoded as version`, async () => { - const result = await updateSuccess(client, repository, registry, type, id, attributes, { - namespace, - references, - }); - expect(result).toEqual({ - id, - type, - ...mockTimestampFields, - version: mockVersion, - attributes, - references, - namespaces: [namespace], - }); - }); - - it(`includes namespaces if type is multi-namespace`, async () => { - const result = await updateSuccess( - client, - repository, - registry, - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes - ); - expect(result).toMatchObject({ - namespaces: expect.any(Array), - }); - }); - - it(`includes namespaces if type is not multi-namespace`, async () => { - const result = await updateSuccess(client, repository, registry, type, id, attributes); - expect(result).toMatchObject({ - namespaces: ['default'], - }); - }); - - it(`includes originId property if present in cluster call response`, async () => { - const result = await updateSuccess( - client, - repository, - registry, - type, - id, - attributes, - {}, - { originId } - ); - expect(result).toMatchObject({ originId }); - }); - }); - }); - describe('#openPointInTimeForType', () => { const type = 'index-pattern'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts index ba96c10da0090..861a2f847d97b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts @@ -9,3 +9,4 @@ export { decorateEsError } from './decorate_es_error'; export { getRootFields, includedFields } from './included_fields'; export { createRepositoryHelpers } from './create_helpers'; +export { isValidRequest } from './update_utils'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/update_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/update_utils.ts new file mode 100644 index 0000000000000..c686cbb7ca68b --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/update_utils.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; + +export const isValidRequest = ({ + allowedTypes, + type, + id, +}: { + allowedTypes: string[]; + type: string; + id?: string; +}) => { + return !id + ? { + validRequest: false, + error: SavedObjectsErrorHelpers.createBadRequestError('id cannot be empty'), + } + : !allowedTypes.includes(type) + ? { + validRequest: false, + error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id), + } + : { + validRequest: true, + }; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_helpers.mocks.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_helpers.mocks.ts index 69f0124f681e1..e50cc4d1036eb 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_helpers.mocks.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_helpers.mocks.ts @@ -92,6 +92,8 @@ const createPreflightCheckHelperMock = (): PreflightCheckHelperMock => { preflightCheckForBulkDelete: jest.fn(), preflightCheckNamespaces: jest.fn(), preflightCheckForUpsertAliasConflict: jest.fn(), + preflightGetDocForUpdate: jest.fn(), + preflightCheckNamespacesForUpdate: jest.fn(), }; return mock; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts index 2cb4f32f441cb..29c00e9d41ac1 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts @@ -103,7 +103,7 @@ export const expectErrorConflict = (obj: TypeIdTuple, overrides?: Record) => expectErrorResult(obj, createUnsupportedTypeErrorPayload(obj.type), overrides); -export const KIBANA_VERSION = '2.0.0'; +export const KIBANA_VERSION = '8.8.0'; export const ALLOWED_CONVERT_VERSION = '8.0.0'; export const CUSTOM_INDEX_TYPE = 'customIndex'; /** This type has namespaceType: 'agnostic'. */ @@ -439,7 +439,7 @@ export const getMockGetResponse = ( } const namespaceId = namespaces[0] === 'default' ? undefined : namespaces[0]; - return { + const result = { // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these found: true, _id: `${registry.isSingleNamespace(type) && namespaceId ? `${namespaceId}:` : ''}${type}:${id}`, @@ -464,6 +464,7 @@ export const getMockGetResponse = ( ...mockTimestampFields, } as SavedObjectsRawDocSource, } as estypes.GetResponse; + return result; }; export const getMockMgetResponse = ( @@ -489,35 +490,6 @@ expect.extend({ }, }); -export const mockUpdateResponse = ( - client: ElasticsearchClientMock, - type: string, - id: string, - options?: SavedObjectsUpdateOptions, - namespaces?: string[], - originId?: string -) => { - client.update.mockResponseOnce( - { - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - // don't need the rest of the source for test purposes, just the namespace and namespaces attributes - get: { - _source: { - namespaces: namespaces ?? [options?.namespace ?? 'default'], - namespace: options?.namespace, - - // If the existing saved object contains an originId attribute, the operation will return it in the result. - // The originId parameter is just used for test purposes to modify the mock cluster call response. - ...(!!originId && { originId }), - }, - }, - } as estypes.UpdateResponse, - { statusCode: 200 } - ); -}; - export const updateSuccess = async >( client: ElasticsearchClientMock, repository: SavedObjectsRepository, @@ -528,20 +500,40 @@ export const updateSuccess = async >( options?: SavedObjectsUpdateOptions, internalOptions: { originId?: string; - mockGetResponseValue?: estypes.GetResponse; + mockGetResponseAsNotFound?: estypes.GetResponse; } = {}, objNamespaces?: string[] ) => { - const { mockGetResponseValue, originId } = internalOptions; - if (registry.isMultiNamespace(type)) { - const mockGetResponse = - mockGetResponseValue ?? - getMockGetResponse(registry, { type, id }, objNamespaces ?? options?.namespace); - client.get.mockResponseOnce(mockGetResponse, { statusCode: 200 }); + const { mockGetResponseAsNotFound, originId } = internalOptions; + const mockGetResponse = + mockGetResponseAsNotFound ?? + getMockGetResponse(registry, { type, id, originId }, objNamespaces ?? options?.namespace); + client.get.mockResponseOnce(mockGetResponse, { statusCode: 200 }); + if (!mockGetResponseAsNotFound) { + // index doc from existing doc + client.index.mockResponseImplementation((params) => { + return { + body: { + _id: params.id, + ...mockVersionProps, + } as estypes.CreateResponse, + }; + }); + } + if (mockGetResponseAsNotFound) { + // upsert case: create the doc. (be careful here, we're also sending mockGetResponseValue as { found: false }) + client.create.mockResponseImplementation((params) => { + return { + body: { + _id: params.id, + ...mockVersionProps, + } as estypes.CreateResponse, + }; + }); } - mockUpdateResponse(client, type, id, options, objNamespaces, originId); + const result = await repository.update(type, id, attributes, options); - expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); + expect(client.get).toHaveBeenCalled(); // not asserting on the number of calls here, we end up testing the test mocks and not the actual implementation return result; }; diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/update.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/update.ts index 66f7103102b6e..e830b32601c5b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/update.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/update.ts @@ -18,6 +18,7 @@ export interface SavedObjectsUpdateOptions extends SavedOb /** * An opaque version number which changes on each successful write operation. * Can be used for implementing optimistic concurrency control. + * Unused for multi-namespace objects */ version?: string; /** {@inheritdoc SavedObjectReference} */ @@ -31,6 +32,8 @@ export interface SavedObjectsUpdateOptions extends SavedOb * Defaults to `0` when `version` is provided, `3` otherwise. */ retryOnConflict?: number; + /** {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} */ + migrationVersionCompatibility?: 'compatible' | 'raw'; } /** diff --git a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.ts b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.ts index 96eded287975c..cb3036fa9d668 100644 --- a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.ts +++ b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.ts @@ -116,7 +116,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer { ...(includeNamespaces && { namespaces }), ...(originId && { originId }), attributes: _source[type], - references: references || [], + references: references || [], // adds references default ...(managed != null ? { managed } : {}), ...(migrationVersion && { migrationVersion }), ...(coreMigrationVersion && { coreMigrationVersion }), diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts index 11a6545bfd612..b720260619aca 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts @@ -62,7 +62,12 @@ export const registerUpdateRoute = ( }); const { type, id } = req.params; const { attributes, version, references, upsert } = req.body; - const options: SavedObjectsUpdateOptions = { version, references, upsert }; + const options: SavedObjectsUpdateOptions = { + version, + references, + upsert, + migrationVersionCompatibility: 'raw' as const, + }; const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsUpdate({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts index 82328c51fd6c1..0efda4a5bce9b 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts @@ -57,12 +57,13 @@ import { import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { DeprecationRegistryProvider } from '@kbn/core-deprecations-server'; import type { NodeInfo } from '@kbn/core-node-server'; -import { MAIN_SAVED_OBJECT_INDEX, ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; +import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import { registerRoutes } from './routes'; import { calculateStatus$ } from './status'; import { registerCoreObjectTypes } from './object_types'; import { getSavedObjectsDeprecationsProvider } from './deprecations'; import { applyTypeDefaults } from './apply_type_defaults'; +import { getAllIndices } from './utils'; /** * @internal @@ -202,7 +203,6 @@ export class SavedObjectsService }, getTypeRegistry: () => this.typeRegistry, getDefaultIndex: () => MAIN_SAVED_OBJECT_INDEX, - getAllIndices: () => [...ALL_SAVED_OBJECT_INDICES], }; } @@ -326,6 +326,8 @@ export class SavedObjectsService clientProvider.setClientFactory(clientFactory); } + const allIndices = getAllIndices({ registry: this.typeRegistry }); + this.started = true; return { @@ -361,7 +363,7 @@ export class SavedObjectsService }); return [...indices]; }, - getAllIndices: () => [...ALL_SAVED_OBJECT_INDICES], + getAllIndices: () => [...allIndices], }; } diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/utils/get_all_indices.test.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/utils/get_all_indices.test.ts new file mode 100644 index 0000000000000..c3a09df0ed529 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/utils/get_all_indices.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { type SavedObjectsType, MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import { SavedObjectTypeRegistry } from '@kbn/core-saved-objects-base-server-internal'; +import { getAllIndices } from './get_all_indices'; + +describe('getAllIndices', () => { + const createType = (parts: Partial): SavedObjectsType => ({ + name: 'test', + hidden: false, + namespaceType: 'single', + mappings: { properties: {} }, + ...parts, + }); + + const createRegistry = (...types: Array>): SavedObjectTypeRegistry => { + const registry = new SavedObjectTypeRegistry(); + types.forEach((type) => { + registry.registerType(createType(type)); + }); + + return registry; + }; + + it('returns the indices that are used by registered types', () => { + const registry = createRegistry( + { name: 'type_1' }, + { name: 'type_2', indexPattern: '.kibana_ingest' } + ); + expect(getAllIndices({ registry })).toEqual([MAIN_SAVED_OBJECT_INDEX, '.kibana_ingest']); + }); + + it('returns each index only once', () => { + const registry = createRegistry( + { name: 'type_1' }, + { name: 'type_2', indexPattern: '.kibana_foo' }, + { name: 'type_3' }, + { name: 'type_4', indexPattern: '.kibana_foo' } + ); + expect(getAllIndices({ registry })).toEqual([MAIN_SAVED_OBJECT_INDEX, '.kibana_foo']); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/utils/get_all_indices.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/utils/get_all_indices.ts new file mode 100644 index 0000000000000..166cbf8f78ef1 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/utils/get_all_indices.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import type { SavedObjectTypeRegistry } from '@kbn/core-saved-objects-base-server-internal'; + +export const getAllIndices = ({ registry }: { registry: SavedObjectTypeRegistry }): string[] => { + return [ + ...new Set(registry.getAllTypes().map((type) => type.indexPattern ?? MAIN_SAVED_OBJECT_INDEX)), + ]; +}; diff --git a/src/plugins/discover/public/components/index.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/utils/index.ts similarity index 85% rename from src/plugins/discover/public/components/index.ts rename to packages/core/saved-objects/core-saved-objects-server-internal/src/utils/index.ts index a794b8c90a477..e931a9c64c039 100644 --- a/src/plugins/discover/public/components/index.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/utils/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { DeferredSpinner } from './common/deferred_spinner'; +export { getAllIndices } from './get_all_indices'; diff --git a/packages/core/saved-objects/core-saved-objects-server-mocks/src/saved_objects_service.mock.ts b/packages/core/saved-objects/core-saved-objects-server-mocks/src/saved_objects_service.mock.ts index 2d7d5ff847330..58b7c424ac19a 100644 --- a/packages/core/saved-objects/core-saved-objects-server-mocks/src/saved_objects_service.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-server-mocks/src/saved_objects_service.mock.ts @@ -77,11 +77,9 @@ const createSetupContractMock = () => { setSpacesExtension: jest.fn(), registerType: jest.fn(), getDefaultIndex: jest.fn(), - getAllIndices: jest.fn(), }; setupContract.getDefaultIndex.mockReturnValue(MAIN_SAVED_OBJECT_INDEX); - setupContract.getAllIndices.mockReturnValue([MAIN_SAVED_OBJECT_INDEX]); return setupContract; }; diff --git a/packages/core/saved-objects/core-saved-objects-server/src/contracts.ts b/packages/core/saved-objects/core-saved-objects-server/src/contracts.ts index e08da10172f3c..1f20524f9a114 100644 --- a/packages/core/saved-objects/core-saved-objects-server/src/contracts.ts +++ b/packages/core/saved-objects/core-saved-objects-server/src/contracts.ts @@ -137,13 +137,6 @@ export interface SavedObjectsServiceSetup { * Returns the default index used for saved objects. */ getDefaultIndex: () => string; - - /** - * Returns all (aliases to) kibana system indices used for saved object storage. - * - * @deprecated use the `start` contract counterpart. - */ - getAllIndices: () => string[]; } /** @@ -235,6 +228,8 @@ export interface SavedObjectsServiceStart { getDefaultIndex: () => string; /** * Returns all (aliases to) kibana system indices used for saved object storage. + * + * @remarks Only the indices effectively present in the current running environment will be returned. */ getAllIndices: () => string[]; } diff --git a/packages/core/saved-objects/core-saved-objects-server/src/serialization.ts b/packages/core/saved-objects/core-saved-objects-server/src/serialization.ts index 8b3b6b4924d79..a5fbf87145d8d 100644 --- a/packages/core/saved-objects/core-saved-objects-server/src/serialization.ts +++ b/packages/core/saved-objects/core-saved-objects-server/src/serialization.ts @@ -73,7 +73,12 @@ export interface SavedObjectsRawDoc { _primary_term?: number; } -/** @public */ +/** + * Saved object document as stored in `_source` of doc in ES index + * Similar to SavedObjectDoc and excludes `version`, includes `references`, has `attributes` in [typeMapping] + * + * @public + */ export interface SavedObjectsRawDocSource { type: string; namespace?: string; diff --git a/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_serverless_root.ts b/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_serverless_root.ts index 4e5ac0de487b5..3e12b16832714 100644 --- a/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_serverless_root.ts +++ b/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_serverless_root.ts @@ -6,13 +6,12 @@ * Side Public License, v 1. */ +import Path from 'path'; import { defaultsDeep } from 'lodash'; import { Client, HttpConnection } from '@elastic/elasticsearch'; import { Cluster } from '@kbn/es'; -import Path from 'path'; import { REPO_ROOT } from '@kbn/repo-info'; import { ToolingLog } from '@kbn/tooling-log'; -import execa from 'execa'; import { CliArgs } from '@kbn/config'; import { createRoot, type TestElasticsearchUtils, type TestKibanaUtils } from './create_root'; @@ -64,6 +63,7 @@ export function createTestServerlessInstances({ }; } +const ES_BASE_PATH_DIR = Path.join(REPO_ROOT, '.es/es_test_serverless'); function createServerlessES() { const log = new ToolingLog({ level: 'info', @@ -74,37 +74,23 @@ function createServerlessES() { es, start: async () => { await es.runServerless({ - basePath: Path.join(REPO_ROOT, '.es/es_test_serverless'), + basePath: ES_BASE_PATH_DIR, + teardown: true, + background: true, + clean: true, + kill: true, + waitForReady: true, }); - // runServerless doesn't wait until the nodes are up - await waitUntilClusterReady(getServerlessESClient()); return { getClient: getServerlessESClient, stop: async () => { - // hack to stop the ES cluster - await execa('docker', ['container', 'stop', 'es01', 'es02', 'es03']); + await es.stop(); }, }; }, }; } -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -const waitUntilClusterReady = async (client: Client, timeoutMs = 60 * 1000) => { - const started = Date.now(); - - while (started + timeoutMs > Date.now()) { - try { - await client.info(); - break; - } catch (e) { - await delay(1000); - /* trap to continue */ - } - } -}; - const getServerlessESClient = () => { return new Client({ // node ports not configurable from @@ -113,22 +99,44 @@ const getServerlessESClient = () => { }); }; -const defaults = { - server: { - restrictInternalApis: true, - versioned: { - versionResolution: 'newest', - strictClientVersionCheck: false, +const getServerlessDefault = () => { + return { + server: { + restrictInternalApis: true, + versioned: { + versionResolution: 'newest', + strictClientVersionCheck: false, + }, }, - }, - migrations: { - algorithm: 'zdt', - }, - elasticsearch: { - serviceAccountToken: 'BEEF', - }, + migrations: { + algorithm: 'zdt', + zdt: { + runOnRoles: ['ui'], + }, + }, + logging: { + loggers: [ + { + name: 'root', + level: 'error', + appenders: ['console'], + }, + { + name: 'elasticsearch.deprecation', + level: 'all', + appenders: ['deprecation'], + }, + ], + appenders: { + deprecation: { type: 'console', layout: { type: 'json' } }, + console: { type: 'console', layout: { type: 'pattern' } }, + }, + }, + }; }; - function createServerlessKibana(settings = {}, cliArgs: Partial = {}) { - return createRoot(defaultsDeep(settings, defaults), { ...cliArgs, serverless: true }); + return createRoot(defaultsDeep(settings, getServerlessDefault()), { + ...cliArgs, + serverless: true, + }); } diff --git a/packages/deeplinks/observability/constants.ts b/packages/deeplinks/observability/constants.ts index addd326de2ce3..a28c9e4aaff21 100644 --- a/packages/deeplinks/observability/constants.ts +++ b/packages/deeplinks/observability/constants.ts @@ -8,6 +8,8 @@ export const LOGS_APP_ID = 'logs'; +export const OBSERVABILITY_LOG_EXPLORER = 'observability-log-explorer'; + export const OBSERVABILITY_OVERVIEW_APP_ID = 'observability-overview'; export const METRICS_APP_ID = 'metrics'; diff --git a/packages/deeplinks/observability/deep_links.ts b/packages/deeplinks/observability/deep_links.ts index 19432ef37d3ba..1d405c1a20620 100644 --- a/packages/deeplinks/observability/deep_links.ts +++ b/packages/deeplinks/observability/deep_links.ts @@ -5,10 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { DISCOVER_APP_ID } from '@kbn/deeplinks-analytics'; import { LOGS_APP_ID, + OBSERVABILITY_LOG_EXPLORER, OBSERVABILITY_OVERVIEW_APP_ID, METRICS_APP_ID, APM_APP_ID, @@ -16,6 +16,7 @@ import { } from './constants'; type LogsApp = typeof LOGS_APP_ID; +type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER; type ObservabilityOverviewApp = typeof OBSERVABILITY_OVERVIEW_APP_ID; type MetricsApp = typeof METRICS_APP_ID; type ApmApp = typeof APM_APP_ID; @@ -23,13 +24,12 @@ type ObservabilityOnboardingApp = typeof OBSERVABILITY_ONBOARDING_APP_ID; export type AppId = | LogsApp + | ObservabilityLogExplorerApp | ObservabilityOverviewApp | ObservabilityOnboardingApp | ApmApp | MetricsApp; -export type DiscoverLogExplorerId = `${typeof DISCOVER_APP_ID}:log-explorer`; - export type LogsLinkId = 'log-categories' | 'settings' | 'anomalies' | 'stream'; export type ObservabilityOverviewLinkId = @@ -55,7 +55,6 @@ export type LinkId = LogsLinkId | ObservabilityOverviewLinkId | MetricsLinkId | export type DeepLinkId = | AppId - | DiscoverLogExplorerId | `${LogsApp}:${LogsLinkId}` | `${ObservabilityOverviewApp}:${ObservabilityOverviewLinkId}` | `${MetricsApp}:${MetricsLinkId}` diff --git a/packages/deeplinks/observability/tsconfig.json b/packages/deeplinks/observability/tsconfig.json index 9f5bc24d4f316..94b099694eaf4 100644 --- a/packages/deeplinks/observability/tsconfig.json +++ b/packages/deeplinks/observability/tsconfig.json @@ -16,6 +16,5 @@ "target/**/*" ], "kbn_references": [ - "@kbn/deeplinks-analytics", ] } diff --git a/packages/kbn-alerts-ui-shared/index.ts b/packages/kbn-alerts-ui-shared/index.ts index 6815a66bce902..6daec0f82b108 100644 --- a/packages/kbn-alerts-ui-shared/index.ts +++ b/packages/kbn-alerts-ui-shared/index.ts @@ -9,3 +9,4 @@ export { AlertLifecycleStatusBadge } from './src/alert_lifecycle_status_badge'; export type { AlertLifecycleStatusBadgeProps } from './src/alert_lifecycle_status_badge'; export { MaintenanceWindowCallout } from './src/maintenance_window_callout'; +export { AddMessageVariables } from './src/add_message_variables'; diff --git a/packages/kbn-alerts-ui-shared/jest.config.js b/packages/kbn-alerts-ui-shared/jest.config.js index 31062b3280e41..54f2c74a56d3a 100644 --- a/packages/kbn-alerts-ui-shared/jest.config.js +++ b/packages/kbn-alerts-ui-shared/jest.config.js @@ -10,4 +10,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../..', roots: ['/packages/kbn-alerts-ui-shared'], + setupFilesAfterEnv: ['/packages/kbn-alerts-ui-shared/setup_tests.ts'], }; diff --git a/packages/kbn-alerts-ui-shared/setup_tests.ts b/packages/kbn-alerts-ui-shared/setup_tests.ts new file mode 100644 index 0000000000000..8d1acb9232934 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/setup_tests.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss b/packages/kbn-alerts-ui-shared/src/add_message_variables/add_message_variables.scss similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss rename to packages/kbn-alerts-ui-shared/src/add_message_variables/add_message_variables.scss diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx b/packages/kbn-alerts-ui-shared/src/add_message_variables/index.test.tsx similarity index 95% rename from x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx rename to packages/kbn-alerts-ui-shared/src/add_message_variables/index.test.tsx index 641587c349382..80603d9eb8615 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/add_message_variables/index.test.tsx @@ -1,13 +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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; -import { AddMessageVariables } from './add_message_variables'; +import { AddMessageVariables } from '.'; describe('AddMessageVariables', () => { test('it renders variables and filter bar', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx rename to packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx index f986930470acc..0aa9bae65c29e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx @@ -1,8 +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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useMemo, useState } from 'react'; @@ -23,7 +24,7 @@ import { } from '@elastic/eui'; import { ActionVariable } from '@kbn/alerting-plugin/common'; import './add_message_variables.scss'; -import { TruncatedText } from '../../common/truncated_text'; +import { TruncatedText } from './truncated_text'; import * as i18n from './translations'; interface Props { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/translations.js b/packages/kbn-alerts-ui-shared/src/add_message_variables/translations.ts similarity index 57% rename from x-pack/plugins/triggers_actions_ui/public/application/components/translations.js rename to packages/kbn-alerts-ui-shared/src/add_message_variables/translations.ts index 0e089f1a830e8..b19e9173797a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/translations.js +++ b/packages/kbn-alerts-ui-shared/src/add_message_variables/translations.ts @@ -1,70 +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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; export const LOADING_VARIABLES = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.loadingMessage', + 'alertsUIShared.components.addMessageVariables.loadingMessage', { defaultMessage: 'Loading variables', } ); export const NO_VARIABLES_FOUND = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.noVariablesFound', + 'alertsUIShared.components.addMessageVariables.noVariablesFound', { defaultMessage: 'No variables found', } ); export const NO_VARIABLES_AVAILABLE = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.noVariablesAvailable', + 'alertsUIShared.components.addMessageVariables.noVariablesAvailable', { defaultMessage: 'No variables available', } ); export const DEPRECATED_VARIABLES_ARE_SHOWN = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreShown', + 'alertsUIShared.components.addMessageVariables.deprecatedVariablesAreShown', { defaultMessage: 'Deprecated variables are shown', } ); export const DEPRECATED_VARIABLES_ARE_HIDDEN = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreHidden', + 'alertsUIShared.components.addMessageVariables.deprecatedVariablesAreHidden', { defaultMessage: 'Deprecated variables are hidden', } ); export const HIDE = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.hideDeprecatedVariables', + 'alertsUIShared.components.addMessageVariables.hideDeprecatedVariables', { defaultMessage: 'Hide', } ); export const SHOW_ALL = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.showAllDeprecatedVariables', + 'alertsUIShared.components.addMessageVariables.showAllDeprecatedVariables', { defaultMessage: 'Show all', } ); export const ADD_VARIABLE_POPOVER_BUTTON = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton', + 'alertsUIShared.components.addMessageVariables.addVariablePopoverButton', { defaultMessage: 'Add variable', } ); export const ADD_VARIABLE_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle', + 'alertsUIShared.components.addMessageVariables.addRuleVariableTitle', { defaultMessage: 'Add variable', } diff --git a/packages/kbn-alerts-ui-shared/src/add_message_variables/truncated_text.tsx b/packages/kbn-alerts-ui-shared/src/add_message_variables/truncated_text.tsx new file mode 100644 index 0000000000000..52535ab435e68 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/add_message_variables/truncated_text.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { css } from '@emotion/react'; +import { EuiText } from '@elastic/eui'; + +const LINE_CLAMP = 2; + +const styles = { + truncatedText: css` + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: ${LINE_CLAMP}; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; + `, +}; + +const TruncatedTextComponent: React.FC<{ text: string }> = ({ text }) => ( + + {text} + +); + +TruncatedTextComponent.displayName = 'TruncatedText'; + +export const TruncatedText = React.memo(TruncatedTextComponent); diff --git a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx index b29f0ddc613c4..9a05a6bd09222 100644 --- a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx @@ -106,7 +106,6 @@ describe('MaintenanceWindowCallout', () => { { wrapper: TestProviders } ); - // @ts-expect-error Jest types are incomplete in packages expect(await findByText('Maintenance window is running')).toBeInTheDocument(); expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1); }); @@ -119,7 +118,7 @@ describe('MaintenanceWindowCallout', () => { const { container } = render(, { wrapper: TestProviders, }); - // @ts-expect-error Jest types are incomplete in packages + expect(container).toBeEmptyDOMElement(); expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1); }); @@ -130,7 +129,7 @@ describe('MaintenanceWindowCallout', () => { const { container } = render(, { wrapper: TestProviders, }); - // @ts-expect-error Jest types are incomplete in packages + expect(container).toBeEmptyDOMElement(); expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1); }); @@ -192,7 +191,7 @@ describe('MaintenanceWindowCallout', () => { const { container } = render(, { wrapper: TestProviders, }); - // @ts-expect-error Jest types are incomplete in packages + expect(container).toBeEmptyDOMElement(); }); @@ -213,7 +212,7 @@ describe('MaintenanceWindowCallout', () => { const { findByText } = render(, { wrapper: TestProviders, }); - // @ts-expect-error Jest types are incomplete in packages + expect(await findByText('Maintenance window is running')).toBeInTheDocument(); }); }); diff --git a/packages/kbn-config-mocks/src/config_service.mock.ts b/packages/kbn-config-mocks/src/config_service.mock.ts index 09a282965eba8..268f5a4558022 100644 --- a/packages/kbn-config-mocks/src/config_service.mock.ts +++ b/packages/kbn-config-mocks/src/config_service.mock.ts @@ -27,6 +27,8 @@ const createConfigServiceMock = ({ validate: jest.fn(), getHandledDeprecatedConfigs: jest.fn(), getDeprecatedConfigPath$: jest.fn(), + addDynamicConfigPaths: jest.fn(), + setDynamicConfigOverrides: jest.fn(), }; mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index c38875bdae6ed..434534f6d888a 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { BehaviorSubject, Observable } from 'rxjs'; -import { first, take } from 'rxjs/operators'; +import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs'; +import { first, map, take } from 'rxjs/operators'; import { mockApplyDeprecations, @@ -670,3 +670,43 @@ describe('getDeprecatedConfigPath$', () => { expect(deprecatedConfigPath).toEqual(mockedChangedPaths); }); }); + +describe('Dynamic Overrides', () => { + let configService: ConfigService; + + beforeEach(async () => { + const rawConfig$ = new BehaviorSubject>({ namespace1: { key: 'value' } }); + const rawConfigProvider = createRawConfigServiceMock({ rawConfig$ }); + + configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + await configService.setSchema('namespace1', schema.object({ key: schema.string() })); + + expect( + await firstValueFrom(configService.getConfig$().pipe(map((cfg) => cfg.toRaw()))) + ).toStrictEqual({ namespace1: { key: 'value' } }); + }); + + test('throws validation error when attempted to set an override that has not been registered as dynamic', () => { + expect(() => + configService.setDynamicConfigOverrides({ 'namespace1.key': 'another-value' }) + ).toThrowErrorMatchingInlineSnapshot(`"[namespace1.key]: not a valid dynamic option"`); + }); + + test('throws validation error when a registered as dynamic option is invalid', () => { + configService.addDynamicConfigPaths('namespace1', ['key']); + expect(() => + configService.setDynamicConfigOverrides({ 'namespace1.key': 1 }) + ).toThrowErrorMatchingInlineSnapshot( + `"[config validation of [namespace1].key]: expected value of type [string] but got [number]"` + ); + }); + + test('overrides the static settings with the dynamic ones', async () => { + configService.addDynamicConfigPaths('namespace1', ['key']); + configService.setDynamicConfigOverrides({ 'namespace1.key': 'another-value' }); + + expect( + await firstValueFrom(configService.getConfig$().pipe(map((cfg) => cfg.toRaw()))) + ).toStrictEqual({ namespace1: { key: 'another-value' } }); + }); +}); diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index b9ef887e1002c..0026876f70b4d 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -7,16 +7,18 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Type } from '@kbn/config-schema'; -import { isEqual } from 'lodash'; +import { SchemaTypeError, Type, ValidationError } from '@kbn/config-schema'; +import { cloneDeep, isEqual, merge } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { BehaviorSubject, combineLatest, firstValueFrom, Observable } from 'rxjs'; import { distinctUntilChanged, first, map, shareReplay, tap } from 'rxjs/operators'; import { Logger, LoggerFactory } from '@kbn/logging'; import { getDocLinks, DocLinks } from '@kbn/doc-links'; +import { getFlattenedObject } from '@kbn/std'; import { Config, ConfigPath, Env } from '..'; import { hasConfigPathIntersection } from './config'; -import { RawConfigurationProvider } from './raw/raw_config_service'; +import { RawConfigurationProvider } from './raw'; import { applyDeprecations, ConfigDeprecationWithContext, @@ -60,6 +62,8 @@ export class ConfigService { private readonly handledPaths: Set = new Set(); private readonly schemas = new Map>(); private readonly deprecations = new BehaviorSubject([]); + private readonly dynamicPaths = new Map(); + private readonly overrides$ = new BehaviorSubject>({}); private readonly handledDeprecatedConfigs = new Map(); constructor( @@ -71,9 +75,14 @@ export class ConfigService { this.deprecationLog = logger.get('config', 'deprecation'); this.docLinks = getDocLinks({ kibanaBranch: env.packageInfo.branch }); - this.config$ = combineLatest([this.rawConfigProvider.getConfig$(), this.deprecations]).pipe( - map(([rawConfig, deprecations]) => { - const migrated = applyDeprecations(rawConfig, deprecations); + this.config$ = combineLatest([ + this.rawConfigProvider.getConfig$(), + this.deprecations, + this.overrides$, + ]).pipe( + map(([rawConfig, deprecations, overrides]) => { + const overridden = merge(rawConfig, overrides); + const migrated = applyDeprecations(overridden, deprecations); this.deprecatedConfigPaths.next(migrated.changedPaths); return new ObjectToConfigAdapter(migrated.config); }), @@ -213,6 +222,59 @@ export class ConfigService { return this.deprecatedConfigPaths.asObservable(); } + /** + * Adds a specific setting to be allowed to change dynamically. + * @param configPath The namespace of the config + * @param dynamicConfigPaths The config keys that can be dynamically changed + */ + public addDynamicConfigPaths(configPath: ConfigPath, dynamicConfigPaths: string[]) { + const _configPath = Array.isArray(configPath) ? configPath.join('.') : configPath; + this.dynamicPaths.set(_configPath, dynamicConfigPaths); + } + + /** + * Used for dynamically extending the overrides. + * These overrides are not persisted and will be discarded after restarts. + * @param newOverrides + */ + public setDynamicConfigOverrides(newOverrides: Record) { + const globalOverrides = cloneDeep(this.overrides$.value); + + const flattenedOverrides = getFlattenedObject(newOverrides); + + const validateWithNamespace = new Set(); + + keyLoop: for (const key in flattenedOverrides) { + // this if is enforced by an eslint rule :shrug: + if (key in flattenedOverrides) { + for (const [configPath, dynamicConfigKeys] of this.dynamicPaths.entries()) { + if ( + key.startsWith(`${configPath}.`) && + dynamicConfigKeys.some( + // The key is explicitly allowed OR its prefix is + (dynamicConfigKey) => + key === `${configPath}.${dynamicConfigKey}` || + key.startsWith(`${configPath}.${dynamicConfigKey}.`) + ) + ) { + validateWithNamespace.add(configPath); + set(globalOverrides, key, flattenedOverrides[key]); + continue keyLoop; + } + } + throw new ValidationError(new SchemaTypeError(`not a valid dynamic option`, [key])); + } + } + + const globalOverridesAsConfig = new ObjectToConfigAdapter( + merge({}, this.lastConfig, globalOverrides) + ); + + validateWithNamespace.forEach((ns) => this.validateAtPath(ns, globalOverridesAsConfig.get(ns))); + + this.overrides$.next(globalOverrides); + } + private async logDeprecation() { const rawConfig = await firstValueFrom(this.rawConfigProvider.getConfig$()); const deprecations = await firstValueFrom(this.deprecations); diff --git a/packages/kbn-dev-utils/index.ts b/packages/kbn-dev-utils/index.ts index 86bdafebccf97..2faf0000fde29 100644 --- a/packages/kbn-dev-utils/index.ts +++ b/packages/kbn-dev-utils/index.ts @@ -19,6 +19,7 @@ export { KBN_P12_PATH, KBN_P12_PASSWORD, } from './src/certs'; +export * from './src/dev_service_account'; export * from './src/axios'; export * from './src/plugin_list'; export * from './src/streams'; diff --git a/packages/kbn-dev-utils/src/dev_service_account.ts b/packages/kbn-dev-utils/src/dev_service_account.ts new file mode 100644 index 0000000000000..efcd51b3f1559 --- /dev/null +++ b/packages/kbn-dev-utils/src/dev_service_account.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const env = process.env; + +/** + * `kibana-dev` service account token for connecting to ESS + * See packages/kbn-es/src/ess_resources/README.md + */ +export const kibanaDevServiceAccount = { + token: + env.TEST_KIBANA_SERVICE_ACCOUNT_TOKEN || + 'AAEAAWVsYXN0aWMva2liYW5hL2tpYmFuYS1kZXY6VVVVVVVVTEstKiBaNA', +}; diff --git a/packages/kbn-discover-utils/index.ts b/packages/kbn-discover-utils/index.ts index dae9c268f80db..d19f50c5dc646 100644 --- a/packages/kbn-discover-utils/index.ts +++ b/packages/kbn-discover-utils/index.ts @@ -13,9 +13,11 @@ export { DEFAULT_COLUMNS_SETTING, DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, - ENABLE_SQL, + ENABLE_ESQL, FIELDS_LIMIT_SETTING, HIDE_ANNOUNCEMENTS, + KNOWN_FIELD_TYPE_LIST, + KNOWN_FIELD_TYPES, MAX_DOC_FIELDS_DISPLAYED, MODIFY_COLUMNS_ON_SWITCH, ROW_HEIGHT_OPTION, @@ -34,8 +36,10 @@ export { formatFieldValue, formatHit, getDocId, + getFieldTypeName, getIgnoredReason, getShouldShowFieldHandler, + isKnownFieldType, isNestedFieldParent, usePager, } from './src'; diff --git a/packages/kbn-discover-utils/src/constants.ts b/packages/kbn-discover-utils/src/constants.ts index 60aaf63d9c70e..7582b610c6638 100644 --- a/packages/kbn-discover-utils/src/constants.ts +++ b/packages/kbn-discover-utils/src/constants.ts @@ -12,7 +12,7 @@ export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn'; export const DOC_TABLE_LEGACY = 'doc_table:legacy'; -export const ENABLE_SQL = 'discover:enableSql'; +export const ENABLE_ESQL = 'discover:enableESQL'; export const FIELDS_LIMIT_SETTING = 'fields:popularLimit'; export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements'; export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed'; diff --git a/packages/kbn-discover-utils/src/types.ts b/packages/kbn-discover-utils/src/types.ts index 752de1dcb9e9d..5a2f3ed9b085e 100644 --- a/packages/kbn-discover-utils/src/types.ts +++ b/packages/kbn-discover-utils/src/types.ts @@ -8,7 +8,7 @@ import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -export type { IgnoredReason, ShouldShowFieldInTableHandler } from './utils'; +export type { FieldTypeKnown, IgnoredReason, ShouldShowFieldInTableHandler } from './utils'; export interface EsHitRecord extends Omit { _source?: Record; diff --git a/packages/kbn-unified-field-list/src/utils/field_types/field_types.ts b/packages/kbn-discover-utils/src/utils/field_types.ts similarity index 87% rename from packages/kbn-unified-field-list/src/utils/field_types/field_types.ts rename to packages/kbn-discover-utils/src/utils/field_types.ts index 0851d6dc1e412..320f606dd0d8e 100644 --- a/packages/kbn-unified-field-list/src/utils/field_types/field_types.ts +++ b/packages/kbn-discover-utils/src/utils/field_types.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { FieldTypeKnown } from '../../types'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; + +export type FieldTypeKnown = Exclude< + DataViewField['timeSeriesMetric'] | DataViewField['type'], + undefined +>; /** * Field types for which name and description are defined diff --git a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_name.test.ts b/packages/kbn-discover-utils/src/utils/get_field_type_name.test.ts similarity index 100% rename from packages/kbn-unified-field-list/src/utils/field_types/get_field_type_name.test.ts rename to packages/kbn-discover-utils/src/utils/get_field_type_name.test.ts diff --git a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_name.ts b/packages/kbn-discover-utils/src/utils/get_field_type_name.ts similarity index 59% rename from packages/kbn-unified-field-list/src/utils/field_types/get_field_type_name.ts rename to packages/kbn-discover-utils/src/utils/get_field_type_name.ts index 23ae35a4c2ef3..7e7bd102f3353 100644 --- a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_name.ts +++ b/packages/kbn-discover-utils/src/utils/get_field_type_name.ts @@ -14,7 +14,7 @@ import { KNOWN_FIELD_TYPES } from './field_types'; * A user-friendly name of an unknown field type */ export const UNKNOWN_FIELD_TYPE_MESSAGE = i18n.translate( - 'unifiedFieldList.fieldNameIcons.unknownFieldAriaLabel', + 'discover.fieldNameIcons.unknownFieldAriaLabel', { defaultMessage: 'Unknown field', } @@ -32,7 +32,7 @@ export function getFieldTypeName(type?: string) { if (type === 'source') { // Note that this type is currently not provided, type for _source is undefined - return i18n.translate('unifiedFieldList.fieldNameIcons.sourceFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { defaultMessage: 'Source field', }); } @@ -40,107 +40,107 @@ export function getFieldTypeName(type?: string) { const knownType: KNOWN_FIELD_TYPES = type as KNOWN_FIELD_TYPES; switch (knownType) { case KNOWN_FIELD_TYPES.DOCUMENT: - return i18n.translate('unifiedFieldList.fieldNameIcons.recordAriaLabel', { + return i18n.translate('discover.fieldNameIcons.recordAriaLabel', { defaultMessage: 'Records', }); case KNOWN_FIELD_TYPES.BINARY: - return i18n.translate('unifiedFieldList.fieldNameIcons.binaryAriaLabel', { + return i18n.translate('discover.fieldNameIcons.binaryAriaLabel', { defaultMessage: 'Binary', }); case KNOWN_FIELD_TYPES.BOOLEAN: - return i18n.translate('unifiedFieldList.fieldNameIcons.booleanAriaLabel', { + return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { defaultMessage: 'Boolean', }); case KNOWN_FIELD_TYPES.CONFLICT: - return i18n.translate('unifiedFieldList.fieldNameIcons.conflictFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { defaultMessage: 'Conflict', }); case KNOWN_FIELD_TYPES.COUNTER: - return i18n.translate('unifiedFieldList.fieldNameIcons.counterFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.counterFieldAriaLabel', { defaultMessage: 'Counter metric', }); case KNOWN_FIELD_TYPES.DATE: - return i18n.translate('unifiedFieldList.fieldNameIcons.dateFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { defaultMessage: 'Date', }); case KNOWN_FIELD_TYPES.DATE_RANGE: - return i18n.translate('unifiedFieldList.fieldNameIcons.dateRangeFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.dateRangeFieldAriaLabel', { defaultMessage: 'Date range', }); case KNOWN_FIELD_TYPES.DENSE_VECTOR: - return i18n.translate('unifiedFieldList.fieldNameIcons.denseVectorFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.denseVectorFieldAriaLabel', { defaultMessage: 'Dense vector', }); case KNOWN_FIELD_TYPES.GAUGE: - return i18n.translate('unifiedFieldList.fieldNameIcons.gaugeFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.gaugeFieldAriaLabel', { defaultMessage: 'Gauge metric', }); case KNOWN_FIELD_TYPES.GEO_POINT: - return i18n.translate('unifiedFieldList.fieldNameIcons.geoPointFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { defaultMessage: 'Geo point', }); case KNOWN_FIELD_TYPES.GEO_SHAPE: - return i18n.translate('unifiedFieldList.fieldNameIcons.geoShapeFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { defaultMessage: 'Geo shape', }); case KNOWN_FIELD_TYPES.HISTOGRAM: - return i18n.translate('unifiedFieldList.fieldNameIcons.histogramFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.histogramFieldAriaLabel', { defaultMessage: 'Histogram', }); case KNOWN_FIELD_TYPES.IP: - return i18n.translate('unifiedFieldList.fieldNameIcons.ipAddressFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { defaultMessage: 'IP address', }); case KNOWN_FIELD_TYPES.IP_RANGE: - return i18n.translate('unifiedFieldList.fieldNameIcons.ipRangeFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.ipRangeFieldAriaLabel', { defaultMessage: 'IP range', }); case KNOWN_FIELD_TYPES.FLATTENED: - return i18n.translate('unifiedFieldList.fieldNameIcons.flattenedFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.flattenedFieldAriaLabel', { defaultMessage: 'Flattened', }); case KNOWN_FIELD_TYPES.MURMUR3: - return i18n.translate('unifiedFieldList.fieldNameIcons.murmur3FieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { defaultMessage: 'Murmur3', }); case KNOWN_FIELD_TYPES.NUMBER: - return i18n.translate('unifiedFieldList.fieldNameIcons.numberFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { defaultMessage: 'Number', }); case KNOWN_FIELD_TYPES.RANK_FEATURE: - return i18n.translate('unifiedFieldList.fieldNameIcons.rankFeatureFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.rankFeatureFieldAriaLabel', { defaultMessage: 'Rank feature', }); case KNOWN_FIELD_TYPES.RANK_FEATURES: - return i18n.translate('unifiedFieldList.fieldNameIcons.rankFeaturesFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.rankFeaturesFieldAriaLabel', { defaultMessage: 'Rank features', }); case KNOWN_FIELD_TYPES.POINT: - return i18n.translate('unifiedFieldList.fieldNameIcons.pointFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.pointFieldAriaLabel', { defaultMessage: 'Point', }); case KNOWN_FIELD_TYPES.SHAPE: - return i18n.translate('unifiedFieldList.fieldNameIcons.shapeFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.shapeFieldAriaLabel', { defaultMessage: 'Shape', }); case KNOWN_FIELD_TYPES.STRING: - return i18n.translate('unifiedFieldList.fieldNameIcons.stringFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { defaultMessage: 'String', }); case KNOWN_FIELD_TYPES.TEXT: - return i18n.translate('unifiedFieldList.fieldNameIcons.textFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.textFieldAriaLabel', { defaultMessage: 'Text', }); case KNOWN_FIELD_TYPES.KEYWORD: - return i18n.translate('unifiedFieldList.fieldNameIcons.keywordFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.keywordFieldAriaLabel', { defaultMessage: 'Keyword', }); case KNOWN_FIELD_TYPES.NESTED: - return i18n.translate('unifiedFieldList.fieldNameIcons.nestedFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { defaultMessage: 'Nested', }); case KNOWN_FIELD_TYPES.VERSION: - return i18n.translate('unifiedFieldList.fieldNameIcons.versionFieldAriaLabel', { + return i18n.translate('discover.fieldNameIcons.versionFieldAriaLabel', { defaultMessage: 'Version', }); default: diff --git a/packages/kbn-discover-utils/src/utils/index.ts b/packages/kbn-discover-utils/src/utils/index.ts index 4828fcf82a447..fc8288f533deb 100644 --- a/packages/kbn-discover-utils/src/utils/index.ts +++ b/packages/kbn-discover-utils/src/utils/index.ts @@ -7,9 +7,11 @@ */ export * from './build_data_record'; +export * from './field_types'; export * from './format_hit'; export * from './format_value'; export * from './get_doc_id'; +export * from './get_field_type_name'; export * from './get_ignored_reason'; export * from './get_should_show_field_handler'; export * from './nested_fields'; diff --git a/packages/kbn-discover-utils/types.ts b/packages/kbn-discover-utils/types.ts index 69ea11ea628d8..ea9c48f34c39f 100644 --- a/packages/kbn-discover-utils/types.ts +++ b/packages/kbn-discover-utils/types.ts @@ -9,6 +9,7 @@ export type { DataTableRecord, EsHitRecord, + FieldTypeKnown, IgnoredReason, ShouldShowFieldInTableHandler, } from './src/types'; diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 1b1973e35ec52..a8d63137fa0a9 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -622,6 +622,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { }, apis: { bulkIndexAlias: `${ELASTICSEARCH_DOCS}indices-aliases.html`, + indexStats: `${ELASTICSEARCH_DOCS}indices-stats.html`, byteSizeUnits: `${ELASTICSEARCH_DOCS}api-conventions.html#byte-units`, createAutoFollowPattern: `${ELASTICSEARCH_DOCS}ccr-put-auto-follow-pattern.html`, createFollower: `${ELASTICSEARCH_DOCS}ccr-put-follow.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index b29017f71d739..428ef86267dd1 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -361,6 +361,7 @@ export interface DocLinks { readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; + indexStats: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index 2c23eec3ca2aa..42df7340c1853 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -8,25 +8,15 @@ import type { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; -import { KbnClient } from '@kbn/test'; import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; -import { migrateSavedObjectIndices, createStats, cleanSavedObjectIndices } from '../lib'; +import { createStats, cleanSavedObjectIndices } from '../lib'; -export async function emptyKibanaIndexAction({ - client, - log, - kbnClient, -}: { - client: Client; - log: ToolingLog; - kbnClient: KbnClient; -}) { +export async function emptyKibanaIndexAction({ client, log }: { client: Client; log: ToolingLog }) { const stats = createStats('emptyKibanaIndex', log); await cleanSavedObjectIndices({ client, stats, log }); - await migrateSavedObjectIndices(kbnClient); await client.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); - ALL_SAVED_OBJECT_INDICES.forEach((indexPattern) => stats.createdIndex(indexPattern)); + return stats.toJSON(); } diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index 65de41148d8db..d5c989f080486 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -154,14 +154,12 @@ export class EsArchiver { } /** - * Delete any Kibana indices, and initialize the Kibana index as Kibana would do - * on startup. + * Cleanup saved object indices, preserving the space:default saved object. */ async emptyKibanaIndex() { return await emptyKibanaIndexAction({ client: this.client, log: this.log, - kbnClient: this.kbnClient, }); } diff --git a/packages/kbn-es-query/index.ts b/packages/kbn-es-query/index.ts index 1e0d9092adefb..a14724fc3a748 100644 --- a/packages/kbn-es-query/index.ts +++ b/packages/kbn-es-query/index.ts @@ -54,6 +54,8 @@ export { isOfAggregateQueryType, getAggregateQueryMode, getIndexPatternFromSQLQuery, + getIndexPatternFromESQLQuery, + getLanguageDisplayName, } from './src/es_query'; export { diff --git a/packages/kbn-es-query/src/es_query/build_es_query.ts b/packages/kbn-es-query/src/es_query/build_es_query.ts index 9d1b0b1c6b139..ff9908bcb0cc3 100644 --- a/packages/kbn-es-query/src/es_query/build_es_query.ts +++ b/packages/kbn-es-query/src/es_query/build_es_query.ts @@ -12,7 +12,7 @@ import { buildQueryFromKuery } from './from_kuery'; import { buildQueryFromFilters } from './from_filters'; import { buildQueryFromLucene } from './from_lucene'; import { Filter, Query, AggregateQuery } from '../filters'; -import { isOfQueryType } from './es_query_sql'; +import { isOfQueryType } from './es_aggregate_query'; import { BoolQuery, DataViewBase } from './types'; import type { KueryQueryOptions } from '../kuery'; import type { EsQueryFiltersConfig } from './from_filters'; diff --git a/packages/kbn-es-query/src/es_query/es_query_sql.test.ts b/packages/kbn-es-query/src/es_query/es_aggregate_query.test.ts similarity index 81% rename from packages/kbn-es-query/src/es_query/es_query_sql.test.ts rename to packages/kbn-es-query/src/es_query/es_aggregate_query.test.ts index da909c6e5f9b4..2ab161e0f7517 100644 --- a/packages/kbn-es-query/src/es_query/es_query_sql.test.ts +++ b/packages/kbn-es-query/src/es_query/es_aggregate_query.test.ts @@ -11,7 +11,8 @@ import { isOfAggregateQueryType, getAggregateQueryMode, getIndexPatternFromSQLQuery, -} from './es_query_sql'; + getIndexPatternFromESQLQuery, +} from './es_aggregate_query'; describe('sql query helpers', () => { describe('isOfQueryType', () => { @@ -81,4 +82,20 @@ describe('sql query helpers', () => { expect(idxPattern8).toBe('logstash-1234!'); }); }); + + describe('getIndexPatternFromESQLQuery', () => { + it('should return the index pattern string from esql queries', () => { + const idxPattern1 = getIndexPatternFromESQLQuery('FROM foo'); + expect(idxPattern1).toBe('foo'); + + const idxPattern3 = getIndexPatternFromESQLQuery('from foo | project abc, def'); + expect(idxPattern3).toBe('foo'); + + const idxPattern4 = getIndexPatternFromESQLQuery('from foo | project a | limit 2'); + expect(idxPattern4).toBe('foo'); + + const idxPattern5 = getIndexPatternFromESQLQuery('from foo | limit 2'); + expect(idxPattern5).toBe('foo'); + }); + }); }); diff --git a/packages/kbn-es-query/src/es_query/es_query_sql.ts b/packages/kbn-es-query/src/es_query/es_aggregate_query.ts similarity index 67% rename from packages/kbn-es-query/src/es_query/es_query_sql.ts rename to packages/kbn-es-query/src/es_query/es_aggregate_query.ts index 46de33dc04e86..1e87552e98b83 100644 --- a/packages/kbn-es-query/src/es_query/es_query_sql.ts +++ b/packages/kbn-es-query/src/es_query/es_aggregate_query.ts @@ -28,7 +28,11 @@ export function getAggregateQueryMode(query: AggregateQuery): Language { return Object.keys(query)[0] as Language; } -// retrieves the index pattern from the aggregate query +export function getLanguageDisplayName(language: string): string { + return language === 'esql' ? 'es|ql' : language; +} + +// retrieves the index pattern from the aggregate query for SQL export function getIndexPatternFromSQLQuery(sqlQuery?: string): string { let sql = sqlQuery?.replaceAll('"', '').replaceAll("'", ''); const splitFroms = sql?.split(new RegExp(/FROM\s/, 'ig')); @@ -44,3 +48,20 @@ export function getIndexPatternFromSQLQuery(sqlQuery?: string): string { } return ''; } + +// retrieves the index pattern from the aggregate query for ES|QL +export function getIndexPatternFromESQLQuery(esql?: string): string { + const splitFroms = esql?.split(new RegExp(/FROM\s/, 'ig')); + const fromsLength = splitFroms?.length ?? 0; + if (splitFroms && splitFroms?.length > 2) { + esql = `${splitFroms[fromsLength - 2]} FROM ${splitFroms[fromsLength - 1]}`; + } + const parsedString = esql?.replaceAll('`', ''); + // case insensitive match for the index pattern + const regex = new RegExp(/FROM\s+([\w*-.!@$^()~;]+)/, 'i'); + const matches = parsedString?.match(regex); + if (matches) { + return matches[1]; + } + return ''; +} diff --git a/packages/kbn-es-query/src/es_query/index.ts b/packages/kbn-es-query/src/es_query/index.ts index 1dca3a5524cfb..22141a52e93f5 100644 --- a/packages/kbn-es-query/src/es_query/index.ts +++ b/packages/kbn-es-query/src/es_query/index.ts @@ -18,7 +18,9 @@ export { isOfAggregateQueryType, getAggregateQueryMode, getIndexPatternFromSQLQuery, -} from './es_query_sql'; + getLanguageDisplayName, + getIndexPatternFromESQLQuery, +} from './es_aggregate_query'; export { fromCombinedFilter } from './from_combined_filter'; export type { IFieldSubType, diff --git a/packages/kbn-es/README.mdx b/packages/kbn-es/README.mdx index e6e5727ccb897..90bc36bc1da58 100644 --- a/packages/kbn-es/README.mdx +++ b/packages/kbn-es/README.mdx @@ -10,13 +10,19 @@ tags: ['kibana', 'dev', 'contributor', 'operations', 'es'] > A command line utility for running elasticsearch from snapshot, source, archive, docker, serverless or even building snapshot artifacts. ## Getting started +To run, go to the Kibana root and run `node scripts/es --help` to get the latest command line options. + +The script attempts to preserve the existing interfaces used by Elasticsearch CLI. This includes passing through options with the `-E` argument and the `ES_JAVA_OPTS` environment variable for Java options. + If running elasticsearch from source, elasticsearch needs to be cloned to a sibling directory of Kibana. -If running elasticsearch serverless or a docker container, docker is required to be installed locally. Installation instructions can be found [here](https://www.docker.com/). +### Serverless & Docker Prerequisites +If running elasticsearch serverless or a docker container, there is some required initial setup: -To run, go to the Kibana root and run `node scripts/es --help` to get the latest command line options. +1. Install Docker. Instructions can be found [here](https://www.docker.com/). +1. Authentication with Elastic's Docker registry [here](https://docker-auth.elastic.co/github_auth). +1. Increase OS virtual memory limits. More info in the [ES docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html). -The script attempts to preserve the existing interfaces used by Elasticsearch CLI. This includes passing through options with the `-E` argument and the `ES_JAVA_OPTS` environment variable for Java options. ### Examples diff --git a/packages/kbn-es/index.ts b/packages/kbn-es/index.ts index aed2ab7af41c5..3ccb220be6b52 100644 --- a/packages/kbn-es/index.ts +++ b/packages/kbn-es/index.ts @@ -8,4 +8,9 @@ export { run } from './src/cli'; export { Cluster } from './src/cluster'; -export { SYSTEM_INDICES_SUPERUSER } from './src/utils'; +export { + SYSTEM_INDICES_SUPERUSER, + ELASTIC_SERVERLESS_SUPERUSER, + ELASTIC_SERVERLESS_SUPERUSER_PASSWORD, + getDockerFileMountPath, +} from './src/utils'; diff --git a/packages/kbn-es/src/cli_commands/docker.ts b/packages/kbn-es/src/cli_commands/docker.ts index cb5a57731b907..aad68bf0a1dae 100644 --- a/packages/kbn-es/src/cli_commands/docker.ts +++ b/packages/kbn-es/src/cli_commands/docker.ts @@ -12,7 +12,7 @@ import { ToolingLog } from '@kbn/tooling-log'; import { getTimeReporter } from '@kbn/ci-stats-reporter'; import { Cluster } from '../cluster'; -import { DOCKER_IMG, DOCKER_REPO, DOCKER_TAG } from '../utils'; +import { DOCKER_IMG, DOCKER_REPO, DOCKER_TAG, DEFAULT_PORT } from '../utils'; import { Command } from './types'; export const docker: Command = { @@ -27,8 +27,12 @@ export const docker: Command = { --tag Image tag of ES to run from ${DOCKER_REPO} [default: ${DOCKER_TAG}] --image Full path to image of ES to run, has precedence over tag. [default: ${DOCKER_IMG}] --password Sets password for elastic user [default: ${password}] + --port The port to bind to on 127.0.0.1 [default: ${DEFAULT_PORT}] + --ssl Sets up SSL and enables security plugin on Elasticsearch + --kill Kill running ES nodes if detected -E Additional key=value settings to pass to Elasticsearch -D Override Docker command + -F Absolute paths for files to mount into container Examples: @@ -50,9 +54,11 @@ export const docker: Command = { alias: { esArgs: 'E', dockerCmd: 'D', + files: 'F', }, string: ['tag', 'image', 'D'], + boolean: ['ssl', 'kill'], default: defaults, }); diff --git a/packages/kbn-es/src/cli_commands/serverless.ts b/packages/kbn-es/src/cli_commands/serverless.ts index ab2d6d4b63926..51cc1b619017a 100644 --- a/packages/kbn-es/src/cli_commands/serverless.ts +++ b/packages/kbn-es/src/cli_commands/serverless.ts @@ -11,8 +11,8 @@ import getopts from 'getopts'; import { ToolingLog } from '@kbn/tooling-log'; import { getTimeReporter } from '@kbn/ci-stats-reporter'; -import { Cluster } from '../cluster'; -import { SERVERLESS_REPO, SERVERLESS_TAG, SERVERLESS_IMG } from '../utils'; +import { Cluster, type ServerlessOptions } from '../cluster'; +import { SERVERLESS_REPO, SERVERLESS_TAG, SERVERLESS_IMG, DEFAULT_PORT } from '../utils'; import { Command } from './types'; export const serverless: Command = { @@ -22,10 +22,15 @@ export const serverless: Command = { return dedent` Options: - --tag Image tag of ES Serverless to run from ${SERVERLESS_REPO} [default: ${SERVERLESS_TAG}] - --image Full path of ES Serverless image to run, has precedence over tag. [default: ${SERVERLESS_IMG}] + --tag Image tag of ESS to run from ${SERVERLESS_REPO} [default: ${SERVERLESS_TAG}] + --image Full path of ESS image to run, has precedence over tag. [default: ${SERVERLESS_IMG}] --clean Remove existing file system object store before running + --port The port to bind to on 127.0.0.1 [default: ${DEFAULT_PORT}] + --ssl Sets up SSL and enables security plugin on Elasticsearch + --kill Kill running ESS nodes if detected + --background Start ESS without attaching to the first node's logs -E Additional key=value settings to pass to Elasticsearch + -F Absolute paths for files to mount into containers Examples: @@ -46,13 +51,14 @@ export const serverless: Command = { alias: { basePath: 'base-path', esArgs: 'E', + files: 'F', }, string: ['tag', 'image'], - boolean: ['clean'], + boolean: ['clean', 'ssl', 'kill', 'background'], default: defaults, - }); + }) as unknown as ServerlessOptions; const cluster = new Cluster(); await cluster.runServerless({ diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index b88e4a788fb72..084cb9c601578 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -16,13 +16,15 @@ const { Client } = require('@elastic/elasticsearch'); const { downloadSnapshot, installSnapshot, installSource, installArchive } = require('./install'); const { ES_BIN, ES_PLUGIN_BIN, ES_KEYSTORE_BIN } = require('./paths'); const { - log: defaultLog, - parseEsLog, extractConfigFiles, + log: defaultLog, NativeRealm, + parseEsLog, parseTimeoutToMs, - runServerlessCluster, runDockerContainer, + runServerlessCluster, + stopServerlessCluster, + teardownServerlessClusterSync, } = require('./utils'); const { createCliError } = require('./errors'); const { promisify } = require('util'); @@ -34,7 +36,7 @@ const DEFAULT_READY_TIMEOUT = parseTimeoutToMs('1m'); /** @typedef {import('./cluster_exec_options').EsClusterExecOptions} ExecOptions */ /** @typedef {import('./utils').DockerOptions} DockerOptions */ -/** @typedef {import('./utils').ServerlessOptions}ServerlessrOptions */ +/** @typedef {import('./utils').ServerlessOptions}ServerlessOptions */ // listen to data on stream until map returns anything but undefined const first = (stream, map) => @@ -276,6 +278,10 @@ exports.Cluster = class Cluster { } this._stopCalled = true; + if (this._serverlessNodes?.length) { + return await stopServerlessCluster(this._log, this._serverlessNodes); + } + if (!this._process || !this._outcome) { throw new Error('ES has not been started'); } @@ -295,6 +301,10 @@ exports.Cluster = class Cluster { this._stopCalled; + if (this._serverlessNodes?.length) { + return await stopServerlessCluster(this._log, this._serverlessNodes); + } + if (!this._process || !this._outcome) { throw new Error('ES has not been started'); } @@ -573,7 +583,15 @@ exports.Cluster = class Cluster { throw new Error('ES has already been started'); } - await runServerlessCluster(this._log, options); + this._serverlessNodes = await runServerlessCluster(this._log, options); + + if (options.teardown) { + /** + * Ideally would be async and an event like beforeExit or SIGINT, + * but those events are not being triggered in FTR child process. + */ + process.on('exit', () => teardownServerlessClusterSync(this._log, options)); + } } /** diff --git a/packages/kbn-es/src/ess_resources/README.md b/packages/kbn-es/src/ess_resources/README.md new file mode 100644 index 0000000000000..a7af386bcff1f --- /dev/null +++ b/packages/kbn-es/src/ess_resources/README.md @@ -0,0 +1,49 @@ +# Elasticsearch Serverless Resources +The resources in this directory are used for seeding Elasticsearch Serverless (ESS) images with users, roles and tokens for SSL and authentication. ESS requires file realm authentication, so we will bind mount them into the containers at `/usr/share/elasticsearch/config/`. + +## Users + +### Default user + +The default superuser authentication to login to Kibana is: + +``` +username: elastic_serverless +password: changeme +``` + +### Adding users + +1. Add the user:encrypted_password to `users` file. The encrypted password for `elastic_serverless` is `changeme` if you want to reuse the value. +1. Set the new user's roles in `users_roles` file. +1. Add the username to `operator_users.yml` in the array for file realm users. + + +## Service Account and Tokens + +This section for Service Accounts was originally from the [ESS repository](https://github.com/elastic/elasticsearch-serverless/blob/main/serverless-build-tools/src/main/resources/README.service_tokens.md). + +The "service_tokens" file contains this line: +``` +elastic/kibana/kibana-dev:$2a$10$mY2RuGROhk56vLNh.Mgwue98BnkdQPlTR.yGh38ao5jhPJobvuBCq +``` + +That line defines a single service token +- For the `elastic/kibana` service account +- The token is named `kibana-dev` +- The token's secret is hashed using bcrypt (`$2a$`) using `10` rounds + +Although Elasticsearch used PBKDF2_STRETCH by default, the k8s controller +creates tokens using bcrypt, so we mimic that here. + +The hash is not reversible, so this README is here to tell you what the secret is. +The secret value is: `UUUUUULK-* Z4` +That produces an encoded token of: `AAEAAWVsYXN0aWMva2liYW5hL2tpYmFuYS1kZXY6VVVVVVVVTEstKiBaNA` +Yes, the secret was specially chosen to produce an encoded value that can be more easily recognised in development. + +If a node is configured to use this `service_tokens` file, then you can authenticate to it with +``` +curl -H "Authorization: Bearer AAEAAWVsYXN0aWMva2liYW5hL2tpYmFuYS1kZXY6VVVVVVVVTEstKiBaNA" http://localhost:9200/_security/_authenticate +``` + +The name of the token (`kibana-dev`) is important because the `operator_users.yml` file designates that token as an operator and allows us to seed an ESS cluster with this token. \ No newline at end of file diff --git a/packages/kbn-es/src/ess_resources/jwks.json b/packages/kbn-es/src/ess_resources/jwks.json new file mode 100644 index 0000000000000..944705b31416b --- /dev/null +++ b/packages/kbn-es/src/ess_resources/jwks.json @@ -0,0 +1,10 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "n": "v9-88aGdE4E85PuEycxTA6LkM3TBvNScoeP6A-dd0Myo6-LfBlp1r7BPBWmvi_SC6Zam3U1LE3AekDMwqJg304my0pvh8wOwlmRpgKXDXjvj4s59vdeVNhCB9doIthUABd310o9lyb55fWc_qQYE2LK9AyEjicJswafguH6txV4IwSl13ieZAxni0Ca4CwdzXO1Oi34XjHF8F5x_0puTaQzHn5bPG4fiIJN-pwie0Ba4VEDPO5ca4lLXWVi1bn8xMDTAULrBAXJwDaDdS05KMbc4sPlyQPhtY1gcYvUbozUPYxSWwA7fZgFzV_h-uy_oXf1EXttOxSgog1z3cJzf6Q" + } + ] +} diff --git a/packages/kbn-es/src/ess_resources/operator_users.yml b/packages/kbn-es/src/ess_resources/operator_users.yml new file mode 100644 index 0000000000000..859226f258ebf --- /dev/null +++ b/packages/kbn-es/src/ess_resources/operator_users.yml @@ -0,0 +1,9 @@ +operator: + - usernames: ["elastic_serverless", "system_indices_superuser"] + realm_type: "file" + auth_type: "realm" + - usernames: [ "elastic/kibana" ] + realm_type: "_service_account" + auth_type: "token" + token_source: "file" + token_names: [ "kibana-dev" ] diff --git a/packages/kbn-es/src/ess_resources/role_mapping.yml b/packages/kbn-es/src/ess_resources/role_mapping.yml new file mode 100644 index 0000000000000..882bf8a76fd16 --- /dev/null +++ b/packages/kbn-es/src/ess_resources/role_mapping.yml @@ -0,0 +1,14 @@ +# Role mapping configuration file which has elasticsearch roles as keys +# that map to one or more user or group distinguished names + +#roleA: this is an elasticsearch role +# - groupA-DN this is a group distinguished name +# - groupB-DN +# - user1-DN this is the full user distinguished name + +#power_user: +# - "cn=admins,dc=example,dc=com" +#user: +# - "cn=users,dc=example,dc=com" +# - "cn=admins,dc=example,dc=com" +# - "cn=John Doe,cn=other users,dc=example,dc=com" \ No newline at end of file diff --git a/packages/kbn-es/src/ess_resources/roles.yml b/packages/kbn-es/src/ess_resources/roles.yml new file mode 100644 index 0000000000000..38d3ecfa77d97 --- /dev/null +++ b/packages/kbn-es/src/ess_resources/roles.yml @@ -0,0 +1,791 @@ +system_indices_superuser: + cluster: ['all'] + indices: + - names: ['*'] + privileges: ['all'] + allow_restricted_indices: true + applications: + - application: '*' + privileges: ['*'] + resources: ['*'] + run_as: ['*'] + +# ----- +# Source: https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/roles.yml +# and: https://github.com/elastic/project-controller/blob/main/internal/project/esproject/config/roles.yml +# ----- +viewer: + cluster: [] + indices: + - names: + - "*" + privileges: + - read + - names: + - "/~(([.]|ilm-history-).*)/" + privileges: + - "read" + - "view_index_metadata" + allow_restricted_indices: false + - names: + - ".siem-signals*" + - ".lists-*" + - ".items-*" + privileges: + - "read" + - "view_index_metadata" + allow_restricted_indices: false + - names: + - ".alerts*" + - ".preview.alerts*" + privileges: + - "read" + - "view_index_metadata" + allow_restricted_indices: false + applications: + - application: "kibana-.kibana" + privileges: + - "read" + resources: + - "*" + run_as: [] +editor: + cluster: [] + indices: + - names: + - "/~(([.]|ilm-history-).*)/" + privileges: + - "read" + - "view_index_metadata" + allow_restricted_indices: false + - names: + - "observability-annotations" + privileges: + - "read" + - "view_index_metadata" + - "write" + allow_restricted_indices: false + - names: + - ".siem-signals*" + - ".lists-*" + - ".items-*" + privileges: + - "read" + - "view_index_metadata" + - "write" + - "maintenance" + allow_restricted_indices: false + - names: + - ".internal.alerts*" + - ".alerts*" + - ".internal.preview.alerts*" + - ".preview.alerts*" + privileges: + - "read" + - "view_index_metadata" + - "write" + - "maintenance" + allow_restricted_indices: false + applications: + - application: "kibana-.kibana" + privileges: + - "all" + resources: + - "*" + run_as: [] + +# ----- +# Source: https://github.com/elastic/project-controller/blob/main/internal/project/security/config/roles.yml +# ----- +t1_analyst: + cluster: + indices: + - names: + - ".alerts-security*" + - ".siem-signals-*" + privileges: + - read + - write + - maintenance + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + - metrics-endpoint.metadata_current_* + - ".fleet-agents*" + - ".fleet-actions*" + privileges: + - read + applications: + - application: ml + privileges: + - read + resources: "*" + - application: siem + privileges: + - read + - read_alerts + - endpoint_list_read + resources: "*" + - application: securitySolutionCases + privileges: + - read + resources: "*" + - application: actions + privileges: + - read + resources: "*" + - application: builtInAlerts + privileges: + - read + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + - application: osquery + privileges: + - read + - run_saved_queries + resources: "*" + +t2_analyst: + cluster: + indices: + - names: + - .alerts-security* + - .siem-signals-* + privileges: + - read + - write + - maintenance + - names: + - .lists* + - .items* + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + - metrics-endpoint.metadata_current_* + - .fleet-agents* + - .fleet-actions* + privileges: + - read + applications: + - application: ml + privileges: + - read + resources: "*" + - application: siem + privileges: + - read + - read_alerts + - endpoint_list_read + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - read + resources: "*" + - application: builtInAlerts + privileges: + - read + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + - application: osquery + privileges: + - read + - run_saved_queries + resources: "*" + +t3_analyst: + cluster: + indices: + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + privileges: + - read + - write + - names: + - .alerts-security* + - .siem-signals-* + privileges: + - read + - write + - names: + - .lists* + - .items* + privileges: + - read + - write + - names: + - metrics-endpoint.metadata_current_* + - .fleet-agents* + - .fleet-actions* + privileges: + - read + applications: + - application: ml + privileges: + - read + resources: "*" + - application: siem + privileges: + - all + - read_alerts + - crud_alerts + - endpoint_list_all + - trusted_applications_all + - event_filters_all + - host_isolation_exceptions_all + - blocklist_all + - policy_management_read # Elastic Defend Policy Management + - host_isolation_all + - process_operations_all + - actions_log_management_all # Response actions history + - file_operations_all + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - read + resources: "*" + - application: builtInAlerts + privileges: + - all + resources: "*" + - application: osquery + privileges: + - all + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + +threat_intelligence_analyst: + cluster: + indices: + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - .lists* + - .items* + - packetbeat-* + - winlogbeat-* + privileges: + - read + - names: + - .alerts-security* + - .siem-signals-* + privileges: + - read + - write + - maintenance + - names: + - metrics-endpoint.metadata_current_* + - .fleet-agents* + - .fleet-actions* + privileges: + - read + applications: + - application: ml + privileges: + - read + resources: "*" + - application: siem + privileges: + - read + - read_alerts + - endpoint_list_read + - blocklist_all + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - read + resources: "*" + - application: builtInAlerts + privileges: + - read + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + - application: osquery + privileges: + - all + resources: "*" + +rule_author: + cluster: + indices: + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + privileges: + - read + - write + - names: + - .alerts-security* + - .siem-signals-* + - .internal.preview.alerts-security* + - .preview.alerts-security* + privileges: + - read + - write + - maintenance + - view_index_metadata + - names: + - .lists* + - .items* + privileges: + - read + - write + - names: + - metrics-endpoint.metadata_current_* + - .fleet-agents* + - .fleet-actions* + privileges: + - read + applications: + - application: ml + privileges: + - read + resources: "*" + - application: siem + privileges: + - all + - read_alerts + - crud_alerts + - policy_management_all + - endpoint_list_all + - trusted_applications_all + - event_filters_all + - host_isolation_exceptions_read + - blocklist_all + - actions_log_management_read + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - read + resources: "*" + - application: builtInAlerts + privileges: + - all + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + +soc_manager: + cluster: + indices: + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + privileges: + - read + - write + - names: + - .alerts-security* + - .siem-signals-* + - .preview.alerts-security* + - .internal.preview.alerts-security* + privileges: + - read + - write + - manage + - names: + - .lists* + - .items* + privileges: + - read + - write + - names: + - metrics-endpoint.metadata_current_* + - .fleet-agents* + - .fleet-actions* + privileges: + - read + applications: + - application: ml + privileges: + - read + resources: "*" + - application: siem + privileges: + - all + - read_alerts + - crud_alerts + - policy_management_all + - endpoint_list_all + - trusted_applications_all + - event_filters_all + - host_isolation_exceptions_all + - blocklist_all + - host_isolation_all + - process_operations_all + - actions_log_management_all + - file_operations_all + - execute_operations_all + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - all + resources: "*" + - application: builtInAlerts + privileges: + - all + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + - application: osquery + privileges: + - all + resources: "*" + - application: savedObjectsManagement + privileges: + - all + resources: "*" + +detections_admin: + cluster: + indices: + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + - .lists* + - .items* + - .alerts-security* + - .siem-signals-* + - .preview.alerts-security* + - .internal.preview.alerts-security* + privileges: + - read + - write + - manage + - names: + - metrics-endpoint.metadata_current_* + - .fleet-agents* + - .fleet-actions* + privileges: + - read + applications: + - application: ml + privileges: + - all + resources: "*" + - application: siem + privileges: + - all + - read_alerts + - crud_alerts + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - read + resources: "*" + - application: builtInAlerts + privileges: + - all + resources: "*" + - application: dev_tools + privileges: + - all + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + +platform_engineer: + cluster: + - manage + indices: + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + - .lists* + - .items* + - .alerts-security* + - .siem-signals-* + - .preview.alerts-security* + - .internal.preview.alerts-security* + privileges: + - all + applications: + - application: ml + privileges: + - all + resources: "*" + - application: siem + privileges: + - all + - read_alerts + - crud_alerts + - policy_management_all + - endpoint_list_all + - trusted_applications_all + - event_filters_all + - host_isolation_exceptions_all + - blocklist_all + - actions_log_management_read + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - all + resources: "*" + - application: builtInAlerts + privileges: + - all + resources: "*" + - application: fleet + privileges: + - all + resources: "*" + - application: fleetv2 + privileges: + - all + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + - application: osquery + privileges: + - all + resources: "*" + +endpoint_operations_analyst: + cluster: + indices: + - names: + - metrics-endpoint.metadata_current_* + - .fleet-agents* + - .fleet-actions* + privileges: + - read + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + - .lists* + - .items* + privileges: + - read + - names: + - .alerts-security* + - .siem-signals-* + - .preview.alerts-security* + - .internal.preview.alerts-security* + privileges: + - read + - write + applications: + - application: ml + privileges: + - read + resources: "*" + - application: siem + privileges: + - all + - read_alerts + - policy_management_all + - endpoint_list_all + - trusted_applications_all + - event_filters_all + - host_isolation_exceptions_all + - blocklist_all + - host_isolation_all + - process_operations_all + - actions_log_management_all # Response History + - file_operations_all + - execute_operations_all # Execute + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - all + resources: "*" + - application: builtInAlerts + privileges: + - all + resources: "*" + - application: osquery + privileges: + - all + resources: "*" + - application: fleet + privileges: + - all + resources: "*" + - application: fleetv2 + privileges: + - all + resources: "*" + - application: spaces + privileges: + - all + resources: "*" + +endpoint_policy_manager: + cluster: + indices: + - names: + - metrics-endpoint.metadata_current_* + - .fleet-agents* + - .fleet-actions* + privileges: + - read + - names: + - apm-*-transaction* + - traces-apm* + - auditbeat-* + - endgame-* + - filebeat-* + - logs-* + - packetbeat-* + - winlogbeat-* + - .lists* + - .items* + privileges: + - read + - names: + - .alerts-security* + - .siem-signals-* + - .preview.alerts-security* + - .internal.preview.alerts-security* + privileges: + - read + - write + - manage + applications: + - application: ml + privileges: + - read + resources: "*" + - application: siem + privileges: + - all + - read_alerts + - crud_alerts + - policy_management_all + - trusted_applications_all + - event_filters_all + - host_isolation_exceptions_all + - blocklist_all + - endpoint_list_all + resources: "*" + - application: securitySolutionCases + privileges: + - all + resources: "*" + - application: actions + privileges: + - all + resources: "*" + - application: builtInAlerts + privileges: + - all + resources: "*" + - application: osquery + privileges: + - all + resources: "*" + - application: fleet + privileges: + - all + resources: "*" + - application: fleetv2 + privileges: + - all + resources: "*" + - application: spaces + privileges: + - all + resources: "*" diff --git a/packages/kbn-es/src/ess_resources/secrets.json b/packages/kbn-es/src/ess_resources/secrets.json new file mode 100644 index 0000000000000..ceb7366ee5321 --- /dev/null +++ b/packages/kbn-es/src/ess_resources/secrets.json @@ -0,0 +1,11 @@ +{ + "metadata": { + "version": "1", + "compatibility": "8.11.0" + }, + "string_secrets": { + "xpack.security.http.ssl.keystore.secure_password": "storepass", + "xpack.security.transport.ssl.keystore.secure_password": "storepass", + "xpack.security.authc.realms.jwt.jwt1.client_authentication.shared_secret": "my_super_secret" + } +} \ No newline at end of file diff --git a/packages/kbn-es/src/ess_resources/service_tokens b/packages/kbn-es/src/ess_resources/service_tokens new file mode 100644 index 0000000000000..34bc7476f177f --- /dev/null +++ b/packages/kbn-es/src/ess_resources/service_tokens @@ -0,0 +1 @@ +elastic/kibana/kibana-dev:$2a$10$mY2RuGROhk56vLNh.Mgwue98BnkdQPlTR.yGh38ao5jhPJobvuBCq \ No newline at end of file diff --git a/packages/kbn-es/src/ess_resources/users b/packages/kbn-es/src/ess_resources/users new file mode 100644 index 0000000000000..add4b7325c23d --- /dev/null +++ b/packages/kbn-es/src/ess_resources/users @@ -0,0 +1,2 @@ +elastic_serverless:$2a$10$nN6sRtQl2KX9Gn8kV/.NpOLSk6Jwn8TehEDnZ7aaAgzyl/dy5PYzW +system_indices_superuser:$2a$10$nN6sRtQl2KX9Gn8kV/.NpOLSk6Jwn8TehEDnZ7aaAgzyl/dy5PYzW diff --git a/packages/kbn-es/src/ess_resources/users_roles b/packages/kbn-es/src/ess_resources/users_roles new file mode 100644 index 0000000000000..aa42046898601 --- /dev/null +++ b/packages/kbn-es/src/ess_resources/users_roles @@ -0,0 +1,2 @@ +superuser:elastic_serverless +system_indices_superuser:system_indices_superuser diff --git a/packages/kbn-es/src/paths.ts b/packages/kbn-es/src/paths.ts index 1d909f523302e..76cf4271c7ce8 100644 --- a/packages/kbn-es/src/paths.ts +++ b/packages/kbn-es/src/paths.ts @@ -7,7 +7,7 @@ */ import Os from 'os'; -import Path from 'path'; +import { resolve } from 'path'; function maybeUseBat(bin: string) { return Os.platform().startsWith('win') ? `${bin}.bat` : bin; @@ -15,7 +15,7 @@ function maybeUseBat(bin: string) { const tempDir = Os.tmpdir(); -export const BASE_PATH = Path.resolve(tempDir, 'kbn-es'); +export const BASE_PATH = resolve(tempDir, 'kbn-es'); export const GRADLE_BIN = maybeUseBat('./gradlew'); export const ES_BIN = maybeUseBat('bin/elasticsearch'); @@ -23,3 +23,30 @@ export const ES_PLUGIN_BIN = maybeUseBat('bin/elasticsearch-plugin'); export const ES_CONFIG = 'config/elasticsearch.yml'; export const ES_KEYSTORE_BIN = maybeUseBat('./bin/elasticsearch-keystore'); + +export const ESS_OPERATOR_USERS_PATH = resolve(__dirname, './ess_resources/operator_users.yml'); +export const ESS_SERVICE_TOKENS_PATH = resolve(__dirname, './ess_resources/service_tokens'); + +export const ESS_USERS_PATH = resolve(__dirname, './ess_resources/users'); +export const ESS_USERS_ROLES_PATH = resolve(__dirname, './ess_resources/users_roles'); + +export const ESS_ROLES_PATH = resolve(__dirname, './ess_resources/roles.yml'); +export const ESS_ROLE_MAPPING_PATH = resolve(__dirname, './ess_resources/role_mapping.yml'); + +export const ESS_SECRETS_PATH = resolve(__dirname, './ess_resources/secrets.json'); + +export const ESS_JWKS_PATH = resolve(__dirname, './ess_resources/jwks.json'); + +export const ESS_RESOURCES_PATHS = [ + ESS_OPERATOR_USERS_PATH, + ESS_ROLE_MAPPING_PATH, + ESS_ROLES_PATH, + ESS_SERVICE_TOKENS_PATH, + ESS_USERS_PATH, + ESS_USERS_ROLES_PATH, +]; + +export const ESS_CONFIG_PATH = '/usr/share/elasticsearch/config/'; + +// Files need to be inside config for permissions reasons inside the container +export const ESS_FILES_PATH = `${ESS_CONFIG_PATH}files/`; diff --git a/packages/kbn-es/src/utils/docker.test.ts b/packages/kbn-es/src/utils/docker.test.ts index ef978fe76c409..35696e1f91af8 100644 --- a/packages/kbn-es/src/utils/docker.test.ts +++ b/packages/kbn-es/src/utils/docker.test.ts @@ -12,21 +12,33 @@ import { stat } from 'fs/promises'; import { DOCKER_IMG, + detectRunningNodes, maybeCreateDockerNetwork, + maybePullDockerImage, resolveDockerCmd, resolveDockerImage, resolveEsArgs, + resolvePort, runDockerContainer, runServerlessCluster, runServerlessEsNode, SERVERLESS_IMG, setupServerlessVolumes, + stopServerlessCluster, + teardownServerlessClusterSync, verifyDockerInstalled, } from './docker'; import { ToolingLog, ToolingLogCollectingWriter } from '@kbn/tooling-log'; +import { ES_P12_PATH } from '@kbn/dev-utils'; +import { ESS_RESOURCES_PATHS } from '../paths'; jest.mock('execa'); const execa = jest.requireMock('execa'); +jest.mock('@elastic/elasticsearch', () => { + return { + Client: jest.fn(), + }; +}); const log = new ToolingLog(); const logWriter = new ToolingLogCollectingWriter(); @@ -62,7 +74,7 @@ const volumeCmdTest = async (volumeCmd: string[]) => { // extract only permission from mode // eslint-disable-next-line no-bitwise - expect((await stat(serverlessObjectStorePath)).mode & 0o777).toBe(0o766); + expect((await stat(serverlessObjectStorePath)).mode & 0o777).toBe(0o777); }; describe('resolveDockerImage()', () => { @@ -103,6 +115,32 @@ describe('resolveDockerImage()', () => { }); }); +describe('resolvePort()', () => { + test('should return default port when no options', () => { + const port = resolvePort({}); + + expect(port).toMatchInlineSnapshot(` + Array [ + "-p", + "127.0.0.1:9200:9200", + ] + `); + }); + + test('should return custom port when passed in options', () => { + const port = resolvePort({ port: 9220 }); + + expect(port).toMatchInlineSnapshot(` + Array [ + "-p", + "127.0.0.1:9220:9220", + "--env", + "http.port=9220", + ] + `); + }); +}); + describe('verifyDockerInstalled()', () => { test('should call the correct Docker command and log the version', async () => { execa.mockImplementationOnce(() => Promise.resolve({ stdout: 'Docker Version 123' })); @@ -190,6 +228,55 @@ describe('maybeCreateDockerNetwork()', () => { }); }); +describe('maybePullDockerImage()', () => { + test('should pull the passed image', async () => { + execa.mockImplementationOnce(() => Promise.resolve({ exitCode: 0 })); + + await maybePullDockerImage(log, DOCKER_IMG); + + expect(execa.mock.calls[0][0]).toEqual('docker'); + expect(execa.mock.calls[0][1]).toEqual(expect.arrayContaining(['pull', DOCKER_IMG])); + }); +}); + +describe('detectRunningNodes()', () => { + const nodes = ['es01', 'es02', 'es03']; + + test('should not error if no nodes detected', async () => { + execa.mockImplementationOnce(() => Promise.resolve({ stdout: '' })); + + await detectRunningNodes(log, {}); + + expect(execa.mock.calls).toHaveLength(1); + expect(execa.mock.calls[0][1]).toEqual(expect.arrayContaining(['ps', '--quiet', '--filter'])); + }); + + test('should kill nodes if detected and kill passed', async () => { + execa.mockImplementationOnce(() => + Promise.resolve({ + stdout: nodes.join('\n'), + }) + ); + + await detectRunningNodes(log, { kill: true }); + + expect(execa.mock.calls).toHaveLength(2); + expect(execa.mock.calls[1][1]).toEqual(expect.arrayContaining(nodes.concat('kill'))); + }); + + test('should error if nodes detected and kill not passed', async () => { + execa.mockImplementationOnce(() => + Promise.resolve({ + stdout: nodes.join('\n'), + }) + ); + + await expect(detectRunningNodes(log, {})).rejects.toThrowErrorMatchingInlineSnapshot( + `"ES has already been started, pass --kill to automatically stop the nodes on startup."` + ); + }); +}); + describe('resolveEsArgs()', () => { const defaultEsArgs: Array<[string, string]> = [ ['foo', 'bar'], @@ -253,6 +340,39 @@ describe('resolveEsArgs()', () => { ] `); }); + + test('should add SSL args and enable security when SSL is passed', () => { + const esArgs = resolveEsArgs([...defaultEsArgs, ['xpack.security.enabled', 'false']], { + ssl: true, + }); + + expect(esArgs).toHaveLength(20); + expect(esArgs).not.toEqual(expect.arrayContaining(['xpack.security.enabled=false'])); + expect(esArgs).toMatchInlineSnapshot(` + Array [ + "--env", + "foo=bar", + "--env", + "qux=zip", + "--env", + "xpack.security.enabled=true", + "--env", + "xpack.security.http.ssl.enabled=true", + "--env", + "xpack.security.http.ssl.keystore.path=/usr/share/elasticsearch/config/certs/elasticsearch.p12", + "--env", + "xpack.security.http.ssl.verification_mode=certificate", + "--env", + "xpack.security.transport.ssl.enabled=true", + "--env", + "xpack.security.transport.ssl.keystore.path=/usr/share/elasticsearch/config/certs/elasticsearch.p12", + "--env", + "xpack.security.transport.ssl.verification_mode=certificate", + "--env", + "xpack.security.operator_privileges.enabled=true", + ] + `); + }); }); describe('setupServerlessVolumes()', () => { @@ -292,6 +412,20 @@ describe('setupServerlessVolumes()', () => { volumeCmdTest(volumeCmd); expect(existsSync(`${serverlessObjectStorePath}/cluster_state/lease`)).toBe(false); }); + + test('should add SSL volumes when ssl is passed', async () => { + mockFs(existingObjectStore); + + const volumeCmd = await setupServerlessVolumes(log, { basePath: baseEsPath, ssl: true }); + + const requiredPaths = [`${baseEsPath}:/objectstore:z`, ES_P12_PATH, ...ESS_RESOURCES_PATHS]; + const pathsNotIncludedInCmd = requiredPaths.filter( + (path) => !volumeCmd.some((cmd) => cmd.includes(path)) + ); + + expect(volumeCmd).toHaveLength(20); + expect(pathsNotIncludedInCmd).toEqual([]); + }); }); describe('runServerlessEsNode()', () => { @@ -333,8 +467,65 @@ describe('runServerlessCluster()', () => { await runServerlessCluster(log, { basePath: baseEsPath }); - // Verify Docker and network then run three nodes - expect(execa.mock.calls).toHaveLength(5); + // setupDocker execa calls then run three nodes and attach logger + expect(execa.mock.calls).toHaveLength(8); + }); + describe('waitForReady', () => { + test('should wait for serverless nodes to be ready to serve requests', async () => { + mockFs({ + [baseEsPath]: {}, + }); + execa.mockImplementation(() => Promise.resolve({ stdout: '' })); + const info = jest.fn(); + jest.requireMock('@elastic/elasticsearch').Client.mockImplementation(() => ({ info })); + + info.mockImplementationOnce(() => Promise.reject()); // first call fails + info.mockImplementationOnce(() => Promise.resolve()); // then succeeds + + await runServerlessCluster(log, { basePath: baseEsPath, waitForReady: true }); + expect(info).toHaveBeenCalledTimes(2); + }); + }); +}); + +describe('stopServerlessCluster()', () => { + test('should stop passed in nodes', async () => { + const nodes = ['es01', 'es02', 'es03']; + execa.mockImplementation(() => Promise.resolve({ stdout: '' })); + + await stopServerlessCluster(log, nodes); + + expect(execa.mock.calls[0][0]).toEqual('docker'); + expect(execa.mock.calls[0][1]).toEqual( + expect.arrayContaining(['container', 'stop'].concat(nodes)) + ); + }); +}); + +describe('teardownServerlessClusterSync()', () => { + const defaultOptions = { basePath: 'foo/bar' }; + + test('should kill running serverless nodes', () => { + const nodes = ['es01', 'es02', 'es03']; + execa.commandSync.mockImplementation(() => ({ + stdout: nodes.join('\n'), + })); + + teardownServerlessClusterSync(log, defaultOptions); + + expect(execa.commandSync.mock.calls).toHaveLength(2); + expect(execa.commandSync.mock.calls[0][0]).toEqual(expect.stringContaining(SERVERLESS_IMG)); + expect(execa.commandSync.mock.calls[1][0]).toEqual(`docker kill ${nodes.join(' ')}`); + }); + + test('should not kill if no serverless nodes', () => { + execa.commandSync.mockImplementation(() => ({ + stdout: '\n', + })); + + teardownServerlessClusterSync(log, defaultOptions); + + expect(execa.commandSync.mock.calls).toHaveLength(1); }); }); @@ -364,7 +555,7 @@ describe('runDockerContainer()', () => { execa.mockImplementation(() => Promise.resolve({ stdout: '' })); await expect(runDockerContainer(log, {})).resolves.toEqual({ stdout: '' }); - // Verify Docker and network then run container - expect(execa.mock.calls).toHaveLength(3); + // setupDocker execa calls then run container + expect(execa.mock.calls).toHaveLength(5); }); }); diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index 6552545e10a3e..01db89a14c6ab 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -9,17 +9,36 @@ import chalk from 'chalk'; import execa from 'execa'; import fs from 'fs'; import Fsp from 'fs/promises'; -import { resolve } from 'path'; +import { resolve, basename, join } from 'path'; +import { Client, HttpConnection } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; -import { kibanaPackageJson as pkg } from '@kbn/repo-info'; +import { kibanaPackageJson as pkg, REPO_ROOT } from '@kbn/repo-info'; +import { ES_P12_PASSWORD, ES_P12_PATH } from '@kbn/dev-utils'; import { createCliError } from '../errors'; import { EsClusterExecOptions } from '../cluster_exec_options'; +import { + ESS_RESOURCES_PATHS, + ESS_SECRETS_PATH, + ESS_JWKS_PATH, + ESS_CONFIG_PATH, + ESS_FILES_PATH, +} from '../paths'; +import { + ELASTIC_SERVERLESS_SUPERUSER, + ELASTIC_SERVERLESS_SUPERUSER_PASSWORD, +} from './ess_file_realm'; +import { SYSTEM_INDICES_SUPERUSER } from './native_realm'; interface BaseOptions { tag?: string; image?: string; + port?: number; + ssl?: boolean; + /** Kill running cluster before starting a new cluster */ + kill?: boolean; + files?: string | string[]; } export interface DockerOptions extends EsClusterExecOptions, BaseOptions { @@ -27,8 +46,16 @@ export interface DockerOptions extends EsClusterExecOptions, BaseOptions { } export interface ServerlessOptions extends EsClusterExecOptions, BaseOptions { + /** Clean (or delete) all data created by the ES cluster after it is stopped */ clean?: boolean; + /** Path to the directory where the ES cluster will store data */ basePath: string; + /** If this process exits, teardown the ES cluster as well */ + teardown?: boolean; + /** Start the ES cluster in the background instead of remaining attached: useful for running tests */ + background?: boolean; + /** Wait for the ES cluster to be ready to serve requests */ + waitForReady?: boolean; } interface ServerlessEsNodeArgs { @@ -38,6 +65,7 @@ interface ServerlessEsNodeArgs { params: string[]; } +export const DEFAULT_PORT = 9200; const DOCKER_REGISTRY = 'docker.elastic.co'; const DOCKER_BASE_CMD = [ @@ -53,9 +81,6 @@ const DOCKER_BASE_CMD = [ '--name', 'es01', - '-p', - '127.0.0.1:9200:9200', - '-p', '127.0.0.1:9300:9300', ]; @@ -78,6 +103,8 @@ export const SERVERLESS_REPO = `${DOCKER_REGISTRY}/elasticsearch-ci/elasticsearc export const SERVERLESS_TAG = 'latest'; export const SERVERLESS_IMG = `${SERVERLESS_REPO}:${SERVERLESS_TAG}`; +// See for default cluster settings +// https://github.com/elastic/elasticsearch-serverless/blob/main/serverless-build-tools/src/main/kotlin/elasticsearch.serverless-run.gradle.kts const SHARED_SERVERLESS_PARAMS = [ 'run', @@ -85,9 +112,16 @@ const SHARED_SERVERLESS_PARAMS = [ '--detach', + '--interactive', + + '--tty', + '--net', 'elastic', + '--env', + 'path.repo=/objectstore', + '--env', 'cluster.initial_master_nodes=es01,es02,es03', @@ -99,27 +133,65 @@ const SHARED_SERVERLESS_PARAMS = [ '--env', 'stateless.object_store.bucket=stateless', - - '--env', - 'path.repo=/objectstore', ]; // only allow certain ES args to be overwrote by options const DEFAULT_SERVERLESS_ESARGS: Array<[string, string]> = [ ['ES_JAVA_OPTS', '-Xms1g -Xmx1g'], - ['xpack.security.enabled', 'false'], + ['ES_LOG_STYLE', 'file'], ['cluster.name', 'stateless'], + + ['ingest.geoip.downloader.enabled', 'false'], + + ['xpack.ml.enabled', 'true'], + + ['xpack.security.enabled', 'false'], +]; + +const DEFAULT_SSL_ESARGS: Array<[string, string]> = [ + ['xpack.security.enabled', 'true'], + + ['xpack.security.http.ssl.enabled', 'true'], + + ['xpack.security.http.ssl.keystore.path', `${ESS_CONFIG_PATH}certs/elasticsearch.p12`], + + ['xpack.security.http.ssl.verification_mode', 'certificate'], + + ['xpack.security.transport.ssl.enabled', 'true'], + + ['xpack.security.transport.ssl.keystore.path', `${ESS_CONFIG_PATH}certs/elasticsearch.p12`], + + ['xpack.security.transport.ssl.verification_mode', 'certificate'], + + ['xpack.security.operator_privileges.enabled', 'true'], +]; + +const SERVERLESS_SSL_ESARGS: Array<[string, string]> = [ + ['xpack.security.authc.realms.jwt.jwt1.client_authentication.type', 'shared_secret'], + + ['xpack.security.authc.realms.jwt.jwt1.order', '-98'], + + ['xpack.security.authc.realms.jwt.jwt1.allowed_issuer', 'https://kibana.elastic.co/jwt/'], + + ['xpack.security.authc.realms.jwt.jwt1.allowed_audiences', 'elasticsearch'], + + ['xpack.security.authc.realms.jwt.jwt1.pkc_jwkset_path', `${ESS_CONFIG_PATH}secrets/jwks.json`], + + ['xpack.security.authc.realms.jwt.jwt1.claims.principal', 'sub'], +]; + +const DOCKER_SSL_ESARGS: Array<[string, string]> = [ + ['xpack.security.http.ssl.keystore.password', ES_P12_PASSWORD], + + ['xpack.security.transport.ssl.keystore.password', ES_P12_PASSWORD], ]; const SERVERLESS_NODES: Array> = [ { name: 'es01', params: [ - '-p', - '127.0.0.1:9200:9200', - '-p', '127.0.0.1:9300:9300', @@ -127,9 +199,13 @@ const SERVERLESS_NODES: Array> = [ 'discovery.seed_hosts=es02,es03', '--env', - 'node.roles=["master","index"]', + 'node.roles=["master","remote_cluster_client","ingest","index"]', + ], + esArgs: [ + ['xpack.searchable.snapshot.shared_cache.size', '16MB'], + + ['xpack.searchable.snapshot.shared_cache.region_size', '256K'], ], - esArgs: [['xpack.searchable.snapshot.shared_cache.size', '1gb']], }, { name: 'es02', @@ -144,9 +220,13 @@ const SERVERLESS_NODES: Array> = [ 'discovery.seed_hosts=es01,es03', '--env', - 'node.roles=["master","search"]', + 'node.roles=["master","remote_cluster_client","search"]', + ], + esArgs: [ + ['xpack.searchable.snapshot.shared_cache.size', '16MB'], + + ['xpack.searchable.snapshot.shared_cache.region_size', '256K'], ], - esArgs: [['xpack.searchable.snapshot.shared_cache.size', '1gb']], }, { name: 'es03', @@ -161,7 +241,7 @@ const SERVERLESS_NODES: Array> = [ 'discovery.seed_hosts=es01,es02', '--env', - 'node.roles=["master"]', + 'node.roles=["master","remote_cluster_client","ml","transform"]', ], }, ]; @@ -190,6 +270,22 @@ export function resolveDockerImage({ return defaultImg; } +/** + * Determine the port to bind the Serverless index node or Docker node to + */ +export function resolvePort(options: ServerlessOptions | DockerOptions) { + if (options.port) { + return [ + '-p', + `127.0.0.1:${options.port}:${options.port}`, + '--env', + `http.port=${options.port}`, + ]; + } + + return ['-p', `127.0.0.1:${DEFAULT_PORT}:${DEFAULT_PORT}`]; +} + /** * Verify that Docker is installed locally */ @@ -227,12 +323,70 @@ export async function maybeCreateDockerNetwork(log: ToolingLog) { log.indent(-4); } +/** + * + * Pull a Docker image if needed. Ensures latest image. + * Stops serverless from pulling the same image in each node's promise and + * gives better control of log output, instead of falling back to docker run. + */ +export async function maybePullDockerImage(log: ToolingLog, image: string) { + log.info(chalk.bold(`Checking for image: ${image}`)); + + await execa('docker', ['pull', image], { + // inherit is required to show Docker pull output + stdio: ['ignore', 'inherit', 'pipe'], + }).catch(({ message, stderr }) => { + throw createCliError( + stderr.includes('unauthorized: authentication required') + ? `Error authenticating with ${DOCKER_REGISTRY}. Visit https://docker-auth.elastic.co/github_auth to login.` + : message + ); + }); +} + +export async function detectRunningNodes( + log: ToolingLog, + options: ServerlessOptions | DockerOptions +) { + const namesCmd = SERVERLESS_NODES.reduce((acc, { name }) => { + acc.push('--filter', `name=${name}`); + + return acc; + }, []); + + const { stdout } = await execa('docker', ['ps', '--quiet'].concat(namesCmd)); + const runningNodes = stdout.split(/\r?\n/).filter((s) => s); + + if (runningNodes.length) { + if (options.kill) { + log.info(chalk.bold('Killing running ES Nodes.')); + await execa('docker', ['kill'].concat(runningNodes)); + + return; + } + + throw createCliError( + 'ES has already been started, pass --kill to automatically stop the nodes on startup.' + ); + } +} + /** * Common setup for Docker and Serverless containers */ -async function setupDocker(log: ToolingLog) { +async function setupDocker({ + log, + image, + options, +}: { + log: ToolingLog; + image: string; + options: ServerlessOptions | DockerOptions; +}) { await verifyDockerInstalled(log); + await detectRunningNodes(log, options); await maybeCreateDockerNetwork(log); + await maybePullDockerImage(log, image); } /** @@ -242,40 +396,69 @@ export function resolveEsArgs( defaultEsArgs: Array<[string, string]>, options: ServerlessOptions | DockerOptions ) { + const { esArgs: customEsArgs, password, ssl } = options; const esArgs = new Map(defaultEsArgs); - if (options.esArgs) { - const args = typeof options.esArgs === 'string' ? [options.esArgs] : options.esArgs; + if (ssl) { + DEFAULT_SSL_ESARGS.forEach((arg) => { + esArgs.set(arg[0], arg[1]); + }); + } + + if (customEsArgs) { + const args = typeof customEsArgs === 'string' ? [customEsArgs] : customEsArgs; args.forEach((arg) => { const [key, ...value] = arg.split('='); + + // Guide the user to use SSL flag instead of manual setup + if (key === 'xpack.security.enabled' && value?.[0] === 'true') { + throw createCliError( + 'Use the --ssl flag to automatically enable and set up the security plugin.' + ); + } + esArgs.set(key.trim(), value.join('=').trim()); }); } - if (options.password) { - esArgs.set('ELASTIC_PASSWORD', options.password); + if (password) { + esArgs.set('ELASTIC_PASSWORD', password); } return Array.from(esArgs).flatMap((e) => ['--env', e.join('=')]); } +function getESp12Volume() { + return ['--volume', `${ES_P12_PATH}:${ESS_CONFIG_PATH}certs/elasticsearch.p12`]; +} + +/** + * Removes REPO_ROOT from hostPath. Keep the rest to avoid filename collisions. + * Returns the path where a file will be mounted inside the ES or ESS container. + * /root/kibana/package/foo/bar.json => /usr/share/elasticsearch/files/package/foo/bar.json + */ +export function getDockerFileMountPath(hostPath: string) { + return join(ESS_FILES_PATH, hostPath.replace(REPO_ROOT, '')); +} + /** * Setup local volumes for Serverless ES */ export async function setupServerlessVolumes(log: ToolingLog, options: ServerlessOptions) { - const volumePath = resolve(options.basePath, 'stateless'); + const { basePath, clean, ssl, files } = options; + const objectStorePath = resolve(basePath, 'stateless'); - log.info(chalk.bold(`Checking for local Serverless ES object store at ${volumePath}`)); + log.info(chalk.bold(`Checking for local serverless ES object store at ${objectStorePath}`)); log.indent(4); - if (options.clean && fs.existsSync(volumePath)) { + if (clean && fs.existsSync(objectStorePath)) { log.info('Cleaning existing object store.'); - await Fsp.rm(volumePath, { recursive: true, force: true }); + await Fsp.rm(objectStorePath, { recursive: true, force: true }); } - if (options.clean || !fs.existsSync(volumePath)) { - await Fsp.mkdir(volumePath, { recursive: true }).then(() => + if (clean || !fs.existsSync(objectStorePath)) { + await Fsp.mkdir(objectStorePath, { recursive: true }).then(() => log.info('Created new object store.') ); } else { @@ -283,13 +466,45 @@ export async function setupServerlessVolumes(log: ToolingLog, options: Serverles } // Permissions are set separately from mkdir due to default umask - await Fsp.chmod(volumePath, 0o766).then(() => - log.info('Setup object store permissions (chmod 766).') - ); + await Fsp.chmod(objectStorePath, 0o777).then(() => { + log.info('Setup object store permissions (chmod 777).'); + }); log.indent(-4); - return ['--volume', `${options.basePath}:/objectstore:z`]; + const volumeCmds = ['--volume', `${basePath}:/objectstore:z`]; + + if (files) { + const _files = typeof files === 'string' ? [files] : files; + const fileCmds = _files.reduce((acc, filePath) => { + acc.push('--volume', `${filePath}:${getDockerFileMountPath(filePath)}:z`); + + return acc; + }, []); + + volumeCmds.push(...fileCmds); + } + + if (ssl) { + const essResources = ESS_RESOURCES_PATHS.reduce((acc, path) => { + acc.push('--volume', `${path}:${ESS_CONFIG_PATH}${basename(path)}`); + + return acc; + }, []); + + volumeCmds.push( + ...getESp12Volume(), + ...essResources, + + '--volume', + `${ESS_SECRETS_PATH}:${ESS_CONFIG_PATH}secrets/secrets.json:z`, + + '--volume', + `${ESS_JWKS_PATH}:${ESS_CONFIG_PATH}secrets/jwks.json:z` + ); + } + + return volumeCmds; } /** @@ -316,7 +531,7 @@ export async function runServerlessEsNode( image ); - log.info(chalk.bold(`Running Serverless ES node: ${name}`)); + log.info(chalk.bold(`Running serverless ES node: ${name}`)); log.indent(4, () => log.info(chalk.dim(`docker ${dockerCmd.join(' ')}`))); const { stdout } = await execa('docker', dockerCmd); @@ -332,22 +547,53 @@ export async function runServerlessEsNode( ); } +function getESClient( + { node }: { node: string } = { node: `http://localhost:${DEFAULT_PORT}` } +): Client { + return new Client({ + node, + Connection: HttpConnection, + }); +} + +const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); +async function waitUntilClusterReady(timeoutMs = 60 * 1000): Promise { + const started = Date.now(); + const client = getESClient(); + while (started + timeoutMs > Date.now()) { + try { + await client.info(); + break; + } catch (e) { + await delay(1000); + /* trap to continue */ + } + } +} + /** * Runs an ES Serverless Cluster through Docker */ export async function runServerlessCluster(log: ToolingLog, options: ServerlessOptions) { - await setupDocker(log); + const image = getServerlessImage(options); + await setupDocker({ log, image, options }); const volumeCmd = await setupServerlessVolumes(log, options); - const image = getServerlessImage(options); const nodeNames = await Promise.all( - SERVERLESS_NODES.map(async (node) => { + SERVERLESS_NODES.map(async (node, i) => { await runServerlessEsNode(log, { ...node, image, params: node.params.concat( - resolveEsArgs(DEFAULT_SERVERLESS_ESARGS.concat(node.esArgs ?? []), options), + resolveEsArgs( + DEFAULT_SERVERLESS_ESARGS.concat( + node.esArgs ?? [], + options.ssl ? SERVERLESS_SSL_ESARGS : [] + ), + options + ), + i === 0 ? resolvePort(options) : [], volumeCmd ), }); @@ -358,6 +604,62 @@ export async function runServerlessCluster(log: ToolingLog, options: ServerlessO log.success(`Serverless ES cluster running. Stop the cluster: ${chalk.bold(`docker container stop ${nodeNames.join(' ')}`)} `); + + if (options.ssl) { + log.success(`SSL and Security have been enabled for ES. + Login through your browser with username ${chalk.bold.cyan( + ELASTIC_SERVERLESS_SUPERUSER + )} or ${chalk.bold.cyan(SYSTEM_INDICES_SUPERUSER)} and password ${chalk.bold.magenta( + ELASTIC_SERVERLESS_SUPERUSER_PASSWORD + )}. + `); + + log.warning(`Kibana should be started with the SSL flag so that it can authenticate with ES. + See packages/kbn-es/src/ess_resources/README.md for additional information on authentication. + `); + } + + if (options.waitForReady) { + log.info('Waiting until ES is ready to serve requests...'); + await waitUntilClusterReady(); + log.success('ES is ready'); + } + + if (!options.background) { + // The ESS cluster has to be started detached, so we attach a logger afterwards for output + await execa('docker', ['logs', '-f', SERVERLESS_NODES[0].name], { + // inherit is required to show Docker output and Java console output for pw, enrollment token, etc + stdio: ['ignore', 'inherit', 'inherit'], + }); + } + + return nodeNames; +} + +/** + * Stop a serverless ES cluster by node names + */ +export async function stopServerlessCluster(log: ToolingLog, nodes: string[]) { + log.info('Stopping serverless ES cluster.'); + + await execa('docker', ['container', 'stop'].concat(nodes)); +} + +/** + * Kill any serverless ES nodes which are running. + */ +export function teardownServerlessClusterSync(log: ToolingLog, options: ServerlessOptions) { + const { stdout } = execa.commandSync( + `docker ps --filter status=running --filter ancestor=${getServerlessImage(options)} --quiet` + ); + // Filter empty strings + const runningNodes = stdout.split(/\r?\n/).filter((s) => s); + + if (runningNodes.length) { + log.info('Killing running serverless ES nodes.'); + + execa.commandSync(`docker kill ${runningNodes.join(' ')}`); + } } /** @@ -370,29 +672,35 @@ function getDockerImage(options: DockerOptions) { /** * Resolve the full command to run Elasticsearch Docker container */ -export function resolveDockerCmd(options: DockerOptions) { +export function resolveDockerCmd(options: DockerOptions, image: string = DOCKER_IMG) { if (options.dockerCmd) { return options.dockerCmd.split(' '); } return DOCKER_BASE_CMD.concat( - resolveEsArgs(DEFAULT_DOCKER_ESARGS, options), - getDockerImage(options) + resolveEsArgs(DEFAULT_DOCKER_ESARGS.concat(options.ssl ? DOCKER_SSL_ESARGS : []), options), + resolvePort(options), + options.ssl ? getESp12Volume() : [], + image ); } /** - * * Runs an Elasticsearch Docker Container */ export async function runDockerContainer(log: ToolingLog, options: DockerOptions) { - await setupDocker(log); + let image; + + if (!options.dockerCmd) { + image = getDockerImage(options); + await setupDocker({ log, image, options }); + } - const dockerCmd = resolveDockerCmd(options); + const dockerCmd = resolveDockerCmd(options, image); log.info(chalk.dim(`docker ${dockerCmd.join(' ')}`)); return await execa('docker', dockerCmd, { - // inherit is required to show Docker pull output and Java console output for pw, enrollment token, etc + // inherit is required to show Docker output and Java console output for pw, enrollment token, etc stdio: ['ignore', 'inherit', 'inherit'], }); } diff --git a/packages/kbn-es/src/utils/ess_file_realm.ts b/packages/kbn-es/src/utils/ess_file_realm.ts new file mode 100644 index 0000000000000..6b7745ac9351a --- /dev/null +++ b/packages/kbn-es/src/utils/ess_file_realm.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const ELASTIC_SERVERLESS_SUPERUSER = 'elastic_serverless'; +export const ELASTIC_SERVERLESS_SUPERUSER_PASSWORD = 'changeme'; diff --git a/packages/kbn-es/src/utils/index.ts b/packages/kbn-es/src/utils/index.ts index 25591a786603c..cc46d0bf28271 100644 --- a/packages/kbn-es/src/utils/index.ts +++ b/packages/kbn-es/src/utils/index.ts @@ -17,3 +17,4 @@ export { buildSnapshot } from './build_snapshot'; export { archiveForPlatform } from './build_snapshot'; export * from './parse_timeout_to_ms'; export * from './docker'; +export * from './ess_file_realm'; diff --git a/packages/kbn-event-annotation-components/components/annotation_editor_controls/tooltip_annotation_panel.tsx b/packages/kbn-event-annotation-components/components/annotation_editor_controls/tooltip_annotation_panel.tsx index f1ed8ccc7ccb2..e4f3e7f976d1c 100644 --- a/packages/kbn-event-annotation-components/components/annotation_editor_controls/tooltip_annotation_panel.tsx +++ b/packages/kbn-event-annotation-components/components/annotation_editor_controls/tooltip_annotation_panel.tsx @@ -9,7 +9,7 @@ import { htmlIdGenerator, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo } from 'react'; -import { useExistingFieldsReader } from '@kbn/unified-field-list/src/hooks/use_existing_fields'; +import { useExistingFieldsReader, getFieldIconType } from '@kbn/unified-field-list'; import { FieldOption, FieldOptionValue, @@ -139,7 +139,7 @@ export function TooltipSection({ value: { type: 'field', field: field.name, - dataType: field.type, + dataType: getFieldIconType(field), }, exists: dataView.id ? hasFieldData(dataView.id, field.name) : false, compatible: true, diff --git a/packages/kbn-journeys/journey/journey_ftr_config.ts b/packages/kbn-journeys/journey/journey_ftr_config.ts index 7223dc8ab0f5c..1abc141c7bbae 100644 --- a/packages/kbn-journeys/journey/journey_ftr_config.ts +++ b/packages/kbn-journeys/journey/journey_ftr_config.ts @@ -90,6 +90,7 @@ export function makeFtrConfigProvider( `--telemetry.labels=${JSON.stringify(telemetryLabels)}`, '--csp.strict=false', '--csp.warnLegacyBrowsers=false', + '--coreApp.allowDynamicConfigOverrides=true', ], env: { diff --git a/packages/kbn-journeys/journey/journey_ftr_harness.ts b/packages/kbn-journeys/journey/journey_ftr_harness.ts index f6607081d6144..9df84821de032 100644 --- a/packages/kbn-journeys/journey/journey_ftr_harness.ts +++ b/packages/kbn-journeys/journey/journey_ftr_harness.ts @@ -17,6 +17,10 @@ import { asyncMap, asyncForEach } from '@kbn/std'; import { ToolingLog } from '@kbn/tooling-log'; import { Config } from '@kbn/test'; import { EsArchiver, KibanaServer, Es, RetryService } from '@kbn/ftr-common-functional-services'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { Auth } from '../services/auth'; import { getInputDelays } from '../services/input_delays'; @@ -55,9 +59,31 @@ export class JourneyFtrHarness { private apm: apmNode.Agent | null = null; + // Update the Telemetry and APM global labels to link traces with journey + private async updateTelemetryAndAPMLabels(labels: { [k: string]: string }) { + this.log.info(`Updating telemetry & APM labels: ${JSON.stringify(labels)}`); + + await this.kibanaServer.request({ + path: '/internal/core/_settings', + method: 'PUT', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + [X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'ftr', + }, + body: { telemetry: { labels } }, + }); + } + private async setupApm() { const kbnTestServerEnv = this.config.get(`kbnTestServer.env`); + const journeyLabels: { [k: string]: string } = Object.fromEntries( + kbnTestServerEnv.ELASTIC_APM_GLOBAL_LABELS.split(',').map((kv: string) => kv.split('=')) + ); + + // Update labels before start for consistency b/w APM services + await this.updateTelemetryAndAPMLabels(journeyLabels); + this.apm = apmNode.start({ serviceName: 'functional test runner', environment: process.env.CI ? 'ci' : 'development', diff --git a/packages/kbn-journeys/tsconfig.json b/packages/kbn-journeys/tsconfig.json index d52e0f32586af..7917081cb1847 100644 --- a/packages/kbn-journeys/tsconfig.json +++ b/packages/kbn-journeys/tsconfig.json @@ -18,6 +18,7 @@ "@kbn/repo-info", "@kbn/std", "@kbn/test-subj-selector", + "@kbn/core-http-common", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-monaco/src/esql/antlr/.gitignore b/packages/kbn-monaco/src/esql/antlr/.gitignore new file mode 100644 index 0000000000000..f7754e5f01083 --- /dev/null +++ b/packages/kbn-monaco/src/esql/antlr/.gitignore @@ -0,0 +1 @@ +.antlr/* \ No newline at end of file diff --git a/packages/kbn-monaco/src/esql/antlr/esql_lexer.g4 b/packages/kbn-monaco/src/esql/antlr/esql_lexer.g4 index 49d73416712a8..b5688e0915a33 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_lexer.g4 +++ b/packages/kbn-monaco/src/esql/antlr/esql_lexer.g4 @@ -7,15 +7,23 @@ lexer grammar esql_lexer; -EVAL : 'eval' -> pushMode(EXPRESSION); -EXPLAIN : 'explain' -> pushMode(EXPRESSION); -FROM : 'from' -> pushMode(SOURCE_IDENTIFIERS); -ROW : 'row' -> pushMode(EXPRESSION); -STATS : 'stats' -> pushMode(EXPRESSION); -WHERE : 'where' -> pushMode(EXPRESSION); -SORT : 'sort' -> pushMode(EXPRESSION); -LIMIT : 'limit' -> pushMode(EXPRESSION); -PROJECT : 'project' -> pushMode(SOURCE_IDENTIFIERS); +DISSECT : D I S S E C T -> pushMode(EXPRESSION); +GROK : G R O K -> pushMode(EXPRESSION); +EVAL : E V A L -> pushMode(EXPRESSION); +EXPLAIN : E X P L A I N -> pushMode(EXPLAIN_MODE); +FROM : F R O M -> pushMode(SOURCE_IDENTIFIERS); +ROW : R O W -> pushMode(EXPRESSION); +STATS : S T A T S -> pushMode(EXPRESSION); +WHERE : W H E R E -> pushMode(EXPRESSION); +SORT : S O R T -> pushMode(EXPRESSION); +MV_EXPAND : M V UNDERSCORE E X P A N D -> pushMode(EXPRESSION); +LIMIT : L I M I T -> pushMode(EXPRESSION); +PROJECT : P R O J E C T -> pushMode(EXPRESSION); +DROP : D R O P -> pushMode(EXPRESSION); +RENAME : R E N A M E -> pushMode(EXPRESSION); +SHOW : S H O W -> pushMode(EXPRESSION); +ENRICH : E N R I C H -> pushMode(ENRICH_IDENTIFIERS); +KEEP : K E E P -> pushMode(EXPRESSION); LINE_COMMENT : '//' ~[\r\n]* '\r'? '\n'? -> channel(HIDDEN) @@ -28,7 +36,12 @@ MULTILINE_COMMENT 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; @@ -71,17 +84,34 @@ DECIMAL_LITERAL BY : 'by'; +DATE_LITERAL + : 'year' + | 'month' + | 'day' + | 'second' + | 'minute' + | 'hour' + ; + AND : 'and'; ASSIGN : '='; COMMA : ','; DOT : '.'; LP : '('; -OPENING_BRACKET : '[' -> pushMode(DEFAULT_MODE); -CLOSING_BRACKET : ']' -> popMode, popMode; // pop twice, once to clear mode of current cmd and once to exit DEFAULT_MODE -NOT : 'not'; -NULL : 'null'; +OPENING_BRACKET : '[' -> pushMode(EXPRESSION), pushMode(EXPRESSION); +CLOSING_BRACKET : ']' -> popMode, popMode; +NOT : N O T; +LIKE: L I K E; +RLIKE: R L I K E; +IN: I N; +IS: I S; +AS: A S; +NULL : N U L L; OR : 'or'; RP : ')'; +UNDERSCORE: '_'; +INFO : 'info'; +FUNCTIONS : 'functions'; BOOLEAN_VALUE : 'true' @@ -102,6 +132,7 @@ MINUS : '-'; ASTERISK : '*'; SLASH : '/'; PERCENT : '%'; +TEN: '10'; ORDERING : 'asc' @@ -114,16 +145,74 @@ NULLS_ORDERING_DIRECTION | 'last' ; +MATH_FUNCTION + : R O U N D + | A B S + | P O W + | L O G TEN + | P I + | T A U + | E + | S U B S T R I N G + | T R I M + | C O N C A T + | S T A R T S UNDERSCORE W I T H + | D A T E UNDERSCORE F O R M A T + | D A T E UNDERSCORE T R U N C + | D A T E UNDERSCORE P A R S E + | A U T O UNDERSCORE B U C K E T + | I S UNDERSCORE F I N I T E + | I S UNDERSCORE I N F I N I T E + | C A S E + | L E N G T H + | M V UNDERSCORE M A X + | M V UNDERSCORE M I N + | M V UNDERSCORE A V G + | M V UNDERSCORE S U M + | M V UNDERSCORE C O U N T + | M V UNDERSCORE C O N C A T + | M V UNDERSCORE J O I N + | M V UNDERSCORE M E D I A N + | M V UNDERSCORE D E D U P E + | M E T A D A T A + | S P L I T + | T O UNDERSCORE S T R I N G + | T O UNDERSCORE S T R + | T O UNDERSCORE B O O L + | T O UNDERSCORE B O O L E A N + | T O UNDERSCORE D A T E T I M E + | T O UNDERSCORE D T + | T O UNDERSCORE D B L + | T O UNDERSCORE D O U B L E + | T O UNDERSCORE I N T + | T O UNDERSCORE I N T E G E R + | T O UNDERSCORE L O N G + | T O UNDERSCORE I P + | T O UNDERSCORE V E R S I O N + | T O UNDERSCORE U N S I G N E D UNDERSCORE L O N G + ; + UNARY_FUNCTION - : 'round' - | 'avg' - | 'min' - | 'max' - | 'sum' + : A V G + | M I N + | M A X + | S U M + | C O U N T + | C O U N T UNDERSCORE D I S T I N C T + | P E R C E N T I L E + | M E D I A N + | M E D I A N UNDERSCORE A B S O L U T E UNDERSCORE D E V I A T I O N + ; + +WHERE_FUNCTIONS + : C I D R UNDERSCORE M A T C H ; UNQUOTED_IDENTIFIER - : (LETTER | '_') (LETTER | DIGIT | '_')* + : LETTER (LETTER | DIGIT | '_' | ASTERISK)* + // 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 | '_' | ASTERISK)+ ; QUOTED_IDENTIFIER @@ -146,9 +235,11 @@ EXPR_WS 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); +METADATA: M E T A D A T A; SRC_UNQUOTED_IDENTIFIER : SRC_UNQUOTED_IDENTIFIER_PART+ @@ -174,3 +265,65 @@ SRC_MULTILINE_COMMENT SRC_WS : WS -> channel(HIDDEN) ; + +mode ENRICH_IDENTIFIERS; + +ON : O N; +WITH : W I T H; + +ENR_PIPE : '|' -> type(PIPE), popMode; +ENR_CLOSING_BRACKET : ']' -> popMode, popMode, type(CLOSING_BRACKET); +ENR_COMMA : ',' -> type(COMMA); +ENR_ASSIGN : '=' -> type(ASSIGN); + +ENR_UNQUOTED_IDENTIFIER + : ENR_UNQUOTED_IDENTIFIER_PART+ + ; + +fragment ENR_UNQUOTED_IDENTIFIER_PART + : ~[=`|,[\]/ \t\r\n]+ + | '/' ~[*/] // allow single / but not followed by another / or * which would start a comment + ; + +ENR_QUOTED_IDENTIFIER + : QUOTED_IDENTIFIER + ; + +ENR_LINE_COMMENT + : LINE_COMMENT -> channel(HIDDEN) + ; + +ENR_MULTILINE_COMMENT + : MULTILINE_COMMENT -> channel(HIDDEN) + ; + +ENR_WS + : WS -> channel(HIDDEN) + ; + +fragment A : [aA]; // match either an 'a' or 'A' +fragment B : [bB]; +fragment C : [cC]; +fragment D : [dD]; +fragment E : [eE]; +fragment F : [fF]; +fragment G : [gG]; +fragment H : [hH]; +fragment I : [iI]; +fragment J : [jJ]; +fragment K : [kK]; +fragment L : [lL]; +fragment M : [mM]; +fragment N : [nN]; +fragment O : [oO]; +fragment P : [pP]; +fragment Q : [qQ]; +fragment R : [rR]; +fragment S : [sS]; +fragment T : [tT]; +fragment U : [uU]; +fragment V : [vV]; +fragment W : [wW]; +fragment X : [xX]; +fragment Y : [yY]; +fragment Z : [zZ]; \ No newline at end of file diff --git a/packages/kbn-monaco/src/esql/antlr/esql_lexer.interp b/packages/kbn-monaco/src/esql/antlr/esql_lexer.interp index a7fee84591c3b..394ed7fc4ee63 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_lexer.interp +++ b/packages/kbn-monaco/src/esql/antlr/esql_lexer.interp @@ -1,14 +1,25 @@ token literal names: null -'eval' -'explain' -'from' -'row' -'stats' -'where' -'sort' -'limit' -'project' +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null null null null @@ -17,17 +28,26 @@ null null null 'by' +null 'and' null null '.' '(' -'[' +null ']' -'not' -'null' +null +null +null +null +null +null +null 'or' ')' +'_' +'info' +'functions' null null '+' @@ -35,6 +55,7 @@ null '*' '/' '%' +'10' null 'nulls' null @@ -49,9 +70,22 @@ null null null null +null +null +null +null +null +null +null +null +null +null +null token symbolic names: null +DISSECT +GROK EVAL EXPLAIN FROM @@ -59,16 +93,26 @@ ROW STATS WHERE SORT +MV_EXPAND LIMIT PROJECT +DROP +RENAME +SHOW +ENRICH +KEEP LINE_COMMENT MULTILINE_COMMENT WS +EXPLAIN_WS +EXPLAIN_LINE_COMMENT +EXPLAIN_MULTILINE_COMMENT PIPE STRING INTEGER_LITERAL DECIMAL_LITERAL BY +DATE_LITERAL AND ASSIGN COMMA @@ -77,9 +121,17 @@ LP OPENING_BRACKET CLOSING_BRACKET NOT +LIKE +RLIKE +IN +IS +AS NULL OR RP +UNDERSCORE +INFO +FUNCTIONS BOOLEAN_VALUE COMPARISON_OPERATOR PLUS @@ -87,22 +139,36 @@ MINUS ASTERISK SLASH PERCENT +TEN ORDERING NULLS_ORDERING NULLS_ORDERING_DIRECTION +MATH_FUNCTION UNARY_FUNCTION +WHERE_FUNCTIONS UNQUOTED_IDENTIFIER QUOTED_IDENTIFIER EXPR_LINE_COMMENT EXPR_MULTILINE_COMMENT EXPR_WS +METADATA SRC_UNQUOTED_IDENTIFIER SRC_QUOTED_IDENTIFIER SRC_LINE_COMMENT SRC_MULTILINE_COMMENT SRC_WS +ON +WITH +ENR_UNQUOTED_IDENTIFIER +ENR_QUOTED_IDENTIFIER +ENR_LINE_COMMENT +ENR_MULTILINE_COMMENT +ENR_WS +EXPLAIN_PIPE rule names: +DISSECT +GROK EVAL EXPLAIN FROM @@ -110,11 +176,22 @@ ROW STATS WHERE SORT +MV_EXPAND LIMIT PROJECT +DROP +RENAME +SHOW +ENRICH +KEEP LINE_COMMENT MULTILINE_COMMENT WS +EXPLAIN_OPENING_BRACKET +EXPLAIN_PIPE +EXPLAIN_WS +EXPLAIN_LINE_COMMENT +EXPLAIN_MULTILINE_COMMENT PIPE DIGIT LETTER @@ -125,6 +202,7 @@ STRING INTEGER_LITERAL DECIMAL_LITERAL BY +DATE_LITERAL AND ASSIGN COMMA @@ -133,9 +211,17 @@ LP OPENING_BRACKET CLOSING_BRACKET NOT +LIKE +RLIKE +IN +IS +AS NULL OR RP +UNDERSCORE +INFO +FUNCTIONS BOOLEAN_VALUE COMPARISON_OPERATOR PLUS @@ -143,25 +229,68 @@ MINUS ASTERISK SLASH PERCENT +TEN ORDERING NULLS_ORDERING NULLS_ORDERING_DIRECTION +MATH_FUNCTION UNARY_FUNCTION +WHERE_FUNCTIONS UNQUOTED_IDENTIFIER QUOTED_IDENTIFIER EXPR_LINE_COMMENT EXPR_MULTILINE_COMMENT EXPR_WS SRC_PIPE +SRC_OPENING_BRACKET SRC_CLOSING_BRACKET SRC_COMMA SRC_ASSIGN +METADATA SRC_UNQUOTED_IDENTIFIER SRC_UNQUOTED_IDENTIFIER_PART SRC_QUOTED_IDENTIFIER SRC_LINE_COMMENT SRC_MULTILINE_COMMENT SRC_WS +ON +WITH +ENR_PIPE +ENR_CLOSING_BRACKET +ENR_COMMA +ENR_ASSIGN +ENR_UNQUOTED_IDENTIFIER +ENR_UNQUOTED_IDENTIFIER_PART +ENR_QUOTED_IDENTIFIER +ENR_LINE_COMMENT +ENR_MULTILINE_COMMENT +ENR_WS +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z channel names: DEFAULT_TOKEN_CHANNEL @@ -169,8 +298,10 @@ HIDDEN mode names: DEFAULT_MODE +EXPLAIN_MODE EXPRESSION SOURCE_IDENTIFIERS +ENRICH_IDENTIFIERS atn: -[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 2, 51, 533, 8, 1, 8, 1, 8, 1, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 4, 52, 9, 52, 4, 53, 9, 53, 4, 54, 9, 54, 4, 55, 9, 55, 4, 56, 9, 56, 4, 57, 9, 57, 4, 58, 9, 58, 4, 59, 9, 59, 4, 60, 9, 60, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 11, 7, 11, 199, 10, 11, 12, 11, 14, 11, 202, 11, 11, 3, 11, 5, 11, 205, 10, 11, 3, 11, 5, 11, 208, 10, 11, 3, 11, 3, 11, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 7, 12, 217, 10, 12, 12, 12, 14, 12, 220, 11, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 13, 6, 13, 228, 10, 13, 13, 13, 14, 13, 229, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 16, 3, 16, 3, 17, 3, 17, 3, 17, 3, 18, 3, 18, 3, 19, 3, 19, 5, 19, 249, 10, 19, 3, 19, 6, 19, 252, 10, 19, 13, 19, 14, 19, 253, 3, 20, 3, 20, 3, 20, 7, 20, 259, 10, 20, 12, 20, 14, 20, 262, 11, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 7, 20, 270, 10, 20, 12, 20, 14, 20, 273, 11, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 5, 20, 280, 10, 20, 3, 20, 5, 20, 283, 10, 20, 5, 20, 285, 10, 20, 3, 21, 6, 21, 288, 10, 21, 13, 21, 14, 21, 289, 3, 22, 6, 22, 293, 10, 22, 13, 22, 14, 22, 294, 3, 22, 3, 22, 7, 22, 299, 10, 22, 12, 22, 14, 22, 302, 11, 22, 3, 22, 3, 22, 6, 22, 306, 10, 22, 13, 22, 14, 22, 307, 3, 22, 6, 22, 311, 10, 22, 13, 22, 14, 22, 312, 3, 22, 3, 22, 7, 22, 317, 10, 22, 12, 22, 14, 22, 320, 11, 22, 5, 22, 322, 10, 22, 3, 22, 3, 22, 3, 22, 3, 22, 6, 22, 328, 10, 22, 13, 22, 14, 22, 329, 3, 22, 3, 22, 5, 22, 334, 10, 22, 3, 23, 3, 23, 3, 23, 3, 24, 3, 24, 3, 24, 3, 24, 3, 25, 3, 25, 3, 26, 3, 26, 3, 27, 3, 27, 3, 28, 3, 28, 3, 29, 3, 29, 3, 29, 3, 29, 3, 30, 3, 30, 3, 30, 3, 30, 3, 30, 3, 31, 3, 31, 3, 31, 3, 31, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 33, 3, 33, 3, 33, 3, 34, 3, 34, 3, 35, 3, 35, 3, 35, 3, 35, 3, 35, 3, 35, 3, 35, 3, 35, 3, 35, 5, 35, 383, 10, 35, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 5, 36, 395, 10, 36, 3, 37, 3, 37, 3, 38, 3, 38, 3, 39, 3, 39, 3, 40, 3, 40, 3, 41, 3, 41, 3, 42, 3, 42, 3, 42, 3, 42, 3, 42, 3, 42, 3, 42, 5, 42, 414, 10, 42, 3, 43, 3, 43, 3, 43, 3, 43, 3, 43, 3, 43, 3, 44, 3, 44, 3, 44, 3, 44, 3, 44, 3, 44, 3, 44, 3, 44, 3, 44, 5, 44, 431, 10, 44, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 3, 45, 5, 45, 450, 10, 45, 3, 46, 3, 46, 5, 46, 454, 10, 46, 3, 46, 3, 46, 3, 46, 7, 46, 459, 10, 46, 12, 46, 14, 46, 462, 11, 46, 3, 47, 3, 47, 3, 47, 3, 47, 7, 47, 468, 10, 47, 12, 47, 14, 47, 471, 11, 47, 3, 47, 3, 47, 3, 48, 3, 48, 3, 48, 3, 48, 3, 49, 3, 49, 3, 49, 3, 49, 3, 50, 3, 50, 3, 50, 3, 50, 3, 51, 3, 51, 3, 51, 3, 51, 3, 51, 3, 52, 3, 52, 3, 52, 3, 52, 3, 52, 3, 52, 3, 53, 3, 53, 3, 53, 3, 53, 3, 54, 3, 54, 3, 54, 3, 54, 3, 55, 6, 55, 507, 10, 55, 13, 55, 14, 55, 508, 3, 56, 6, 56, 512, 10, 56, 13, 56, 14, 56, 513, 3, 56, 3, 56, 5, 56, 518, 10, 56, 3, 57, 3, 57, 3, 58, 3, 58, 3, 58, 3, 58, 3, 59, 3, 59, 3, 59, 3, 59, 3, 60, 3, 60, 3, 60, 3, 60, 4, 218, 271, 2, 2, 61, 5, 2, 3, 7, 2, 4, 9, 2, 5, 11, 2, 6, 13, 2, 7, 15, 2, 8, 17, 2, 9, 19, 2, 10, 21, 2, 11, 23, 2, 12, 25, 2, 13, 27, 2, 14, 29, 2, 15, 31, 2, 2, 33, 2, 2, 35, 2, 2, 37, 2, 2, 39, 2, 2, 41, 2, 16, 43, 2, 17, 45, 2, 18, 47, 2, 19, 49, 2, 20, 51, 2, 21, 53, 2, 22, 55, 2, 23, 57, 2, 24, 59, 2, 25, 61, 2, 26, 63, 2, 27, 65, 2, 28, 67, 2, 29, 69, 2, 30, 71, 2, 31, 73, 2, 32, 75, 2, 33, 77, 2, 34, 79, 2, 35, 81, 2, 36, 83, 2, 37, 85, 2, 38, 87, 2, 39, 89, 2, 40, 91, 2, 41, 93, 2, 42, 95, 2, 43, 97, 2, 44, 99, 2, 45, 101, 2, 46, 103, 2, 2, 105, 2, 2, 107, 2, 2, 109, 2, 2, 111, 2, 47, 113, 2, 2, 115, 2, 48, 117, 2, 49, 119, 2, 50, 121, 2, 51, 5, 2, 3, 4, 13, 4, 2, 12, 12, 15, 15, 5, 2, 11, 12, 15, 15, 34, 34, 3, 2, 50, 59, 4, 2, 67, 92, 99, 124, 7, 2, 36, 36, 94, 94, 112, 112, 116, 116, 118, 118, 6, 2, 12, 12, 15, 15, 36, 36, 94, 94, 4, 2, 71, 71, 103, 103, 4, 2, 45, 45, 47, 47, 3, 2, 98, 98, 12, 2, 11, 12, 15, 15, 34, 34, 46, 46, 49, 49, 63, 63, 93, 93, 95, 95, 98, 98, 126, 126, 4, 2, 44, 44, 49, 49, 2, 570, 2, 5, 3, 2, 2, 2, 2, 7, 3, 2, 2, 2, 2, 9, 3, 2, 2, 2, 2, 11, 3, 2, 2, 2, 2, 13, 3, 2, 2, 2, 2, 15, 3, 2, 2, 2, 2, 17, 3, 2, 2, 2, 2, 19, 3, 2, 2, 2, 2, 21, 3, 2, 2, 2, 2, 23, 3, 2, 2, 2, 2, 25, 3, 2, 2, 2, 2, 27, 3, 2, 2, 2, 3, 29, 3, 2, 2, 2, 3, 41, 3, 2, 2, 2, 3, 43, 3, 2, 2, 2, 3, 45, 3, 2, 2, 2, 3, 47, 3, 2, 2, 2, 3, 49, 3, 2, 2, 2, 3, 51, 3, 2, 2, 2, 3, 53, 3, 2, 2, 2, 3, 55, 3, 2, 2, 2, 3, 57, 3, 2, 2, 2, 3, 59, 3, 2, 2, 2, 3, 61, 3, 2, 2, 2, 3, 63, 3, 2, 2, 2, 3, 65, 3, 2, 2, 2, 3, 67, 3, 2, 2, 2, 3, 69, 3, 2, 2, 2, 3, 71, 3, 2, 2, 2, 3, 73, 3, 2, 2, 2, 3, 75, 3, 2, 2, 2, 3, 77, 3, 2, 2, 2, 3, 79, 3, 2, 2, 2, 3, 81, 3, 2, 2, 2, 3, 83, 3, 2, 2, 2, 3, 85, 3, 2, 2, 2, 3, 87, 3, 2, 2, 2, 3, 89, 3, 2, 2, 2, 3, 91, 3, 2, 2, 2, 3, 93, 3, 2, 2, 2, 3, 95, 3, 2, 2, 2, 3, 97, 3, 2, 2, 2, 3, 99, 3, 2, 2, 2, 3, 101, 3, 2, 2, 2, 4, 103, 3, 2, 2, 2, 4, 105, 3, 2, 2, 2, 4, 107, 3, 2, 2, 2, 4, 109, 3, 2, 2, 2, 4, 111, 3, 2, 2, 2, 4, 115, 3, 2, 2, 2, 4, 117, 3, 2, 2, 2, 4, 119, 3, 2, 2, 2, 4, 121, 3, 2, 2, 2, 5, 123, 3, 2, 2, 2, 7, 130, 3, 2, 2, 2, 9, 140, 3, 2, 2, 2, 11, 147, 3, 2, 2, 2, 13, 153, 3, 2, 2, 2, 15, 161, 3, 2, 2, 2, 17, 169, 3, 2, 2, 2, 19, 176, 3, 2, 2, 2, 21, 184, 3, 2, 2, 2, 23, 194, 3, 2, 2, 2, 25, 211, 3, 2, 2, 2, 27, 227, 3, 2, 2, 2, 29, 233, 3, 2, 2, 2, 31, 237, 3, 2, 2, 2, 33, 239, 3, 2, 2, 2, 35, 241, 3, 2, 2, 2, 37, 244, 3, 2, 2, 2, 39, 246, 3, 2, 2, 2, 41, 284, 3, 2, 2, 2, 43, 287, 3, 2, 2, 2, 45, 333, 3, 2, 2, 2, 47, 335, 3, 2, 2, 2, 49, 338, 3, 2, 2, 2, 51, 342, 3, 2, 2, 2, 53, 344, 3, 2, 2, 2, 55, 346, 3, 2, 2, 2, 57, 348, 3, 2, 2, 2, 59, 350, 3, 2, 2, 2, 61, 354, 3, 2, 2, 2, 63, 359, 3, 2, 2, 2, 65, 363, 3, 2, 2, 2, 67, 368, 3, 2, 2, 2, 69, 371, 3, 2, 2, 2, 71, 382, 3, 2, 2, 2, 73, 394, 3, 2, 2, 2, 75, 396, 3, 2, 2, 2, 77, 398, 3, 2, 2, 2, 79, 400, 3, 2, 2, 2, 81, 402, 3, 2, 2, 2, 83, 404, 3, 2, 2, 2, 85, 413, 3, 2, 2, 2, 87, 415, 3, 2, 2, 2, 89, 430, 3, 2, 2, 2, 91, 449, 3, 2, 2, 2, 93, 453, 3, 2, 2, 2, 95, 463, 3, 2, 2, 2, 97, 474, 3, 2, 2, 2, 99, 478, 3, 2, 2, 2, 101, 482, 3, 2, 2, 2, 103, 486, 3, 2, 2, 2, 105, 491, 3, 2, 2, 2, 107, 497, 3, 2, 2, 2, 109, 501, 3, 2, 2, 2, 111, 506, 3, 2, 2, 2, 113, 517, 3, 2, 2, 2, 115, 519, 3, 2, 2, 2, 117, 521, 3, 2, 2, 2, 119, 525, 3, 2, 2, 2, 121, 529, 3, 2, 2, 2, 123, 124, 7, 103, 2, 2, 124, 125, 7, 120, 2, 2, 125, 126, 7, 99, 2, 2, 126, 127, 7, 110, 2, 2, 127, 128, 3, 2, 2, 2, 128, 129, 8, 2, 2, 2, 129, 6, 3, 2, 2, 2, 130, 131, 7, 103, 2, 2, 131, 132, 7, 122, 2, 2, 132, 133, 7, 114, 2, 2, 133, 134, 7, 110, 2, 2, 134, 135, 7, 99, 2, 2, 135, 136, 7, 107, 2, 2, 136, 137, 7, 112, 2, 2, 137, 138, 3, 2, 2, 2, 138, 139, 8, 3, 2, 2, 139, 8, 3, 2, 2, 2, 140, 141, 7, 104, 2, 2, 141, 142, 7, 116, 2, 2, 142, 143, 7, 113, 2, 2, 143, 144, 7, 111, 2, 2, 144, 145, 3, 2, 2, 2, 145, 146, 8, 4, 3, 2, 146, 10, 3, 2, 2, 2, 147, 148, 7, 116, 2, 2, 148, 149, 7, 113, 2, 2, 149, 150, 7, 121, 2, 2, 150, 151, 3, 2, 2, 2, 151, 152, 8, 5, 2, 2, 152, 12, 3, 2, 2, 2, 153, 154, 7, 117, 2, 2, 154, 155, 7, 118, 2, 2, 155, 156, 7, 99, 2, 2, 156, 157, 7, 118, 2, 2, 157, 158, 7, 117, 2, 2, 158, 159, 3, 2, 2, 2, 159, 160, 8, 6, 2, 2, 160, 14, 3, 2, 2, 2, 161, 162, 7, 121, 2, 2, 162, 163, 7, 106, 2, 2, 163, 164, 7, 103, 2, 2, 164, 165, 7, 116, 2, 2, 165, 166, 7, 103, 2, 2, 166, 167, 3, 2, 2, 2, 167, 168, 8, 7, 2, 2, 168, 16, 3, 2, 2, 2, 169, 170, 7, 117, 2, 2, 170, 171, 7, 113, 2, 2, 171, 172, 7, 116, 2, 2, 172, 173, 7, 118, 2, 2, 173, 174, 3, 2, 2, 2, 174, 175, 8, 8, 2, 2, 175, 18, 3, 2, 2, 2, 176, 177, 7, 110, 2, 2, 177, 178, 7, 107, 2, 2, 178, 179, 7, 111, 2, 2, 179, 180, 7, 107, 2, 2, 180, 181, 7, 118, 2, 2, 181, 182, 3, 2, 2, 2, 182, 183, 8, 9, 2, 2, 183, 20, 3, 2, 2, 2, 184, 185, 7, 114, 2, 2, 185, 186, 7, 116, 2, 2, 186, 187, 7, 113, 2, 2, 187, 188, 7, 108, 2, 2, 188, 189, 7, 103, 2, 2, 189, 190, 7, 101, 2, 2, 190, 191, 7, 118, 2, 2, 191, 192, 3, 2, 2, 2, 192, 193, 8, 10, 3, 2, 193, 22, 3, 2, 2, 2, 194, 195, 7, 49, 2, 2, 195, 196, 7, 49, 2, 2, 196, 200, 3, 2, 2, 2, 197, 199, 10, 2, 2, 2, 198, 197, 3, 2, 2, 2, 199, 202, 3, 2, 2, 2, 200, 198, 3, 2, 2, 2, 200, 201, 3, 2, 2, 2, 201, 204, 3, 2, 2, 2, 202, 200, 3, 2, 2, 2, 203, 205, 7, 15, 2, 2, 204, 203, 3, 2, 2, 2, 204, 205, 3, 2, 2, 2, 205, 207, 3, 2, 2, 2, 206, 208, 7, 12, 2, 2, 207, 206, 3, 2, 2, 2, 207, 208, 3, 2, 2, 2, 208, 209, 3, 2, 2, 2, 209, 210, 8, 11, 4, 2, 210, 24, 3, 2, 2, 2, 211, 212, 7, 49, 2, 2, 212, 213, 7, 44, 2, 2, 213, 218, 3, 2, 2, 2, 214, 217, 5, 25, 12, 2, 215, 217, 11, 2, 2, 2, 216, 214, 3, 2, 2, 2, 216, 215, 3, 2, 2, 2, 217, 220, 3, 2, 2, 2, 218, 219, 3, 2, 2, 2, 218, 216, 3, 2, 2, 2, 219, 221, 3, 2, 2, 2, 220, 218, 3, 2, 2, 2, 221, 222, 7, 44, 2, 2, 222, 223, 7, 49, 2, 2, 223, 224, 3, 2, 2, 2, 224, 225, 8, 12, 4, 2, 225, 26, 3, 2, 2, 2, 226, 228, 9, 3, 2, 2, 227, 226, 3, 2, 2, 2, 228, 229, 3, 2, 2, 2, 229, 227, 3, 2, 2, 2, 229, 230, 3, 2, 2, 2, 230, 231, 3, 2, 2, 2, 231, 232, 8, 13, 4, 2, 232, 28, 3, 2, 2, 2, 233, 234, 7, 126, 2, 2, 234, 235, 3, 2, 2, 2, 235, 236, 8, 14, 5, 2, 236, 30, 3, 2, 2, 2, 237, 238, 9, 4, 2, 2, 238, 32, 3, 2, 2, 2, 239, 240, 9, 5, 2, 2, 240, 34, 3, 2, 2, 2, 241, 242, 7, 94, 2, 2, 242, 243, 9, 6, 2, 2, 243, 36, 3, 2, 2, 2, 244, 245, 10, 7, 2, 2, 245, 38, 3, 2, 2, 2, 246, 248, 9, 8, 2, 2, 247, 249, 9, 9, 2, 2, 248, 247, 3, 2, 2, 2, 248, 249, 3, 2, 2, 2, 249, 251, 3, 2, 2, 2, 250, 252, 5, 31, 15, 2, 251, 250, 3, 2, 2, 2, 252, 253, 3, 2, 2, 2, 253, 251, 3, 2, 2, 2, 253, 254, 3, 2, 2, 2, 254, 40, 3, 2, 2, 2, 255, 260, 7, 36, 2, 2, 256, 259, 5, 35, 17, 2, 257, 259, 5, 37, 18, 2, 258, 256, 3, 2, 2, 2, 258, 257, 3, 2, 2, 2, 259, 262, 3, 2, 2, 2, 260, 258, 3, 2, 2, 2, 260, 261, 3, 2, 2, 2, 261, 263, 3, 2, 2, 2, 262, 260, 3, 2, 2, 2, 263, 285, 7, 36, 2, 2, 264, 265, 7, 36, 2, 2, 265, 266, 7, 36, 2, 2, 266, 267, 7, 36, 2, 2, 267, 271, 3, 2, 2, 2, 268, 270, 10, 2, 2, 2, 269, 268, 3, 2, 2, 2, 270, 273, 3, 2, 2, 2, 271, 272, 3, 2, 2, 2, 271, 269, 3, 2, 2, 2, 272, 274, 3, 2, 2, 2, 273, 271, 3, 2, 2, 2, 274, 275, 7, 36, 2, 2, 275, 276, 7, 36, 2, 2, 276, 277, 7, 36, 2, 2, 277, 279, 3, 2, 2, 2, 278, 280, 7, 36, 2, 2, 279, 278, 3, 2, 2, 2, 279, 280, 3, 2, 2, 2, 280, 282, 3, 2, 2, 2, 281, 283, 7, 36, 2, 2, 282, 281, 3, 2, 2, 2, 282, 283, 3, 2, 2, 2, 283, 285, 3, 2, 2, 2, 284, 255, 3, 2, 2, 2, 284, 264, 3, 2, 2, 2, 285, 42, 3, 2, 2, 2, 286, 288, 5, 31, 15, 2, 287, 286, 3, 2, 2, 2, 288, 289, 3, 2, 2, 2, 289, 287, 3, 2, 2, 2, 289, 290, 3, 2, 2, 2, 290, 44, 3, 2, 2, 2, 291, 293, 5, 31, 15, 2, 292, 291, 3, 2, 2, 2, 293, 294, 3, 2, 2, 2, 294, 292, 3, 2, 2, 2, 294, 295, 3, 2, 2, 2, 295, 296, 3, 2, 2, 2, 296, 300, 5, 55, 27, 2, 297, 299, 5, 31, 15, 2, 298, 297, 3, 2, 2, 2, 299, 302, 3, 2, 2, 2, 300, 298, 3, 2, 2, 2, 300, 301, 3, 2, 2, 2, 301, 334, 3, 2, 2, 2, 302, 300, 3, 2, 2, 2, 303, 305, 5, 55, 27, 2, 304, 306, 5, 31, 15, 2, 305, 304, 3, 2, 2, 2, 306, 307, 3, 2, 2, 2, 307, 305, 3, 2, 2, 2, 307, 308, 3, 2, 2, 2, 308, 334, 3, 2, 2, 2, 309, 311, 5, 31, 15, 2, 310, 309, 3, 2, 2, 2, 311, 312, 3, 2, 2, 2, 312, 310, 3, 2, 2, 2, 312, 313, 3, 2, 2, 2, 313, 321, 3, 2, 2, 2, 314, 318, 5, 55, 27, 2, 315, 317, 5, 31, 15, 2, 316, 315, 3, 2, 2, 2, 317, 320, 3, 2, 2, 2, 318, 316, 3, 2, 2, 2, 318, 319, 3, 2, 2, 2, 319, 322, 3, 2, 2, 2, 320, 318, 3, 2, 2, 2, 321, 314, 3, 2, 2, 2, 321, 322, 3, 2, 2, 2, 322, 323, 3, 2, 2, 2, 323, 324, 5, 39, 19, 2, 324, 334, 3, 2, 2, 2, 325, 327, 5, 55, 27, 2, 326, 328, 5, 31, 15, 2, 327, 326, 3, 2, 2, 2, 328, 329, 3, 2, 2, 2, 329, 327, 3, 2, 2, 2, 329, 330, 3, 2, 2, 2, 330, 331, 3, 2, 2, 2, 331, 332, 5, 39, 19, 2, 332, 334, 3, 2, 2, 2, 333, 292, 3, 2, 2, 2, 333, 303, 3, 2, 2, 2, 333, 310, 3, 2, 2, 2, 333, 325, 3, 2, 2, 2, 334, 46, 3, 2, 2, 2, 335, 336, 7, 100, 2, 2, 336, 337, 7, 123, 2, 2, 337, 48, 3, 2, 2, 2, 338, 339, 7, 99, 2, 2, 339, 340, 7, 112, 2, 2, 340, 341, 7, 102, 2, 2, 341, 50, 3, 2, 2, 2, 342, 343, 7, 63, 2, 2, 343, 52, 3, 2, 2, 2, 344, 345, 7, 46, 2, 2, 345, 54, 3, 2, 2, 2, 346, 347, 7, 48, 2, 2, 347, 56, 3, 2, 2, 2, 348, 349, 7, 42, 2, 2, 349, 58, 3, 2, 2, 2, 350, 351, 7, 93, 2, 2, 351, 352, 3, 2, 2, 2, 352, 353, 8, 29, 6, 2, 353, 60, 3, 2, 2, 2, 354, 355, 7, 95, 2, 2, 355, 356, 3, 2, 2, 2, 356, 357, 8, 30, 5, 2, 357, 358, 8, 30, 5, 2, 358, 62, 3, 2, 2, 2, 359, 360, 7, 112, 2, 2, 360, 361, 7, 113, 2, 2, 361, 362, 7, 118, 2, 2, 362, 64, 3, 2, 2, 2, 363, 364, 7, 112, 2, 2, 364, 365, 7, 119, 2, 2, 365, 366, 7, 110, 2, 2, 366, 367, 7, 110, 2, 2, 367, 66, 3, 2, 2, 2, 368, 369, 7, 113, 2, 2, 369, 370, 7, 116, 2, 2, 370, 68, 3, 2, 2, 2, 371, 372, 7, 43, 2, 2, 372, 70, 3, 2, 2, 2, 373, 374, 7, 118, 2, 2, 374, 375, 7, 116, 2, 2, 375, 376, 7, 119, 2, 2, 376, 383, 7, 103, 2, 2, 377, 378, 7, 104, 2, 2, 378, 379, 7, 99, 2, 2, 379, 380, 7, 110, 2, 2, 380, 381, 7, 117, 2, 2, 381, 383, 7, 103, 2, 2, 382, 373, 3, 2, 2, 2, 382, 377, 3, 2, 2, 2, 383, 72, 3, 2, 2, 2, 384, 385, 7, 63, 2, 2, 385, 395, 7, 63, 2, 2, 386, 387, 7, 35, 2, 2, 387, 395, 7, 63, 2, 2, 388, 395, 7, 62, 2, 2, 389, 390, 7, 62, 2, 2, 390, 395, 7, 63, 2, 2, 391, 395, 7, 64, 2, 2, 392, 393, 7, 64, 2, 2, 393, 395, 7, 63, 2, 2, 394, 384, 3, 2, 2, 2, 394, 386, 3, 2, 2, 2, 394, 388, 3, 2, 2, 2, 394, 389, 3, 2, 2, 2, 394, 391, 3, 2, 2, 2, 394, 392, 3, 2, 2, 2, 395, 74, 3, 2, 2, 2, 396, 397, 7, 45, 2, 2, 397, 76, 3, 2, 2, 2, 398, 399, 7, 47, 2, 2, 399, 78, 3, 2, 2, 2, 400, 401, 7, 44, 2, 2, 401, 80, 3, 2, 2, 2, 402, 403, 7, 49, 2, 2, 403, 82, 3, 2, 2, 2, 404, 405, 7, 39, 2, 2, 405, 84, 3, 2, 2, 2, 406, 407, 7, 99, 2, 2, 407, 408, 7, 117, 2, 2, 408, 414, 7, 101, 2, 2, 409, 410, 7, 102, 2, 2, 410, 411, 7, 103, 2, 2, 411, 412, 7, 117, 2, 2, 412, 414, 7, 101, 2, 2, 413, 406, 3, 2, 2, 2, 413, 409, 3, 2, 2, 2, 414, 86, 3, 2, 2, 2, 415, 416, 7, 112, 2, 2, 416, 417, 7, 119, 2, 2, 417, 418, 7, 110, 2, 2, 418, 419, 7, 110, 2, 2, 419, 420, 7, 117, 2, 2, 420, 88, 3, 2, 2, 2, 421, 422, 7, 104, 2, 2, 422, 423, 7, 107, 2, 2, 423, 424, 7, 116, 2, 2, 424, 425, 7, 117, 2, 2, 425, 431, 7, 118, 2, 2, 426, 427, 7, 110, 2, 2, 427, 428, 7, 99, 2, 2, 428, 429, 7, 117, 2, 2, 429, 431, 7, 118, 2, 2, 430, 421, 3, 2, 2, 2, 430, 426, 3, 2, 2, 2, 431, 90, 3, 2, 2, 2, 432, 433, 7, 116, 2, 2, 433, 434, 7, 113, 2, 2, 434, 435, 7, 119, 2, 2, 435, 436, 7, 112, 2, 2, 436, 450, 7, 102, 2, 2, 437, 438, 7, 99, 2, 2, 438, 439, 7, 120, 2, 2, 439, 450, 7, 105, 2, 2, 440, 441, 7, 111, 2, 2, 441, 442, 7, 107, 2, 2, 442, 450, 7, 112, 2, 2, 443, 444, 7, 111, 2, 2, 444, 445, 7, 99, 2, 2, 445, 450, 7, 122, 2, 2, 446, 447, 7, 117, 2, 2, 447, 448, 7, 119, 2, 2, 448, 450, 7, 111, 2, 2, 449, 432, 3, 2, 2, 2, 449, 437, 3, 2, 2, 2, 449, 440, 3, 2, 2, 2, 449, 443, 3, 2, 2, 2, 449, 446, 3, 2, 2, 2, 450, 92, 3, 2, 2, 2, 451, 454, 5, 33, 16, 2, 452, 454, 7, 97, 2, 2, 453, 451, 3, 2, 2, 2, 453, 452, 3, 2, 2, 2, 454, 460, 3, 2, 2, 2, 455, 459, 5, 33, 16, 2, 456, 459, 5, 31, 15, 2, 457, 459, 7, 97, 2, 2, 458, 455, 3, 2, 2, 2, 458, 456, 3, 2, 2, 2, 458, 457, 3, 2, 2, 2, 459, 462, 3, 2, 2, 2, 460, 458, 3, 2, 2, 2, 460, 461, 3, 2, 2, 2, 461, 94, 3, 2, 2, 2, 462, 460, 3, 2, 2, 2, 463, 469, 7, 98, 2, 2, 464, 468, 10, 10, 2, 2, 465, 466, 7, 98, 2, 2, 466, 468, 7, 98, 2, 2, 467, 464, 3, 2, 2, 2, 467, 465, 3, 2, 2, 2, 468, 471, 3, 2, 2, 2, 469, 467, 3, 2, 2, 2, 469, 470, 3, 2, 2, 2, 470, 472, 3, 2, 2, 2, 471, 469, 3, 2, 2, 2, 472, 473, 7, 98, 2, 2, 473, 96, 3, 2, 2, 2, 474, 475, 5, 23, 11, 2, 475, 476, 3, 2, 2, 2, 476, 477, 8, 48, 4, 2, 477, 98, 3, 2, 2, 2, 478, 479, 5, 25, 12, 2, 479, 480, 3, 2, 2, 2, 480, 481, 8, 49, 4, 2, 481, 100, 3, 2, 2, 2, 482, 483, 5, 27, 13, 2, 483, 484, 3, 2, 2, 2, 484, 485, 8, 50, 4, 2, 485, 102, 3, 2, 2, 2, 486, 487, 7, 126, 2, 2, 487, 488, 3, 2, 2, 2, 488, 489, 8, 51, 7, 2, 489, 490, 8, 51, 5, 2, 490, 104, 3, 2, 2, 2, 491, 492, 7, 95, 2, 2, 492, 493, 3, 2, 2, 2, 493, 494, 8, 52, 5, 2, 494, 495, 8, 52, 5, 2, 495, 496, 8, 52, 8, 2, 496, 106, 3, 2, 2, 2, 497, 498, 7, 46, 2, 2, 498, 499, 3, 2, 2, 2, 499, 500, 8, 53, 9, 2, 500, 108, 3, 2, 2, 2, 501, 502, 7, 63, 2, 2, 502, 503, 3, 2, 2, 2, 503, 504, 8, 54, 10, 2, 504, 110, 3, 2, 2, 2, 505, 507, 5, 113, 56, 2, 506, 505, 3, 2, 2, 2, 507, 508, 3, 2, 2, 2, 508, 506, 3, 2, 2, 2, 508, 509, 3, 2, 2, 2, 509, 112, 3, 2, 2, 2, 510, 512, 10, 11, 2, 2, 511, 510, 3, 2, 2, 2, 512, 513, 3, 2, 2, 2, 513, 511, 3, 2, 2, 2, 513, 514, 3, 2, 2, 2, 514, 518, 3, 2, 2, 2, 515, 516, 7, 49, 2, 2, 516, 518, 10, 12, 2, 2, 517, 511, 3, 2, 2, 2, 517, 515, 3, 2, 2, 2, 518, 114, 3, 2, 2, 2, 519, 520, 5, 95, 47, 2, 520, 116, 3, 2, 2, 2, 521, 522, 5, 23, 11, 2, 522, 523, 3, 2, 2, 2, 523, 524, 8, 58, 4, 2, 524, 118, 3, 2, 2, 2, 525, 526, 5, 25, 12, 2, 526, 527, 3, 2, 2, 2, 527, 528, 8, 59, 4, 2, 528, 120, 3, 2, 2, 2, 529, 530, 5, 27, 13, 2, 530, 531, 3, 2, 2, 2, 531, 532, 8, 60, 4, 2, 532, 122, 3, 2, 2, 2, 41, 2, 3, 4, 200, 204, 207, 216, 218, 229, 248, 253, 258, 260, 271, 279, 282, 284, 289, 294, 300, 307, 312, 318, 321, 329, 333, 382, 394, 413, 430, 449, 453, 458, 460, 467, 469, 508, 513, 517, 11, 7, 3, 2, 7, 4, 2, 2, 3, 2, 6, 2, 2, 7, 2, 2, 9, 15, 2, 9, 26, 2, 9, 22, 2, 9, 21, 2] \ No newline at end of file +[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 2, 83, 1396, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 4, 52, 9, 52, 4, 53, 9, 53, 4, 54, 9, 54, 4, 55, 9, 55, 4, 56, 9, 56, 4, 57, 9, 57, 4, 58, 9, 58, 4, 59, 9, 59, 4, 60, 9, 60, 4, 61, 9, 61, 4, 62, 9, 62, 4, 63, 9, 63, 4, 64, 9, 64, 4, 65, 9, 65, 4, 66, 9, 66, 4, 67, 9, 67, 4, 68, 9, 68, 4, 69, 9, 69, 4, 70, 9, 70, 4, 71, 9, 71, 4, 72, 9, 72, 4, 73, 9, 73, 4, 74, 9, 74, 4, 75, 9, 75, 4, 76, 9, 76, 4, 77, 9, 77, 4, 78, 9, 78, 4, 79, 9, 79, 4, 80, 9, 80, 4, 81, 9, 81, 4, 82, 9, 82, 4, 83, 9, 83, 4, 84, 9, 84, 4, 85, 9, 85, 4, 86, 9, 86, 4, 87, 9, 87, 4, 88, 9, 88, 4, 89, 9, 89, 4, 90, 9, 90, 4, 91, 9, 91, 4, 92, 9, 92, 4, 93, 9, 93, 4, 94, 9, 94, 4, 95, 9, 95, 4, 96, 9, 96, 4, 97, 9, 97, 4, 98, 9, 98, 4, 99, 9, 99, 4, 100, 9, 100, 4, 101, 9, 101, 4, 102, 9, 102, 4, 103, 9, 103, 4, 104, 9, 104, 4, 105, 9, 105, 4, 106, 9, 106, 4, 107, 9, 107, 4, 108, 9, 108, 4, 109, 9, 109, 4, 110, 9, 110, 4, 111, 9, 111, 4, 112, 9, 112, 4, 113, 9, 113, 4, 114, 9, 114, 4, 115, 9, 115, 4, 116, 9, 116, 4, 117, 9, 117, 4, 118, 9, 118, 4, 119, 9, 119, 4, 120, 9, 120, 4, 121, 9, 121, 4, 122, 9, 122, 4, 123, 9, 123, 4, 124, 9, 124, 4, 125, 9, 125, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 19, 3, 19, 3, 19, 3, 19, 7, 19, 399, 10, 19, 12, 19, 14, 19, 402, 11, 19, 3, 19, 5, 19, 405, 10, 19, 3, 19, 5, 19, 408, 10, 19, 3, 19, 3, 19, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 7, 20, 417, 10, 20, 12, 20, 14, 20, 420, 11, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 21, 6, 21, 428, 10, 21, 13, 21, 14, 21, 429, 3, 21, 3, 21, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 23, 3, 23, 3, 23, 3, 23, 3, 23, 3, 24, 3, 24, 3, 24, 3, 24, 3, 25, 3, 25, 3, 25, 3, 25, 3, 26, 3, 26, 3, 26, 3, 26, 3, 27, 3, 27, 3, 27, 3, 27, 3, 28, 3, 28, 3, 29, 3, 29, 3, 30, 3, 30, 3, 30, 3, 31, 3, 31, 3, 32, 3, 32, 5, 32, 471, 10, 32, 3, 32, 6, 32, 474, 10, 32, 13, 32, 14, 32, 475, 3, 33, 3, 33, 3, 33, 7, 33, 481, 10, 33, 12, 33, 14, 33, 484, 11, 33, 3, 33, 3, 33, 3, 33, 3, 33, 3, 33, 3, 33, 7, 33, 492, 10, 33, 12, 33, 14, 33, 495, 11, 33, 3, 33, 3, 33, 3, 33, 3, 33, 3, 33, 5, 33, 502, 10, 33, 3, 33, 5, 33, 505, 10, 33, 5, 33, 507, 10, 33, 3, 34, 6, 34, 510, 10, 34, 13, 34, 14, 34, 511, 3, 35, 6, 35, 515, 10, 35, 13, 35, 14, 35, 516, 3, 35, 3, 35, 7, 35, 521, 10, 35, 12, 35, 14, 35, 524, 11, 35, 3, 35, 3, 35, 6, 35, 528, 10, 35, 13, 35, 14, 35, 529, 3, 35, 6, 35, 533, 10, 35, 13, 35, 14, 35, 534, 3, 35, 3, 35, 7, 35, 539, 10, 35, 12, 35, 14, 35, 542, 11, 35, 5, 35, 544, 10, 35, 3, 35, 3, 35, 3, 35, 3, 35, 6, 35, 550, 10, 35, 13, 35, 14, 35, 551, 3, 35, 3, 35, 5, 35, 556, 10, 35, 3, 36, 3, 36, 3, 36, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 5, 37, 589, 10, 37, 3, 38, 3, 38, 3, 38, 3, 38, 3, 39, 3, 39, 3, 40, 3, 40, 3, 41, 3, 41, 3, 42, 3, 42, 3, 43, 3, 43, 3, 43, 3, 43, 3, 43, 3, 44, 3, 44, 3, 44, 3, 44, 3, 44, 3, 45, 3, 45, 3, 45, 3, 45, 3, 46, 3, 46, 3, 46, 3, 46, 3, 46, 3, 47, 3, 47, 3, 47, 3, 47, 3, 47, 3, 47, 3, 48, 3, 48, 3, 48, 3, 49, 3, 49, 3, 49, 3, 50, 3, 50, 3, 50, 3, 51, 3, 51, 3, 51, 3, 51, 3, 51, 3, 52, 3, 52, 3, 52, 3, 53, 3, 53, 3, 54, 3, 54, 3, 55, 3, 55, 3, 55, 3, 55, 3, 55, 3, 56, 3, 56, 3, 56, 3, 56, 3, 56, 3, 56, 3, 56, 3, 56, 3, 56, 3, 56, 3, 57, 3, 57, 3, 57, 3, 57, 3, 57, 3, 57, 3, 57, 3, 57, 3, 57, 5, 57, 673, 10, 57, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 5, 58, 685, 10, 58, 3, 59, 3, 59, 3, 60, 3, 60, 3, 61, 3, 61, 3, 62, 3, 62, 3, 63, 3, 63, 3, 64, 3, 64, 3, 64, 3, 65, 3, 65, 3, 65, 3, 65, 3, 65, 3, 65, 3, 65, 5, 65, 707, 10, 65, 3, 66, 3, 66, 3, 66, 3, 66, 3, 66, 3, 66, 3, 67, 3, 67, 3, 67, 3, 67, 3, 67, 3, 67, 3, 67, 3, 67, 3, 67, 5, 67, 724, 10, 67, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 3, 68, 5, 68, 1088, 10, 68, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 3, 69, 5, 69, 1171, 10, 69, 3, 70, 3, 70, 3, 70, 3, 70, 3, 70, 3, 70, 3, 70, 3, 70, 3, 70, 3, 70, 3, 70, 3, 71, 3, 71, 3, 71, 3, 71, 3, 71, 7, 71, 1189, 10, 71, 12, 71, 14, 71, 1192, 11, 71, 3, 71, 3, 71, 3, 71, 3, 71, 3, 71, 6, 71, 1199, 10, 71, 13, 71, 14, 71, 1200, 5, 71, 1203, 10, 71, 3, 72, 3, 72, 3, 72, 3, 72, 7, 72, 1209, 10, 72, 12, 72, 14, 72, 1212, 11, 72, 3, 72, 3, 72, 3, 73, 3, 73, 3, 73, 3, 73, 3, 74, 3, 74, 3, 74, 3, 74, 3, 75, 3, 75, 3, 75, 3, 75, 3, 76, 3, 76, 3, 76, 3, 76, 3, 76, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 3, 78, 3, 78, 3, 78, 3, 78, 3, 78, 3, 78, 3, 79, 3, 79, 3, 79, 3, 79, 3, 80, 3, 80, 3, 80, 3, 80, 3, 81, 3, 81, 3, 81, 3, 81, 3, 81, 3, 81, 3, 81, 3, 81, 3, 81, 3, 82, 6, 82, 1263, 10, 82, 13, 82, 14, 82, 1264, 3, 83, 6, 83, 1268, 10, 83, 13, 83, 14, 83, 1269, 3, 83, 3, 83, 5, 83, 1274, 10, 83, 3, 84, 3, 84, 3, 85, 3, 85, 3, 85, 3, 85, 3, 86, 3, 86, 3, 86, 3, 86, 3, 87, 3, 87, 3, 87, 3, 87, 3, 88, 3, 88, 3, 88, 3, 89, 3, 89, 3, 89, 3, 89, 3, 89, 3, 90, 3, 90, 3, 90, 3, 90, 3, 90, 3, 91, 3, 91, 3, 91, 3, 91, 3, 91, 3, 91, 3, 92, 3, 92, 3, 92, 3, 92, 3, 93, 3, 93, 3, 93, 3, 93, 3, 94, 6, 94, 1318, 10, 94, 13, 94, 14, 94, 1319, 3, 95, 6, 95, 1323, 10, 95, 13, 95, 14, 95, 1324, 3, 95, 3, 95, 5, 95, 1329, 10, 95, 3, 96, 3, 96, 3, 97, 3, 97, 3, 97, 3, 97, 3, 98, 3, 98, 3, 98, 3, 98, 3, 99, 3, 99, 3, 99, 3, 99, 3, 100, 3, 100, 3, 101, 3, 101, 3, 102, 3, 102, 3, 103, 3, 103, 3, 104, 3, 104, 3, 105, 3, 105, 3, 106, 3, 106, 3, 107, 3, 107, 3, 108, 3, 108, 3, 109, 3, 109, 3, 110, 3, 110, 3, 111, 3, 111, 3, 112, 3, 112, 3, 113, 3, 113, 3, 114, 3, 114, 3, 115, 3, 115, 3, 116, 3, 116, 3, 117, 3, 117, 3, 118, 3, 118, 3, 119, 3, 119, 3, 120, 3, 120, 3, 121, 3, 121, 3, 122, 3, 122, 3, 123, 3, 123, 3, 124, 3, 124, 3, 125, 3, 125, 4, 418, 493, 2, 2, 126, 7, 2, 3, 9, 2, 4, 11, 2, 5, 13, 2, 6, 15, 2, 7, 17, 2, 8, 19, 2, 9, 21, 2, 10, 23, 2, 11, 25, 2, 12, 27, 2, 13, 29, 2, 14, 31, 2, 15, 33, 2, 16, 35, 2, 17, 37, 2, 18, 39, 2, 19, 41, 2, 20, 43, 2, 21, 45, 2, 22, 47, 2, 2, 49, 2, 83, 51, 2, 23, 53, 2, 24, 55, 2, 25, 57, 2, 26, 59, 2, 2, 61, 2, 2, 63, 2, 2, 65, 2, 2, 67, 2, 2, 69, 2, 27, 71, 2, 28, 73, 2, 29, 75, 2, 30, 77, 2, 31, 79, 2, 32, 81, 2, 33, 83, 2, 34, 85, 2, 35, 87, 2, 36, 89, 2, 37, 91, 2, 38, 93, 2, 39, 95, 2, 40, 97, 2, 41, 99, 2, 42, 101, 2, 43, 103, 2, 44, 105, 2, 45, 107, 2, 46, 109, 2, 47, 111, 2, 48, 113, 2, 49, 115, 2, 50, 117, 2, 51, 119, 2, 52, 121, 2, 53, 123, 2, 54, 125, 2, 55, 127, 2, 56, 129, 2, 57, 131, 2, 58, 133, 2, 59, 135, 2, 60, 137, 2, 61, 139, 2, 62, 141, 2, 63, 143, 2, 64, 145, 2, 65, 147, 2, 66, 149, 2, 67, 151, 2, 68, 153, 2, 69, 155, 2, 2, 157, 2, 2, 159, 2, 2, 161, 2, 2, 163, 2, 2, 165, 2, 70, 167, 2, 71, 169, 2, 2, 171, 2, 72, 173, 2, 73, 175, 2, 74, 177, 2, 75, 179, 2, 76, 181, 2, 77, 183, 2, 2, 185, 2, 2, 187, 2, 2, 189, 2, 2, 191, 2, 78, 193, 2, 2, 195, 2, 79, 197, 2, 80, 199, 2, 81, 201, 2, 82, 203, 2, 2, 205, 2, 2, 207, 2, 2, 209, 2, 2, 211, 2, 2, 213, 2, 2, 215, 2, 2, 217, 2, 2, 219, 2, 2, 221, 2, 2, 223, 2, 2, 225, 2, 2, 227, 2, 2, 229, 2, 2, 231, 2, 2, 233, 2, 2, 235, 2, 2, 237, 2, 2, 239, 2, 2, 241, 2, 2, 243, 2, 2, 245, 2, 2, 247, 2, 2, 249, 2, 2, 251, 2, 2, 253, 2, 2, 7, 2, 3, 4, 5, 6, 39, 4, 2, 12, 12, 15, 15, 5, 2, 11, 12, 15, 15, 34, 34, 3, 2, 50, 59, 4, 2, 67, 92, 99, 124, 7, 2, 36, 36, 94, 94, 112, 112, 116, 116, 118, 118, 6, 2, 12, 12, 15, 15, 36, 36, 94, 94, 4, 2, 71, 71, 103, 103, 4, 2, 45, 45, 47, 47, 4, 2, 66, 66, 97, 97, 3, 2, 98, 98, 12, 2, 11, 12, 15, 15, 34, 34, 46, 46, 49, 49, 63, 63, 93, 93, 95, 95, 98, 98, 126, 126, 4, 2, 44, 44, 49, 49, 4, 2, 67, 67, 99, 99, 4, 2, 68, 68, 100, 100, 4, 2, 69, 69, 101, 101, 4, 2, 70, 70, 102, 102, 4, 2, 72, 72, 104, 104, 4, 2, 73, 73, 105, 105, 4, 2, 74, 74, 106, 106, 4, 2, 75, 75, 107, 107, 4, 2, 76, 76, 108, 108, 4, 2, 77, 77, 109, 109, 4, 2, 78, 78, 110, 110, 4, 2, 79, 79, 111, 111, 4, 2, 80, 80, 112, 112, 4, 2, 81, 81, 113, 113, 4, 2, 82, 82, 114, 114, 4, 2, 83, 83, 115, 115, 4, 2, 84, 84, 116, 116, 4, 2, 85, 85, 117, 117, 4, 2, 86, 86, 118, 118, 4, 2, 87, 87, 119, 119, 4, 2, 88, 88, 120, 120, 4, 2, 89, 89, 121, 121, 4, 2, 90, 90, 122, 122, 4, 2, 91, 91, 123, 123, 4, 2, 92, 92, 124, 124, 2, 1464, 2, 7, 3, 2, 2, 2, 2, 9, 3, 2, 2, 2, 2, 11, 3, 2, 2, 2, 2, 13, 3, 2, 2, 2, 2, 15, 3, 2, 2, 2, 2, 17, 3, 2, 2, 2, 2, 19, 3, 2, 2, 2, 2, 21, 3, 2, 2, 2, 2, 23, 3, 2, 2, 2, 2, 25, 3, 2, 2, 2, 2, 27, 3, 2, 2, 2, 2, 29, 3, 2, 2, 2, 2, 31, 3, 2, 2, 2, 2, 33, 3, 2, 2, 2, 2, 35, 3, 2, 2, 2, 2, 37, 3, 2, 2, 2, 2, 39, 3, 2, 2, 2, 2, 41, 3, 2, 2, 2, 2, 43, 3, 2, 2, 2, 2, 45, 3, 2, 2, 2, 3, 47, 3, 2, 2, 2, 3, 49, 3, 2, 2, 2, 3, 51, 3, 2, 2, 2, 3, 53, 3, 2, 2, 2, 3, 55, 3, 2, 2, 2, 4, 57, 3, 2, 2, 2, 4, 69, 3, 2, 2, 2, 4, 71, 3, 2, 2, 2, 4, 73, 3, 2, 2, 2, 4, 75, 3, 2, 2, 2, 4, 77, 3, 2, 2, 2, 4, 79, 3, 2, 2, 2, 4, 81, 3, 2, 2, 2, 4, 83, 3, 2, 2, 2, 4, 85, 3, 2, 2, 2, 4, 87, 3, 2, 2, 2, 4, 89, 3, 2, 2, 2, 4, 91, 3, 2, 2, 2, 4, 93, 3, 2, 2, 2, 4, 95, 3, 2, 2, 2, 4, 97, 3, 2, 2, 2, 4, 99, 3, 2, 2, 2, 4, 101, 3, 2, 2, 2, 4, 103, 3, 2, 2, 2, 4, 105, 3, 2, 2, 2, 4, 107, 3, 2, 2, 2, 4, 109, 3, 2, 2, 2, 4, 111, 3, 2, 2, 2, 4, 113, 3, 2, 2, 2, 4, 115, 3, 2, 2, 2, 4, 117, 3, 2, 2, 2, 4, 119, 3, 2, 2, 2, 4, 121, 3, 2, 2, 2, 4, 123, 3, 2, 2, 2, 4, 125, 3, 2, 2, 2, 4, 127, 3, 2, 2, 2, 4, 129, 3, 2, 2, 2, 4, 131, 3, 2, 2, 2, 4, 133, 3, 2, 2, 2, 4, 135, 3, 2, 2, 2, 4, 137, 3, 2, 2, 2, 4, 139, 3, 2, 2, 2, 4, 141, 3, 2, 2, 2, 4, 143, 3, 2, 2, 2, 4, 145, 3, 2, 2, 2, 4, 147, 3, 2, 2, 2, 4, 149, 3, 2, 2, 2, 4, 151, 3, 2, 2, 2, 4, 153, 3, 2, 2, 2, 5, 155, 3, 2, 2, 2, 5, 157, 3, 2, 2, 2, 5, 159, 3, 2, 2, 2, 5, 161, 3, 2, 2, 2, 5, 163, 3, 2, 2, 2, 5, 165, 3, 2, 2, 2, 5, 167, 3, 2, 2, 2, 5, 171, 3, 2, 2, 2, 5, 173, 3, 2, 2, 2, 5, 175, 3, 2, 2, 2, 5, 177, 3, 2, 2, 2, 6, 179, 3, 2, 2, 2, 6, 181, 3, 2, 2, 2, 6, 183, 3, 2, 2, 2, 6, 185, 3, 2, 2, 2, 6, 187, 3, 2, 2, 2, 6, 189, 3, 2, 2, 2, 6, 191, 3, 2, 2, 2, 6, 195, 3, 2, 2, 2, 6, 197, 3, 2, 2, 2, 6, 199, 3, 2, 2, 2, 6, 201, 3, 2, 2, 2, 7, 255, 3, 2, 2, 2, 9, 265, 3, 2, 2, 2, 11, 272, 3, 2, 2, 2, 13, 279, 3, 2, 2, 2, 15, 289, 3, 2, 2, 2, 17, 296, 3, 2, 2, 2, 19, 302, 3, 2, 2, 2, 21, 310, 3, 2, 2, 2, 23, 318, 3, 2, 2, 2, 25, 325, 3, 2, 2, 2, 27, 337, 3, 2, 2, 2, 29, 345, 3, 2, 2, 2, 31, 355, 3, 2, 2, 2, 33, 362, 3, 2, 2, 2, 35, 371, 3, 2, 2, 2, 37, 378, 3, 2, 2, 2, 39, 387, 3, 2, 2, 2, 41, 394, 3, 2, 2, 2, 43, 411, 3, 2, 2, 2, 45, 427, 3, 2, 2, 2, 47, 433, 3, 2, 2, 2, 49, 438, 3, 2, 2, 2, 51, 443, 3, 2, 2, 2, 53, 447, 3, 2, 2, 2, 55, 451, 3, 2, 2, 2, 57, 455, 3, 2, 2, 2, 59, 459, 3, 2, 2, 2, 61, 461, 3, 2, 2, 2, 63, 463, 3, 2, 2, 2, 65, 466, 3, 2, 2, 2, 67, 468, 3, 2, 2, 2, 69, 506, 3, 2, 2, 2, 71, 509, 3, 2, 2, 2, 73, 555, 3, 2, 2, 2, 75, 557, 3, 2, 2, 2, 77, 588, 3, 2, 2, 2, 79, 590, 3, 2, 2, 2, 81, 594, 3, 2, 2, 2, 83, 596, 3, 2, 2, 2, 85, 598, 3, 2, 2, 2, 87, 600, 3, 2, 2, 2, 89, 602, 3, 2, 2, 2, 91, 607, 3, 2, 2, 2, 93, 612, 3, 2, 2, 2, 95, 616, 3, 2, 2, 2, 97, 621, 3, 2, 2, 2, 99, 627, 3, 2, 2, 2, 101, 630, 3, 2, 2, 2, 103, 633, 3, 2, 2, 2, 105, 636, 3, 2, 2, 2, 107, 641, 3, 2, 2, 2, 109, 644, 3, 2, 2, 2, 111, 646, 3, 2, 2, 2, 113, 648, 3, 2, 2, 2, 115, 653, 3, 2, 2, 2, 117, 672, 3, 2, 2, 2, 119, 684, 3, 2, 2, 2, 121, 686, 3, 2, 2, 2, 123, 688, 3, 2, 2, 2, 125, 690, 3, 2, 2, 2, 127, 692, 3, 2, 2, 2, 129, 694, 3, 2, 2, 2, 131, 696, 3, 2, 2, 2, 133, 706, 3, 2, 2, 2, 135, 708, 3, 2, 2, 2, 137, 723, 3, 2, 2, 2, 139, 1087, 3, 2, 2, 2, 141, 1170, 3, 2, 2, 2, 143, 1172, 3, 2, 2, 2, 145, 1202, 3, 2, 2, 2, 147, 1204, 3, 2, 2, 2, 149, 1215, 3, 2, 2, 2, 151, 1219, 3, 2, 2, 2, 153, 1223, 3, 2, 2, 2, 155, 1227, 3, 2, 2, 2, 157, 1232, 3, 2, 2, 2, 159, 1238, 3, 2, 2, 2, 161, 1244, 3, 2, 2, 2, 163, 1248, 3, 2, 2, 2, 165, 1252, 3, 2, 2, 2, 167, 1262, 3, 2, 2, 2, 169, 1273, 3, 2, 2, 2, 171, 1275, 3, 2, 2, 2, 173, 1277, 3, 2, 2, 2, 175, 1281, 3, 2, 2, 2, 177, 1285, 3, 2, 2, 2, 179, 1289, 3, 2, 2, 2, 181, 1292, 3, 2, 2, 2, 183, 1297, 3, 2, 2, 2, 185, 1302, 3, 2, 2, 2, 187, 1308, 3, 2, 2, 2, 189, 1312, 3, 2, 2, 2, 191, 1317, 3, 2, 2, 2, 193, 1328, 3, 2, 2, 2, 195, 1330, 3, 2, 2, 2, 197, 1332, 3, 2, 2, 2, 199, 1336, 3, 2, 2, 2, 201, 1340, 3, 2, 2, 2, 203, 1344, 3, 2, 2, 2, 205, 1346, 3, 2, 2, 2, 207, 1348, 3, 2, 2, 2, 209, 1350, 3, 2, 2, 2, 211, 1352, 3, 2, 2, 2, 213, 1354, 3, 2, 2, 2, 215, 1356, 3, 2, 2, 2, 217, 1358, 3, 2, 2, 2, 219, 1360, 3, 2, 2, 2, 221, 1362, 3, 2, 2, 2, 223, 1364, 3, 2, 2, 2, 225, 1366, 3, 2, 2, 2, 227, 1368, 3, 2, 2, 2, 229, 1370, 3, 2, 2, 2, 231, 1372, 3, 2, 2, 2, 233, 1374, 3, 2, 2, 2, 235, 1376, 3, 2, 2, 2, 237, 1378, 3, 2, 2, 2, 239, 1380, 3, 2, 2, 2, 241, 1382, 3, 2, 2, 2, 243, 1384, 3, 2, 2, 2, 245, 1386, 3, 2, 2, 2, 247, 1388, 3, 2, 2, 2, 249, 1390, 3, 2, 2, 2, 251, 1392, 3, 2, 2, 2, 253, 1394, 3, 2, 2, 2, 255, 256, 5, 209, 103, 2, 256, 257, 5, 219, 108, 2, 257, 258, 5, 239, 118, 2, 258, 259, 5, 239, 118, 2, 259, 260, 5, 211, 104, 2, 260, 261, 5, 207, 102, 2, 261, 262, 5, 241, 119, 2, 262, 263, 3, 2, 2, 2, 263, 264, 8, 2, 2, 2, 264, 8, 3, 2, 2, 2, 265, 266, 5, 215, 106, 2, 266, 267, 5, 237, 117, 2, 267, 268, 5, 231, 114, 2, 268, 269, 5, 223, 110, 2, 269, 270, 3, 2, 2, 2, 270, 271, 8, 3, 2, 2, 271, 10, 3, 2, 2, 2, 272, 273, 5, 211, 104, 2, 273, 274, 5, 245, 121, 2, 274, 275, 5, 203, 100, 2, 275, 276, 5, 225, 111, 2, 276, 277, 3, 2, 2, 2, 277, 278, 8, 4, 2, 2, 278, 12, 3, 2, 2, 2, 279, 280, 5, 211, 104, 2, 280, 281, 5, 249, 123, 2, 281, 282, 5, 233, 115, 2, 282, 283, 5, 225, 111, 2, 283, 284, 5, 203, 100, 2, 284, 285, 5, 219, 108, 2, 285, 286, 5, 229, 113, 2, 286, 287, 3, 2, 2, 2, 287, 288, 8, 5, 3, 2, 288, 14, 3, 2, 2, 2, 289, 290, 5, 213, 105, 2, 290, 291, 5, 237, 117, 2, 291, 292, 5, 231, 114, 2, 292, 293, 5, 227, 112, 2, 293, 294, 3, 2, 2, 2, 294, 295, 8, 6, 4, 2, 295, 16, 3, 2, 2, 2, 296, 297, 5, 237, 117, 2, 297, 298, 5, 231, 114, 2, 298, 299, 5, 247, 122, 2, 299, 300, 3, 2, 2, 2, 300, 301, 8, 7, 2, 2, 301, 18, 3, 2, 2, 2, 302, 303, 5, 239, 118, 2, 303, 304, 5, 241, 119, 2, 304, 305, 5, 203, 100, 2, 305, 306, 5, 241, 119, 2, 306, 307, 5, 239, 118, 2, 307, 308, 3, 2, 2, 2, 308, 309, 8, 8, 2, 2, 309, 20, 3, 2, 2, 2, 310, 311, 5, 247, 122, 2, 311, 312, 5, 217, 107, 2, 312, 313, 5, 211, 104, 2, 313, 314, 5, 237, 117, 2, 314, 315, 5, 211, 104, 2, 315, 316, 3, 2, 2, 2, 316, 317, 8, 9, 2, 2, 317, 22, 3, 2, 2, 2, 318, 319, 5, 239, 118, 2, 319, 320, 5, 231, 114, 2, 320, 321, 5, 237, 117, 2, 321, 322, 5, 241, 119, 2, 322, 323, 3, 2, 2, 2, 323, 324, 8, 10, 2, 2, 324, 24, 3, 2, 2, 2, 325, 326, 5, 227, 112, 2, 326, 327, 5, 245, 121, 2, 327, 328, 5, 111, 54, 2, 328, 329, 5, 211, 104, 2, 329, 330, 5, 249, 123, 2, 330, 331, 5, 233, 115, 2, 331, 332, 5, 203, 100, 2, 332, 333, 5, 229, 113, 2, 333, 334, 5, 209, 103, 2, 334, 335, 3, 2, 2, 2, 335, 336, 8, 11, 2, 2, 336, 26, 3, 2, 2, 2, 337, 338, 5, 225, 111, 2, 338, 339, 5, 219, 108, 2, 339, 340, 5, 227, 112, 2, 340, 341, 5, 219, 108, 2, 341, 342, 5, 241, 119, 2, 342, 343, 3, 2, 2, 2, 343, 344, 8, 12, 2, 2, 344, 28, 3, 2, 2, 2, 345, 346, 5, 233, 115, 2, 346, 347, 5, 237, 117, 2, 347, 348, 5, 231, 114, 2, 348, 349, 5, 221, 109, 2, 349, 350, 5, 211, 104, 2, 350, 351, 5, 207, 102, 2, 351, 352, 5, 241, 119, 2, 352, 353, 3, 2, 2, 2, 353, 354, 8, 13, 2, 2, 354, 30, 3, 2, 2, 2, 355, 356, 5, 209, 103, 2, 356, 357, 5, 237, 117, 2, 357, 358, 5, 231, 114, 2, 358, 359, 5, 233, 115, 2, 359, 360, 3, 2, 2, 2, 360, 361, 8, 14, 2, 2, 361, 32, 3, 2, 2, 2, 362, 363, 5, 237, 117, 2, 363, 364, 5, 211, 104, 2, 364, 365, 5, 229, 113, 2, 365, 366, 5, 203, 100, 2, 366, 367, 5, 227, 112, 2, 367, 368, 5, 211, 104, 2, 368, 369, 3, 2, 2, 2, 369, 370, 8, 15, 2, 2, 370, 34, 3, 2, 2, 2, 371, 372, 5, 239, 118, 2, 372, 373, 5, 217, 107, 2, 373, 374, 5, 231, 114, 2, 374, 375, 5, 247, 122, 2, 375, 376, 3, 2, 2, 2, 376, 377, 8, 16, 2, 2, 377, 36, 3, 2, 2, 2, 378, 379, 5, 211, 104, 2, 379, 380, 5, 229, 113, 2, 380, 381, 5, 237, 117, 2, 381, 382, 5, 219, 108, 2, 382, 383, 5, 207, 102, 2, 383, 384, 5, 217, 107, 2, 384, 385, 3, 2, 2, 2, 385, 386, 8, 17, 5, 2, 386, 38, 3, 2, 2, 2, 387, 388, 5, 223, 110, 2, 388, 389, 5, 211, 104, 2, 389, 390, 5, 211, 104, 2, 390, 391, 5, 233, 115, 2, 391, 392, 3, 2, 2, 2, 392, 393, 8, 18, 2, 2, 393, 40, 3, 2, 2, 2, 394, 395, 7, 49, 2, 2, 395, 396, 7, 49, 2, 2, 396, 400, 3, 2, 2, 2, 397, 399, 10, 2, 2, 2, 398, 397, 3, 2, 2, 2, 399, 402, 3, 2, 2, 2, 400, 398, 3, 2, 2, 2, 400, 401, 3, 2, 2, 2, 401, 404, 3, 2, 2, 2, 402, 400, 3, 2, 2, 2, 403, 405, 7, 15, 2, 2, 404, 403, 3, 2, 2, 2, 404, 405, 3, 2, 2, 2, 405, 407, 3, 2, 2, 2, 406, 408, 7, 12, 2, 2, 407, 406, 3, 2, 2, 2, 407, 408, 3, 2, 2, 2, 408, 409, 3, 2, 2, 2, 409, 410, 8, 19, 6, 2, 410, 42, 3, 2, 2, 2, 411, 412, 7, 49, 2, 2, 412, 413, 7, 44, 2, 2, 413, 418, 3, 2, 2, 2, 414, 417, 5, 43, 20, 2, 415, 417, 11, 2, 2, 2, 416, 414, 3, 2, 2, 2, 416, 415, 3, 2, 2, 2, 417, 420, 3, 2, 2, 2, 418, 419, 3, 2, 2, 2, 418, 416, 3, 2, 2, 2, 419, 421, 3, 2, 2, 2, 420, 418, 3, 2, 2, 2, 421, 422, 7, 44, 2, 2, 422, 423, 7, 49, 2, 2, 423, 424, 3, 2, 2, 2, 424, 425, 8, 20, 6, 2, 425, 44, 3, 2, 2, 2, 426, 428, 9, 3, 2, 2, 427, 426, 3, 2, 2, 2, 428, 429, 3, 2, 2, 2, 429, 427, 3, 2, 2, 2, 429, 430, 3, 2, 2, 2, 430, 431, 3, 2, 2, 2, 431, 432, 8, 21, 6, 2, 432, 46, 3, 2, 2, 2, 433, 434, 7, 93, 2, 2, 434, 435, 3, 2, 2, 2, 435, 436, 8, 22, 7, 2, 436, 437, 8, 22, 8, 2, 437, 48, 3, 2, 2, 2, 438, 439, 7, 126, 2, 2, 439, 440, 3, 2, 2, 2, 440, 441, 8, 23, 9, 2, 441, 442, 8, 23, 10, 2, 442, 50, 3, 2, 2, 2, 443, 444, 5, 45, 21, 2, 444, 445, 3, 2, 2, 2, 445, 446, 8, 24, 6, 2, 446, 52, 3, 2, 2, 2, 447, 448, 5, 41, 19, 2, 448, 449, 3, 2, 2, 2, 449, 450, 8, 25, 6, 2, 450, 54, 3, 2, 2, 2, 451, 452, 5, 43, 20, 2, 452, 453, 3, 2, 2, 2, 453, 454, 8, 26, 6, 2, 454, 56, 3, 2, 2, 2, 455, 456, 7, 126, 2, 2, 456, 457, 3, 2, 2, 2, 457, 458, 8, 27, 10, 2, 458, 58, 3, 2, 2, 2, 459, 460, 9, 4, 2, 2, 460, 60, 3, 2, 2, 2, 461, 462, 9, 5, 2, 2, 462, 62, 3, 2, 2, 2, 463, 464, 7, 94, 2, 2, 464, 465, 9, 6, 2, 2, 465, 64, 3, 2, 2, 2, 466, 467, 10, 7, 2, 2, 467, 66, 3, 2, 2, 2, 468, 470, 9, 8, 2, 2, 469, 471, 9, 9, 2, 2, 470, 469, 3, 2, 2, 2, 470, 471, 3, 2, 2, 2, 471, 473, 3, 2, 2, 2, 472, 474, 5, 59, 28, 2, 473, 472, 3, 2, 2, 2, 474, 475, 3, 2, 2, 2, 475, 473, 3, 2, 2, 2, 475, 476, 3, 2, 2, 2, 476, 68, 3, 2, 2, 2, 477, 482, 7, 36, 2, 2, 478, 481, 5, 63, 30, 2, 479, 481, 5, 65, 31, 2, 480, 478, 3, 2, 2, 2, 480, 479, 3, 2, 2, 2, 481, 484, 3, 2, 2, 2, 482, 480, 3, 2, 2, 2, 482, 483, 3, 2, 2, 2, 483, 485, 3, 2, 2, 2, 484, 482, 3, 2, 2, 2, 485, 507, 7, 36, 2, 2, 486, 487, 7, 36, 2, 2, 487, 488, 7, 36, 2, 2, 488, 489, 7, 36, 2, 2, 489, 493, 3, 2, 2, 2, 490, 492, 10, 2, 2, 2, 491, 490, 3, 2, 2, 2, 492, 495, 3, 2, 2, 2, 493, 494, 3, 2, 2, 2, 493, 491, 3, 2, 2, 2, 494, 496, 3, 2, 2, 2, 495, 493, 3, 2, 2, 2, 496, 497, 7, 36, 2, 2, 497, 498, 7, 36, 2, 2, 498, 499, 7, 36, 2, 2, 499, 501, 3, 2, 2, 2, 500, 502, 7, 36, 2, 2, 501, 500, 3, 2, 2, 2, 501, 502, 3, 2, 2, 2, 502, 504, 3, 2, 2, 2, 503, 505, 7, 36, 2, 2, 504, 503, 3, 2, 2, 2, 504, 505, 3, 2, 2, 2, 505, 507, 3, 2, 2, 2, 506, 477, 3, 2, 2, 2, 506, 486, 3, 2, 2, 2, 507, 70, 3, 2, 2, 2, 508, 510, 5, 59, 28, 2, 509, 508, 3, 2, 2, 2, 510, 511, 3, 2, 2, 2, 511, 509, 3, 2, 2, 2, 511, 512, 3, 2, 2, 2, 512, 72, 3, 2, 2, 2, 513, 515, 5, 59, 28, 2, 514, 513, 3, 2, 2, 2, 515, 516, 3, 2, 2, 2, 516, 514, 3, 2, 2, 2, 516, 517, 3, 2, 2, 2, 517, 518, 3, 2, 2, 2, 518, 522, 5, 85, 41, 2, 519, 521, 5, 59, 28, 2, 520, 519, 3, 2, 2, 2, 521, 524, 3, 2, 2, 2, 522, 520, 3, 2, 2, 2, 522, 523, 3, 2, 2, 2, 523, 556, 3, 2, 2, 2, 524, 522, 3, 2, 2, 2, 525, 527, 5, 85, 41, 2, 526, 528, 5, 59, 28, 2, 527, 526, 3, 2, 2, 2, 528, 529, 3, 2, 2, 2, 529, 527, 3, 2, 2, 2, 529, 530, 3, 2, 2, 2, 530, 556, 3, 2, 2, 2, 531, 533, 5, 59, 28, 2, 532, 531, 3, 2, 2, 2, 533, 534, 3, 2, 2, 2, 534, 532, 3, 2, 2, 2, 534, 535, 3, 2, 2, 2, 535, 543, 3, 2, 2, 2, 536, 540, 5, 85, 41, 2, 537, 539, 5, 59, 28, 2, 538, 537, 3, 2, 2, 2, 539, 542, 3, 2, 2, 2, 540, 538, 3, 2, 2, 2, 540, 541, 3, 2, 2, 2, 541, 544, 3, 2, 2, 2, 542, 540, 3, 2, 2, 2, 543, 536, 3, 2, 2, 2, 543, 544, 3, 2, 2, 2, 544, 545, 3, 2, 2, 2, 545, 546, 5, 67, 32, 2, 546, 556, 3, 2, 2, 2, 547, 549, 5, 85, 41, 2, 548, 550, 5, 59, 28, 2, 549, 548, 3, 2, 2, 2, 550, 551, 3, 2, 2, 2, 551, 549, 3, 2, 2, 2, 551, 552, 3, 2, 2, 2, 552, 553, 3, 2, 2, 2, 553, 554, 5, 67, 32, 2, 554, 556, 3, 2, 2, 2, 555, 514, 3, 2, 2, 2, 555, 525, 3, 2, 2, 2, 555, 532, 3, 2, 2, 2, 555, 547, 3, 2, 2, 2, 556, 74, 3, 2, 2, 2, 557, 558, 7, 100, 2, 2, 558, 559, 7, 123, 2, 2, 559, 76, 3, 2, 2, 2, 560, 561, 7, 123, 2, 2, 561, 562, 7, 103, 2, 2, 562, 563, 7, 99, 2, 2, 563, 589, 7, 116, 2, 2, 564, 565, 7, 111, 2, 2, 565, 566, 7, 113, 2, 2, 566, 567, 7, 112, 2, 2, 567, 568, 7, 118, 2, 2, 568, 589, 7, 106, 2, 2, 569, 570, 7, 102, 2, 2, 570, 571, 7, 99, 2, 2, 571, 589, 7, 123, 2, 2, 572, 573, 7, 117, 2, 2, 573, 574, 7, 103, 2, 2, 574, 575, 7, 101, 2, 2, 575, 576, 7, 113, 2, 2, 576, 577, 7, 112, 2, 2, 577, 589, 7, 102, 2, 2, 578, 579, 7, 111, 2, 2, 579, 580, 7, 107, 2, 2, 580, 581, 7, 112, 2, 2, 581, 582, 7, 119, 2, 2, 582, 583, 7, 118, 2, 2, 583, 589, 7, 103, 2, 2, 584, 585, 7, 106, 2, 2, 585, 586, 7, 113, 2, 2, 586, 587, 7, 119, 2, 2, 587, 589, 7, 116, 2, 2, 588, 560, 3, 2, 2, 2, 588, 564, 3, 2, 2, 2, 588, 569, 3, 2, 2, 2, 588, 572, 3, 2, 2, 2, 588, 578, 3, 2, 2, 2, 588, 584, 3, 2, 2, 2, 589, 78, 3, 2, 2, 2, 590, 591, 7, 99, 2, 2, 591, 592, 7, 112, 2, 2, 592, 593, 7, 102, 2, 2, 593, 80, 3, 2, 2, 2, 594, 595, 7, 63, 2, 2, 595, 82, 3, 2, 2, 2, 596, 597, 7, 46, 2, 2, 597, 84, 3, 2, 2, 2, 598, 599, 7, 48, 2, 2, 599, 86, 3, 2, 2, 2, 600, 601, 7, 42, 2, 2, 601, 88, 3, 2, 2, 2, 602, 603, 7, 93, 2, 2, 603, 604, 3, 2, 2, 2, 604, 605, 8, 43, 2, 2, 605, 606, 8, 43, 2, 2, 606, 90, 3, 2, 2, 2, 607, 608, 7, 95, 2, 2, 608, 609, 3, 2, 2, 2, 609, 610, 8, 44, 10, 2, 610, 611, 8, 44, 10, 2, 611, 92, 3, 2, 2, 2, 612, 613, 5, 229, 113, 2, 613, 614, 5, 231, 114, 2, 614, 615, 5, 241, 119, 2, 615, 94, 3, 2, 2, 2, 616, 617, 5, 225, 111, 2, 617, 618, 5, 219, 108, 2, 618, 619, 5, 223, 110, 2, 619, 620, 5, 211, 104, 2, 620, 96, 3, 2, 2, 2, 621, 622, 5, 237, 117, 2, 622, 623, 5, 225, 111, 2, 623, 624, 5, 219, 108, 2, 624, 625, 5, 223, 110, 2, 625, 626, 5, 211, 104, 2, 626, 98, 3, 2, 2, 2, 627, 628, 5, 219, 108, 2, 628, 629, 5, 229, 113, 2, 629, 100, 3, 2, 2, 2, 630, 631, 5, 219, 108, 2, 631, 632, 5, 239, 118, 2, 632, 102, 3, 2, 2, 2, 633, 634, 5, 203, 100, 2, 634, 635, 5, 239, 118, 2, 635, 104, 3, 2, 2, 2, 636, 637, 5, 229, 113, 2, 637, 638, 5, 243, 120, 2, 638, 639, 5, 225, 111, 2, 639, 640, 5, 225, 111, 2, 640, 106, 3, 2, 2, 2, 641, 642, 7, 113, 2, 2, 642, 643, 7, 116, 2, 2, 643, 108, 3, 2, 2, 2, 644, 645, 7, 43, 2, 2, 645, 110, 3, 2, 2, 2, 646, 647, 7, 97, 2, 2, 647, 112, 3, 2, 2, 2, 648, 649, 7, 107, 2, 2, 649, 650, 7, 112, 2, 2, 650, 651, 7, 104, 2, 2, 651, 652, 7, 113, 2, 2, 652, 114, 3, 2, 2, 2, 653, 654, 7, 104, 2, 2, 654, 655, 7, 119, 2, 2, 655, 656, 7, 112, 2, 2, 656, 657, 7, 101, 2, 2, 657, 658, 7, 118, 2, 2, 658, 659, 7, 107, 2, 2, 659, 660, 7, 113, 2, 2, 660, 661, 7, 112, 2, 2, 661, 662, 7, 117, 2, 2, 662, 116, 3, 2, 2, 2, 663, 664, 7, 118, 2, 2, 664, 665, 7, 116, 2, 2, 665, 666, 7, 119, 2, 2, 666, 673, 7, 103, 2, 2, 667, 668, 7, 104, 2, 2, 668, 669, 7, 99, 2, 2, 669, 670, 7, 110, 2, 2, 670, 671, 7, 117, 2, 2, 671, 673, 7, 103, 2, 2, 672, 663, 3, 2, 2, 2, 672, 667, 3, 2, 2, 2, 673, 118, 3, 2, 2, 2, 674, 675, 7, 63, 2, 2, 675, 685, 7, 63, 2, 2, 676, 677, 7, 35, 2, 2, 677, 685, 7, 63, 2, 2, 678, 685, 7, 62, 2, 2, 679, 680, 7, 62, 2, 2, 680, 685, 7, 63, 2, 2, 681, 685, 7, 64, 2, 2, 682, 683, 7, 64, 2, 2, 683, 685, 7, 63, 2, 2, 684, 674, 3, 2, 2, 2, 684, 676, 3, 2, 2, 2, 684, 678, 3, 2, 2, 2, 684, 679, 3, 2, 2, 2, 684, 681, 3, 2, 2, 2, 684, 682, 3, 2, 2, 2, 685, 120, 3, 2, 2, 2, 686, 687, 7, 45, 2, 2, 687, 122, 3, 2, 2, 2, 688, 689, 7, 47, 2, 2, 689, 124, 3, 2, 2, 2, 690, 691, 7, 44, 2, 2, 691, 126, 3, 2, 2, 2, 692, 693, 7, 49, 2, 2, 693, 128, 3, 2, 2, 2, 694, 695, 7, 39, 2, 2, 695, 130, 3, 2, 2, 2, 696, 697, 7, 51, 2, 2, 697, 698, 7, 50, 2, 2, 698, 132, 3, 2, 2, 2, 699, 700, 7, 99, 2, 2, 700, 701, 7, 117, 2, 2, 701, 707, 7, 101, 2, 2, 702, 703, 7, 102, 2, 2, 703, 704, 7, 103, 2, 2, 704, 705, 7, 117, 2, 2, 705, 707, 7, 101, 2, 2, 706, 699, 3, 2, 2, 2, 706, 702, 3, 2, 2, 2, 707, 134, 3, 2, 2, 2, 708, 709, 7, 112, 2, 2, 709, 710, 7, 119, 2, 2, 710, 711, 7, 110, 2, 2, 711, 712, 7, 110, 2, 2, 712, 713, 7, 117, 2, 2, 713, 136, 3, 2, 2, 2, 714, 715, 7, 104, 2, 2, 715, 716, 7, 107, 2, 2, 716, 717, 7, 116, 2, 2, 717, 718, 7, 117, 2, 2, 718, 724, 7, 118, 2, 2, 719, 720, 7, 110, 2, 2, 720, 721, 7, 99, 2, 2, 721, 722, 7, 117, 2, 2, 722, 724, 7, 118, 2, 2, 723, 714, 3, 2, 2, 2, 723, 719, 3, 2, 2, 2, 724, 138, 3, 2, 2, 2, 725, 726, 5, 237, 117, 2, 726, 727, 5, 231, 114, 2, 727, 728, 5, 243, 120, 2, 728, 729, 5, 229, 113, 2, 729, 730, 5, 209, 103, 2, 730, 1088, 3, 2, 2, 2, 731, 732, 5, 203, 100, 2, 732, 733, 5, 205, 101, 2, 733, 734, 5, 239, 118, 2, 734, 1088, 3, 2, 2, 2, 735, 736, 5, 233, 115, 2, 736, 737, 5, 231, 114, 2, 737, 738, 5, 247, 122, 2, 738, 1088, 3, 2, 2, 2, 739, 740, 5, 225, 111, 2, 740, 741, 5, 231, 114, 2, 741, 742, 5, 215, 106, 2, 742, 743, 5, 131, 64, 2, 743, 1088, 3, 2, 2, 2, 744, 745, 5, 233, 115, 2, 745, 746, 5, 219, 108, 2, 746, 1088, 3, 2, 2, 2, 747, 748, 5, 241, 119, 2, 748, 749, 5, 203, 100, 2, 749, 750, 5, 243, 120, 2, 750, 1088, 3, 2, 2, 2, 751, 1088, 5, 211, 104, 2, 752, 753, 5, 239, 118, 2, 753, 754, 5, 243, 120, 2, 754, 755, 5, 205, 101, 2, 755, 756, 5, 239, 118, 2, 756, 757, 5, 241, 119, 2, 757, 758, 5, 237, 117, 2, 758, 759, 5, 219, 108, 2, 759, 760, 5, 229, 113, 2, 760, 761, 5, 215, 106, 2, 761, 1088, 3, 2, 2, 2, 762, 763, 5, 241, 119, 2, 763, 764, 5, 237, 117, 2, 764, 765, 5, 219, 108, 2, 765, 766, 5, 227, 112, 2, 766, 1088, 3, 2, 2, 2, 767, 768, 5, 207, 102, 2, 768, 769, 5, 231, 114, 2, 769, 770, 5, 229, 113, 2, 770, 771, 5, 207, 102, 2, 771, 772, 5, 203, 100, 2, 772, 773, 5, 241, 119, 2, 773, 1088, 3, 2, 2, 2, 774, 775, 5, 239, 118, 2, 775, 776, 5, 241, 119, 2, 776, 777, 5, 203, 100, 2, 777, 778, 5, 237, 117, 2, 778, 779, 5, 241, 119, 2, 779, 780, 5, 239, 118, 2, 780, 781, 5, 111, 54, 2, 781, 782, 5, 247, 122, 2, 782, 783, 5, 219, 108, 2, 783, 784, 5, 241, 119, 2, 784, 785, 5, 217, 107, 2, 785, 1088, 3, 2, 2, 2, 786, 787, 5, 209, 103, 2, 787, 788, 5, 203, 100, 2, 788, 789, 5, 241, 119, 2, 789, 790, 5, 211, 104, 2, 790, 791, 5, 111, 54, 2, 791, 792, 5, 213, 105, 2, 792, 793, 5, 231, 114, 2, 793, 794, 5, 237, 117, 2, 794, 795, 5, 227, 112, 2, 795, 796, 5, 203, 100, 2, 796, 797, 5, 241, 119, 2, 797, 1088, 3, 2, 2, 2, 798, 799, 5, 209, 103, 2, 799, 800, 5, 203, 100, 2, 800, 801, 5, 241, 119, 2, 801, 802, 5, 211, 104, 2, 802, 803, 5, 111, 54, 2, 803, 804, 5, 241, 119, 2, 804, 805, 5, 237, 117, 2, 805, 806, 5, 243, 120, 2, 806, 807, 5, 229, 113, 2, 807, 808, 5, 207, 102, 2, 808, 1088, 3, 2, 2, 2, 809, 810, 5, 209, 103, 2, 810, 811, 5, 203, 100, 2, 811, 812, 5, 241, 119, 2, 812, 813, 5, 211, 104, 2, 813, 814, 5, 111, 54, 2, 814, 815, 5, 233, 115, 2, 815, 816, 5, 203, 100, 2, 816, 817, 5, 237, 117, 2, 817, 818, 5, 239, 118, 2, 818, 819, 5, 211, 104, 2, 819, 1088, 3, 2, 2, 2, 820, 821, 5, 203, 100, 2, 821, 822, 5, 243, 120, 2, 822, 823, 5, 241, 119, 2, 823, 824, 5, 231, 114, 2, 824, 825, 5, 111, 54, 2, 825, 826, 5, 205, 101, 2, 826, 827, 5, 243, 120, 2, 827, 828, 5, 207, 102, 2, 828, 829, 5, 223, 110, 2, 829, 830, 5, 211, 104, 2, 830, 831, 5, 241, 119, 2, 831, 1088, 3, 2, 2, 2, 832, 833, 5, 219, 108, 2, 833, 834, 5, 239, 118, 2, 834, 835, 5, 111, 54, 2, 835, 836, 5, 213, 105, 2, 836, 837, 5, 219, 108, 2, 837, 838, 5, 229, 113, 2, 838, 839, 5, 219, 108, 2, 839, 840, 5, 241, 119, 2, 840, 841, 5, 211, 104, 2, 841, 1088, 3, 2, 2, 2, 842, 843, 5, 219, 108, 2, 843, 844, 5, 239, 118, 2, 844, 845, 5, 111, 54, 2, 845, 846, 5, 219, 108, 2, 846, 847, 5, 229, 113, 2, 847, 848, 5, 213, 105, 2, 848, 849, 5, 219, 108, 2, 849, 850, 5, 229, 113, 2, 850, 851, 5, 219, 108, 2, 851, 852, 5, 241, 119, 2, 852, 853, 5, 211, 104, 2, 853, 1088, 3, 2, 2, 2, 854, 855, 5, 207, 102, 2, 855, 856, 5, 203, 100, 2, 856, 857, 5, 239, 118, 2, 857, 858, 5, 211, 104, 2, 858, 1088, 3, 2, 2, 2, 859, 860, 5, 225, 111, 2, 860, 861, 5, 211, 104, 2, 861, 862, 5, 229, 113, 2, 862, 863, 5, 215, 106, 2, 863, 864, 5, 241, 119, 2, 864, 865, 5, 217, 107, 2, 865, 1088, 3, 2, 2, 2, 866, 867, 5, 227, 112, 2, 867, 868, 5, 245, 121, 2, 868, 869, 5, 111, 54, 2, 869, 870, 5, 227, 112, 2, 870, 871, 5, 203, 100, 2, 871, 872, 5, 249, 123, 2, 872, 1088, 3, 2, 2, 2, 873, 874, 5, 227, 112, 2, 874, 875, 5, 245, 121, 2, 875, 876, 5, 111, 54, 2, 876, 877, 5, 227, 112, 2, 877, 878, 5, 219, 108, 2, 878, 879, 5, 229, 113, 2, 879, 1088, 3, 2, 2, 2, 880, 881, 5, 227, 112, 2, 881, 882, 5, 245, 121, 2, 882, 883, 5, 111, 54, 2, 883, 884, 5, 203, 100, 2, 884, 885, 5, 245, 121, 2, 885, 886, 5, 215, 106, 2, 886, 1088, 3, 2, 2, 2, 887, 888, 5, 227, 112, 2, 888, 889, 5, 245, 121, 2, 889, 890, 5, 111, 54, 2, 890, 891, 5, 239, 118, 2, 891, 892, 5, 243, 120, 2, 892, 893, 5, 227, 112, 2, 893, 1088, 3, 2, 2, 2, 894, 895, 5, 227, 112, 2, 895, 896, 5, 245, 121, 2, 896, 897, 5, 111, 54, 2, 897, 898, 5, 207, 102, 2, 898, 899, 5, 231, 114, 2, 899, 900, 5, 243, 120, 2, 900, 901, 5, 229, 113, 2, 901, 902, 5, 241, 119, 2, 902, 1088, 3, 2, 2, 2, 903, 904, 5, 227, 112, 2, 904, 905, 5, 245, 121, 2, 905, 906, 5, 111, 54, 2, 906, 907, 5, 207, 102, 2, 907, 908, 5, 231, 114, 2, 908, 909, 5, 229, 113, 2, 909, 910, 5, 207, 102, 2, 910, 911, 5, 203, 100, 2, 911, 912, 5, 241, 119, 2, 912, 1088, 3, 2, 2, 2, 913, 914, 5, 227, 112, 2, 914, 915, 5, 245, 121, 2, 915, 916, 5, 111, 54, 2, 916, 917, 5, 221, 109, 2, 917, 918, 5, 231, 114, 2, 918, 919, 5, 219, 108, 2, 919, 920, 5, 229, 113, 2, 920, 1088, 3, 2, 2, 2, 921, 922, 5, 227, 112, 2, 922, 923, 5, 245, 121, 2, 923, 924, 5, 111, 54, 2, 924, 925, 5, 227, 112, 2, 925, 926, 5, 211, 104, 2, 926, 927, 5, 209, 103, 2, 927, 928, 5, 219, 108, 2, 928, 929, 5, 203, 100, 2, 929, 930, 5, 229, 113, 2, 930, 1088, 3, 2, 2, 2, 931, 932, 5, 227, 112, 2, 932, 933, 5, 245, 121, 2, 933, 934, 5, 111, 54, 2, 934, 935, 5, 209, 103, 2, 935, 936, 5, 211, 104, 2, 936, 937, 5, 209, 103, 2, 937, 938, 5, 243, 120, 2, 938, 939, 5, 233, 115, 2, 939, 940, 5, 211, 104, 2, 940, 1088, 3, 2, 2, 2, 941, 942, 5, 227, 112, 2, 942, 943, 5, 211, 104, 2, 943, 944, 5, 241, 119, 2, 944, 945, 5, 203, 100, 2, 945, 946, 5, 209, 103, 2, 946, 947, 5, 203, 100, 2, 947, 948, 5, 241, 119, 2, 948, 949, 5, 203, 100, 2, 949, 1088, 3, 2, 2, 2, 950, 951, 5, 239, 118, 2, 951, 952, 5, 233, 115, 2, 952, 953, 5, 225, 111, 2, 953, 954, 5, 219, 108, 2, 954, 955, 5, 241, 119, 2, 955, 1088, 3, 2, 2, 2, 956, 957, 5, 241, 119, 2, 957, 958, 5, 231, 114, 2, 958, 959, 5, 111, 54, 2, 959, 960, 5, 239, 118, 2, 960, 961, 5, 241, 119, 2, 961, 962, 5, 237, 117, 2, 962, 963, 5, 219, 108, 2, 963, 964, 5, 229, 113, 2, 964, 965, 5, 215, 106, 2, 965, 1088, 3, 2, 2, 2, 966, 967, 5, 241, 119, 2, 967, 968, 5, 231, 114, 2, 968, 969, 5, 111, 54, 2, 969, 970, 5, 239, 118, 2, 970, 971, 5, 241, 119, 2, 971, 972, 5, 237, 117, 2, 972, 1088, 3, 2, 2, 2, 973, 974, 5, 241, 119, 2, 974, 975, 5, 231, 114, 2, 975, 976, 5, 111, 54, 2, 976, 977, 5, 205, 101, 2, 977, 978, 5, 231, 114, 2, 978, 979, 5, 231, 114, 2, 979, 980, 5, 225, 111, 2, 980, 1088, 3, 2, 2, 2, 981, 982, 5, 241, 119, 2, 982, 983, 5, 231, 114, 2, 983, 984, 5, 111, 54, 2, 984, 985, 5, 205, 101, 2, 985, 986, 5, 231, 114, 2, 986, 987, 5, 231, 114, 2, 987, 988, 5, 225, 111, 2, 988, 989, 5, 211, 104, 2, 989, 990, 5, 203, 100, 2, 990, 991, 5, 229, 113, 2, 991, 1088, 3, 2, 2, 2, 992, 993, 5, 241, 119, 2, 993, 994, 5, 231, 114, 2, 994, 995, 5, 111, 54, 2, 995, 996, 5, 209, 103, 2, 996, 997, 5, 203, 100, 2, 997, 998, 5, 241, 119, 2, 998, 999, 5, 211, 104, 2, 999, 1000, 5, 241, 119, 2, 1000, 1001, 5, 219, 108, 2, 1001, 1002, 5, 227, 112, 2, 1002, 1003, 5, 211, 104, 2, 1003, 1088, 3, 2, 2, 2, 1004, 1005, 5, 241, 119, 2, 1005, 1006, 5, 231, 114, 2, 1006, 1007, 5, 111, 54, 2, 1007, 1008, 5, 209, 103, 2, 1008, 1009, 5, 241, 119, 2, 1009, 1088, 3, 2, 2, 2, 1010, 1011, 5, 241, 119, 2, 1011, 1012, 5, 231, 114, 2, 1012, 1013, 5, 111, 54, 2, 1013, 1014, 5, 209, 103, 2, 1014, 1015, 5, 205, 101, 2, 1015, 1016, 5, 225, 111, 2, 1016, 1088, 3, 2, 2, 2, 1017, 1018, 5, 241, 119, 2, 1018, 1019, 5, 231, 114, 2, 1019, 1020, 5, 111, 54, 2, 1020, 1021, 5, 209, 103, 2, 1021, 1022, 5, 231, 114, 2, 1022, 1023, 5, 243, 120, 2, 1023, 1024, 5, 205, 101, 2, 1024, 1025, 5, 225, 111, 2, 1025, 1026, 5, 211, 104, 2, 1026, 1088, 3, 2, 2, 2, 1027, 1028, 5, 241, 119, 2, 1028, 1029, 5, 231, 114, 2, 1029, 1030, 5, 111, 54, 2, 1030, 1031, 5, 219, 108, 2, 1031, 1032, 5, 229, 113, 2, 1032, 1033, 5, 241, 119, 2, 1033, 1088, 3, 2, 2, 2, 1034, 1035, 5, 241, 119, 2, 1035, 1036, 5, 231, 114, 2, 1036, 1037, 5, 111, 54, 2, 1037, 1038, 5, 219, 108, 2, 1038, 1039, 5, 229, 113, 2, 1039, 1040, 5, 241, 119, 2, 1040, 1041, 5, 211, 104, 2, 1041, 1042, 5, 215, 106, 2, 1042, 1043, 5, 211, 104, 2, 1043, 1044, 5, 237, 117, 2, 1044, 1088, 3, 2, 2, 2, 1045, 1046, 5, 241, 119, 2, 1046, 1047, 5, 231, 114, 2, 1047, 1048, 5, 111, 54, 2, 1048, 1049, 5, 225, 111, 2, 1049, 1050, 5, 231, 114, 2, 1050, 1051, 5, 229, 113, 2, 1051, 1052, 5, 215, 106, 2, 1052, 1088, 3, 2, 2, 2, 1053, 1054, 5, 241, 119, 2, 1054, 1055, 5, 231, 114, 2, 1055, 1056, 5, 111, 54, 2, 1056, 1057, 5, 219, 108, 2, 1057, 1058, 5, 233, 115, 2, 1058, 1088, 3, 2, 2, 2, 1059, 1060, 5, 241, 119, 2, 1060, 1061, 5, 231, 114, 2, 1061, 1062, 5, 111, 54, 2, 1062, 1063, 5, 245, 121, 2, 1063, 1064, 5, 211, 104, 2, 1064, 1065, 5, 237, 117, 2, 1065, 1066, 5, 239, 118, 2, 1066, 1067, 5, 219, 108, 2, 1067, 1068, 5, 231, 114, 2, 1068, 1069, 5, 229, 113, 2, 1069, 1088, 3, 2, 2, 2, 1070, 1071, 5, 241, 119, 2, 1071, 1072, 5, 231, 114, 2, 1072, 1073, 5, 111, 54, 2, 1073, 1074, 5, 243, 120, 2, 1074, 1075, 5, 229, 113, 2, 1075, 1076, 5, 239, 118, 2, 1076, 1077, 5, 219, 108, 2, 1077, 1078, 5, 215, 106, 2, 1078, 1079, 5, 229, 113, 2, 1079, 1080, 5, 211, 104, 2, 1080, 1081, 5, 209, 103, 2, 1081, 1082, 5, 111, 54, 2, 1082, 1083, 5, 225, 111, 2, 1083, 1084, 5, 231, 114, 2, 1084, 1085, 5, 229, 113, 2, 1085, 1086, 5, 215, 106, 2, 1086, 1088, 3, 2, 2, 2, 1087, 725, 3, 2, 2, 2, 1087, 731, 3, 2, 2, 2, 1087, 735, 3, 2, 2, 2, 1087, 739, 3, 2, 2, 2, 1087, 744, 3, 2, 2, 2, 1087, 747, 3, 2, 2, 2, 1087, 751, 3, 2, 2, 2, 1087, 752, 3, 2, 2, 2, 1087, 762, 3, 2, 2, 2, 1087, 767, 3, 2, 2, 2, 1087, 774, 3, 2, 2, 2, 1087, 786, 3, 2, 2, 2, 1087, 798, 3, 2, 2, 2, 1087, 809, 3, 2, 2, 2, 1087, 820, 3, 2, 2, 2, 1087, 832, 3, 2, 2, 2, 1087, 842, 3, 2, 2, 2, 1087, 854, 3, 2, 2, 2, 1087, 859, 3, 2, 2, 2, 1087, 866, 3, 2, 2, 2, 1087, 873, 3, 2, 2, 2, 1087, 880, 3, 2, 2, 2, 1087, 887, 3, 2, 2, 2, 1087, 894, 3, 2, 2, 2, 1087, 903, 3, 2, 2, 2, 1087, 913, 3, 2, 2, 2, 1087, 921, 3, 2, 2, 2, 1087, 931, 3, 2, 2, 2, 1087, 941, 3, 2, 2, 2, 1087, 950, 3, 2, 2, 2, 1087, 956, 3, 2, 2, 2, 1087, 966, 3, 2, 2, 2, 1087, 973, 3, 2, 2, 2, 1087, 981, 3, 2, 2, 2, 1087, 992, 3, 2, 2, 2, 1087, 1004, 3, 2, 2, 2, 1087, 1010, 3, 2, 2, 2, 1087, 1017, 3, 2, 2, 2, 1087, 1027, 3, 2, 2, 2, 1087, 1034, 3, 2, 2, 2, 1087, 1045, 3, 2, 2, 2, 1087, 1053, 3, 2, 2, 2, 1087, 1059, 3, 2, 2, 2, 1087, 1070, 3, 2, 2, 2, 1088, 140, 3, 2, 2, 2, 1089, 1090, 5, 203, 100, 2, 1090, 1091, 5, 245, 121, 2, 1091, 1092, 5, 215, 106, 2, 1092, 1171, 3, 2, 2, 2, 1093, 1094, 5, 227, 112, 2, 1094, 1095, 5, 219, 108, 2, 1095, 1096, 5, 229, 113, 2, 1096, 1171, 3, 2, 2, 2, 1097, 1098, 5, 227, 112, 2, 1098, 1099, 5, 203, 100, 2, 1099, 1100, 5, 249, 123, 2, 1100, 1171, 3, 2, 2, 2, 1101, 1102, 5, 239, 118, 2, 1102, 1103, 5, 243, 120, 2, 1103, 1104, 5, 227, 112, 2, 1104, 1171, 3, 2, 2, 2, 1105, 1106, 5, 207, 102, 2, 1106, 1107, 5, 231, 114, 2, 1107, 1108, 5, 243, 120, 2, 1108, 1109, 5, 229, 113, 2, 1109, 1110, 5, 241, 119, 2, 1110, 1171, 3, 2, 2, 2, 1111, 1112, 5, 207, 102, 2, 1112, 1113, 5, 231, 114, 2, 1113, 1114, 5, 243, 120, 2, 1114, 1115, 5, 229, 113, 2, 1115, 1116, 5, 241, 119, 2, 1116, 1117, 5, 111, 54, 2, 1117, 1118, 5, 209, 103, 2, 1118, 1119, 5, 219, 108, 2, 1119, 1120, 5, 239, 118, 2, 1120, 1121, 5, 241, 119, 2, 1121, 1122, 5, 219, 108, 2, 1122, 1123, 5, 229, 113, 2, 1123, 1124, 5, 207, 102, 2, 1124, 1125, 5, 241, 119, 2, 1125, 1171, 3, 2, 2, 2, 1126, 1127, 5, 233, 115, 2, 1127, 1128, 5, 211, 104, 2, 1128, 1129, 5, 237, 117, 2, 1129, 1130, 5, 207, 102, 2, 1130, 1131, 5, 211, 104, 2, 1131, 1132, 5, 229, 113, 2, 1132, 1133, 5, 241, 119, 2, 1133, 1134, 5, 219, 108, 2, 1134, 1135, 5, 225, 111, 2, 1135, 1136, 5, 211, 104, 2, 1136, 1171, 3, 2, 2, 2, 1137, 1138, 5, 227, 112, 2, 1138, 1139, 5, 211, 104, 2, 1139, 1140, 5, 209, 103, 2, 1140, 1141, 5, 219, 108, 2, 1141, 1142, 5, 203, 100, 2, 1142, 1143, 5, 229, 113, 2, 1143, 1171, 3, 2, 2, 2, 1144, 1145, 5, 227, 112, 2, 1145, 1146, 5, 211, 104, 2, 1146, 1147, 5, 209, 103, 2, 1147, 1148, 5, 219, 108, 2, 1148, 1149, 5, 203, 100, 2, 1149, 1150, 5, 229, 113, 2, 1150, 1151, 5, 111, 54, 2, 1151, 1152, 5, 203, 100, 2, 1152, 1153, 5, 205, 101, 2, 1153, 1154, 5, 239, 118, 2, 1154, 1155, 5, 231, 114, 2, 1155, 1156, 5, 225, 111, 2, 1156, 1157, 5, 243, 120, 2, 1157, 1158, 5, 241, 119, 2, 1158, 1159, 5, 211, 104, 2, 1159, 1160, 5, 111, 54, 2, 1160, 1161, 5, 209, 103, 2, 1161, 1162, 5, 211, 104, 2, 1162, 1163, 5, 245, 121, 2, 1163, 1164, 5, 219, 108, 2, 1164, 1165, 5, 203, 100, 2, 1165, 1166, 5, 241, 119, 2, 1166, 1167, 5, 219, 108, 2, 1167, 1168, 5, 231, 114, 2, 1168, 1169, 5, 229, 113, 2, 1169, 1171, 3, 2, 2, 2, 1170, 1089, 3, 2, 2, 2, 1170, 1093, 3, 2, 2, 2, 1170, 1097, 3, 2, 2, 2, 1170, 1101, 3, 2, 2, 2, 1170, 1105, 3, 2, 2, 2, 1170, 1111, 3, 2, 2, 2, 1170, 1126, 3, 2, 2, 2, 1170, 1137, 3, 2, 2, 2, 1170, 1144, 3, 2, 2, 2, 1171, 142, 3, 2, 2, 2, 1172, 1173, 5, 207, 102, 2, 1173, 1174, 5, 219, 108, 2, 1174, 1175, 5, 209, 103, 2, 1175, 1176, 5, 237, 117, 2, 1176, 1177, 5, 111, 54, 2, 1177, 1178, 5, 227, 112, 2, 1178, 1179, 5, 203, 100, 2, 1179, 1180, 5, 241, 119, 2, 1180, 1181, 5, 207, 102, 2, 1181, 1182, 5, 217, 107, 2, 1182, 144, 3, 2, 2, 2, 1183, 1190, 5, 61, 29, 2, 1184, 1189, 5, 61, 29, 2, 1185, 1189, 5, 59, 28, 2, 1186, 1189, 7, 97, 2, 2, 1187, 1189, 5, 125, 61, 2, 1188, 1184, 3, 2, 2, 2, 1188, 1185, 3, 2, 2, 2, 1188, 1186, 3, 2, 2, 2, 1188, 1187, 3, 2, 2, 2, 1189, 1192, 3, 2, 2, 2, 1190, 1188, 3, 2, 2, 2, 1190, 1191, 3, 2, 2, 2, 1191, 1203, 3, 2, 2, 2, 1192, 1190, 3, 2, 2, 2, 1193, 1198, 9, 10, 2, 2, 1194, 1199, 5, 61, 29, 2, 1195, 1199, 5, 59, 28, 2, 1196, 1199, 7, 97, 2, 2, 1197, 1199, 5, 125, 61, 2, 1198, 1194, 3, 2, 2, 2, 1198, 1195, 3, 2, 2, 2, 1198, 1196, 3, 2, 2, 2, 1198, 1197, 3, 2, 2, 2, 1199, 1200, 3, 2, 2, 2, 1200, 1198, 3, 2, 2, 2, 1200, 1201, 3, 2, 2, 2, 1201, 1203, 3, 2, 2, 2, 1202, 1183, 3, 2, 2, 2, 1202, 1193, 3, 2, 2, 2, 1203, 146, 3, 2, 2, 2, 1204, 1210, 7, 98, 2, 2, 1205, 1209, 10, 11, 2, 2, 1206, 1207, 7, 98, 2, 2, 1207, 1209, 7, 98, 2, 2, 1208, 1205, 3, 2, 2, 2, 1208, 1206, 3, 2, 2, 2, 1209, 1212, 3, 2, 2, 2, 1210, 1208, 3, 2, 2, 2, 1210, 1211, 3, 2, 2, 2, 1211, 1213, 3, 2, 2, 2, 1212, 1210, 3, 2, 2, 2, 1213, 1214, 7, 98, 2, 2, 1214, 148, 3, 2, 2, 2, 1215, 1216, 5, 41, 19, 2, 1216, 1217, 3, 2, 2, 2, 1217, 1218, 8, 73, 6, 2, 1218, 150, 3, 2, 2, 2, 1219, 1220, 5, 43, 20, 2, 1220, 1221, 3, 2, 2, 2, 1221, 1222, 8, 74, 6, 2, 1222, 152, 3, 2, 2, 2, 1223, 1224, 5, 45, 21, 2, 1224, 1225, 3, 2, 2, 2, 1225, 1226, 8, 75, 6, 2, 1226, 154, 3, 2, 2, 2, 1227, 1228, 7, 126, 2, 2, 1228, 1229, 3, 2, 2, 2, 1229, 1230, 8, 76, 9, 2, 1230, 1231, 8, 76, 10, 2, 1231, 156, 3, 2, 2, 2, 1232, 1233, 7, 93, 2, 2, 1233, 1234, 3, 2, 2, 2, 1234, 1235, 8, 77, 7, 2, 1235, 1236, 8, 77, 4, 2, 1236, 1237, 8, 77, 4, 2, 1237, 158, 3, 2, 2, 2, 1238, 1239, 7, 95, 2, 2, 1239, 1240, 3, 2, 2, 2, 1240, 1241, 8, 78, 10, 2, 1241, 1242, 8, 78, 10, 2, 1242, 1243, 8, 78, 11, 2, 1243, 160, 3, 2, 2, 2, 1244, 1245, 7, 46, 2, 2, 1245, 1246, 3, 2, 2, 2, 1246, 1247, 8, 79, 12, 2, 1247, 162, 3, 2, 2, 2, 1248, 1249, 7, 63, 2, 2, 1249, 1250, 3, 2, 2, 2, 1250, 1251, 8, 80, 13, 2, 1251, 164, 3, 2, 2, 2, 1252, 1253, 5, 227, 112, 2, 1253, 1254, 5, 211, 104, 2, 1254, 1255, 5, 241, 119, 2, 1255, 1256, 5, 203, 100, 2, 1256, 1257, 5, 209, 103, 2, 1257, 1258, 5, 203, 100, 2, 1258, 1259, 5, 241, 119, 2, 1259, 1260, 5, 203, 100, 2, 1260, 166, 3, 2, 2, 2, 1261, 1263, 5, 169, 83, 2, 1262, 1261, 3, 2, 2, 2, 1263, 1264, 3, 2, 2, 2, 1264, 1262, 3, 2, 2, 2, 1264, 1265, 3, 2, 2, 2, 1265, 168, 3, 2, 2, 2, 1266, 1268, 10, 12, 2, 2, 1267, 1266, 3, 2, 2, 2, 1268, 1269, 3, 2, 2, 2, 1269, 1267, 3, 2, 2, 2, 1269, 1270, 3, 2, 2, 2, 1270, 1274, 3, 2, 2, 2, 1271, 1272, 7, 49, 2, 2, 1272, 1274, 10, 13, 2, 2, 1273, 1267, 3, 2, 2, 2, 1273, 1271, 3, 2, 2, 2, 1274, 170, 3, 2, 2, 2, 1275, 1276, 5, 147, 72, 2, 1276, 172, 3, 2, 2, 2, 1277, 1278, 5, 41, 19, 2, 1278, 1279, 3, 2, 2, 2, 1279, 1280, 8, 85, 6, 2, 1280, 174, 3, 2, 2, 2, 1281, 1282, 5, 43, 20, 2, 1282, 1283, 3, 2, 2, 2, 1283, 1284, 8, 86, 6, 2, 1284, 176, 3, 2, 2, 2, 1285, 1286, 5, 45, 21, 2, 1286, 1287, 3, 2, 2, 2, 1287, 1288, 8, 87, 6, 2, 1288, 178, 3, 2, 2, 2, 1289, 1290, 5, 231, 114, 2, 1290, 1291, 5, 229, 113, 2, 1291, 180, 3, 2, 2, 2, 1292, 1293, 5, 247, 122, 2, 1293, 1294, 5, 219, 108, 2, 1294, 1295, 5, 241, 119, 2, 1295, 1296, 5, 217, 107, 2, 1296, 182, 3, 2, 2, 2, 1297, 1298, 7, 126, 2, 2, 1298, 1299, 3, 2, 2, 2, 1299, 1300, 8, 90, 9, 2, 1300, 1301, 8, 90, 10, 2, 1301, 184, 3, 2, 2, 2, 1302, 1303, 7, 95, 2, 2, 1303, 1304, 3, 2, 2, 2, 1304, 1305, 8, 91, 10, 2, 1305, 1306, 8, 91, 10, 2, 1306, 1307, 8, 91, 11, 2, 1307, 186, 3, 2, 2, 2, 1308, 1309, 7, 46, 2, 2, 1309, 1310, 3, 2, 2, 2, 1310, 1311, 8, 92, 12, 2, 1311, 188, 3, 2, 2, 2, 1312, 1313, 7, 63, 2, 2, 1313, 1314, 3, 2, 2, 2, 1314, 1315, 8, 93, 13, 2, 1315, 190, 3, 2, 2, 2, 1316, 1318, 5, 193, 95, 2, 1317, 1316, 3, 2, 2, 2, 1318, 1319, 3, 2, 2, 2, 1319, 1317, 3, 2, 2, 2, 1319, 1320, 3, 2, 2, 2, 1320, 192, 3, 2, 2, 2, 1321, 1323, 10, 12, 2, 2, 1322, 1321, 3, 2, 2, 2, 1323, 1324, 3, 2, 2, 2, 1324, 1322, 3, 2, 2, 2, 1324, 1325, 3, 2, 2, 2, 1325, 1329, 3, 2, 2, 2, 1326, 1327, 7, 49, 2, 2, 1327, 1329, 10, 13, 2, 2, 1328, 1322, 3, 2, 2, 2, 1328, 1326, 3, 2, 2, 2, 1329, 194, 3, 2, 2, 2, 1330, 1331, 5, 147, 72, 2, 1331, 196, 3, 2, 2, 2, 1332, 1333, 5, 41, 19, 2, 1333, 1334, 3, 2, 2, 2, 1334, 1335, 8, 97, 6, 2, 1335, 198, 3, 2, 2, 2, 1336, 1337, 5, 43, 20, 2, 1337, 1338, 3, 2, 2, 2, 1338, 1339, 8, 98, 6, 2, 1339, 200, 3, 2, 2, 2, 1340, 1341, 5, 45, 21, 2, 1341, 1342, 3, 2, 2, 2, 1342, 1343, 8, 99, 6, 2, 1343, 202, 3, 2, 2, 2, 1344, 1345, 9, 14, 2, 2, 1345, 204, 3, 2, 2, 2, 1346, 1347, 9, 15, 2, 2, 1347, 206, 3, 2, 2, 2, 1348, 1349, 9, 16, 2, 2, 1349, 208, 3, 2, 2, 2, 1350, 1351, 9, 17, 2, 2, 1351, 210, 3, 2, 2, 2, 1352, 1353, 9, 8, 2, 2, 1353, 212, 3, 2, 2, 2, 1354, 1355, 9, 18, 2, 2, 1355, 214, 3, 2, 2, 2, 1356, 1357, 9, 19, 2, 2, 1357, 216, 3, 2, 2, 2, 1358, 1359, 9, 20, 2, 2, 1359, 218, 3, 2, 2, 2, 1360, 1361, 9, 21, 2, 2, 1361, 220, 3, 2, 2, 2, 1362, 1363, 9, 22, 2, 2, 1363, 222, 3, 2, 2, 2, 1364, 1365, 9, 23, 2, 2, 1365, 224, 3, 2, 2, 2, 1366, 1367, 9, 24, 2, 2, 1367, 226, 3, 2, 2, 2, 1368, 1369, 9, 25, 2, 2, 1369, 228, 3, 2, 2, 2, 1370, 1371, 9, 26, 2, 2, 1371, 230, 3, 2, 2, 2, 1372, 1373, 9, 27, 2, 2, 1373, 232, 3, 2, 2, 2, 1374, 1375, 9, 28, 2, 2, 1375, 234, 3, 2, 2, 2, 1376, 1377, 9, 29, 2, 2, 1377, 236, 3, 2, 2, 2, 1378, 1379, 9, 30, 2, 2, 1379, 238, 3, 2, 2, 2, 1380, 1381, 9, 31, 2, 2, 1381, 240, 3, 2, 2, 2, 1382, 1383, 9, 32, 2, 2, 1383, 242, 3, 2, 2, 2, 1384, 1385, 9, 33, 2, 2, 1385, 244, 3, 2, 2, 2, 1386, 1387, 9, 34, 2, 2, 1387, 246, 3, 2, 2, 2, 1388, 1389, 9, 35, 2, 2, 1389, 248, 3, 2, 2, 2, 1390, 1391, 9, 36, 2, 2, 1391, 250, 3, 2, 2, 2, 1392, 1393, 9, 37, 2, 2, 1393, 252, 3, 2, 2, 2, 1394, 1395, 9, 38, 2, 2, 1395, 254, 3, 2, 2, 2, 50, 2, 3, 4, 5, 6, 400, 404, 407, 416, 418, 429, 470, 475, 480, 482, 493, 501, 504, 506, 511, 516, 522, 529, 534, 540, 543, 551, 555, 588, 672, 684, 706, 723, 1087, 1170, 1188, 1190, 1198, 1200, 1202, 1208, 1210, 1264, 1269, 1273, 1319, 1324, 1328, 14, 7, 4, 2, 7, 3, 2, 7, 5, 2, 7, 6, 2, 2, 3, 2, 9, 37, 2, 7, 2, 2, 9, 26, 2, 6, 2, 2, 9, 38, 2, 9, 34, 2, 9, 33, 2] \ No newline at end of file diff --git a/packages/kbn-monaco/src/esql/antlr/esql_lexer.tokens b/packages/kbn-monaco/src/esql/antlr/esql_lexer.tokens index c2dafff2f222c..b72e97b9a2961 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_lexer.tokens +++ b/packages/kbn-monaco/src/esql/antlr/esql_lexer.tokens @@ -1,74 +1,98 @@ -EVAL=1 -EXPLAIN=2 -FROM=3 -ROW=4 -STATS=5 -WHERE=6 -SORT=7 -LIMIT=8 -PROJECT=9 -LINE_COMMENT=10 -MULTILINE_COMMENT=11 -WS=12 -PIPE=13 -STRING=14 -INTEGER_LITERAL=15 -DECIMAL_LITERAL=16 -BY=17 -AND=18 -ASSIGN=19 -COMMA=20 -DOT=21 -LP=22 -OPENING_BRACKET=23 -CLOSING_BRACKET=24 -NOT=25 -NULL=26 -OR=27 -RP=28 -BOOLEAN_VALUE=29 -COMPARISON_OPERATOR=30 -PLUS=31 -MINUS=32 -ASTERISK=33 -SLASH=34 -PERCENT=35 -ORDERING=36 -NULLS_ORDERING=37 -NULLS_ORDERING_DIRECTION=38 -UNARY_FUNCTION=39 -UNQUOTED_IDENTIFIER=40 -QUOTED_IDENTIFIER=41 -EXPR_LINE_COMMENT=42 -EXPR_MULTILINE_COMMENT=43 -EXPR_WS=44 -SRC_UNQUOTED_IDENTIFIER=45 -SRC_QUOTED_IDENTIFIER=46 -SRC_LINE_COMMENT=47 -SRC_MULTILINE_COMMENT=48 -SRC_WS=49 -'eval'=1 -'explain'=2 -'from'=3 -'row'=4 -'stats'=5 -'where'=6 -'sort'=7 -'limit'=8 -'project'=9 -'by'=17 -'and'=18 -'.'=21 -'('=22 -'['=23 -']'=24 -'not'=25 -'null'=26 -'or'=27 -')'=28 -'+'=31 -'-'=32 -'*'=33 -'/'=34 -'%'=35 -'nulls'=37 +DISSECT=1 +GROK=2 +EVAL=3 +EXPLAIN=4 +FROM=5 +ROW=6 +STATS=7 +WHERE=8 +SORT=9 +MV_EXPAND=10 +LIMIT=11 +PROJECT=12 +DROP=13 +RENAME=14 +SHOW=15 +ENRICH=16 +KEEP=17 +LINE_COMMENT=18 +MULTILINE_COMMENT=19 +WS=20 +EXPLAIN_WS=21 +EXPLAIN_LINE_COMMENT=22 +EXPLAIN_MULTILINE_COMMENT=23 +PIPE=24 +STRING=25 +INTEGER_LITERAL=26 +DECIMAL_LITERAL=27 +BY=28 +DATE_LITERAL=29 +AND=30 +ASSIGN=31 +COMMA=32 +DOT=33 +LP=34 +OPENING_BRACKET=35 +CLOSING_BRACKET=36 +NOT=37 +LIKE=38 +RLIKE=39 +IN=40 +IS=41 +AS=42 +NULL=43 +OR=44 +RP=45 +UNDERSCORE=46 +INFO=47 +FUNCTIONS=48 +BOOLEAN_VALUE=49 +COMPARISON_OPERATOR=50 +PLUS=51 +MINUS=52 +ASTERISK=53 +SLASH=54 +PERCENT=55 +TEN=56 +ORDERING=57 +NULLS_ORDERING=58 +NULLS_ORDERING_DIRECTION=59 +MATH_FUNCTION=60 +UNARY_FUNCTION=61 +WHERE_FUNCTIONS=62 +UNQUOTED_IDENTIFIER=63 +QUOTED_IDENTIFIER=64 +EXPR_LINE_COMMENT=65 +EXPR_MULTILINE_COMMENT=66 +EXPR_WS=67 +METADATA=68 +SRC_UNQUOTED_IDENTIFIER=69 +SRC_QUOTED_IDENTIFIER=70 +SRC_LINE_COMMENT=71 +SRC_MULTILINE_COMMENT=72 +SRC_WS=73 +ON=74 +WITH=75 +ENR_UNQUOTED_IDENTIFIER=76 +ENR_QUOTED_IDENTIFIER=77 +ENR_LINE_COMMENT=78 +ENR_MULTILINE_COMMENT=79 +ENR_WS=80 +EXPLAIN_PIPE=81 +'by'=28 +'and'=30 +'.'=33 +'('=34 +']'=36 +'or'=44 +')'=45 +'_'=46 +'info'=47 +'functions'=48 +'+'=51 +'-'=52 +'*'=53 +'/'=54 +'%'=55 +'10'=56 +'nulls'=58 diff --git a/packages/kbn-monaco/src/esql/antlr/esql_lexer.ts b/packages/kbn-monaco/src/esql/antlr/esql_lexer.ts index 064b2fe2c02d1..6d8ddeb0f848e 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_lexer.ts +++ b/packages/kbn-monaco/src/esql/antlr/esql_lexer.ts @@ -17,57 +17,91 @@ import * as Utils from "antlr4ts/misc/Utils"; export class esql_lexer extends Lexer { - public static readonly EVAL = 1; - public static readonly EXPLAIN = 2; - public static readonly FROM = 3; - public static readonly ROW = 4; - public static readonly STATS = 5; - public static readonly WHERE = 6; - public static readonly SORT = 7; - public static readonly LIMIT = 8; - public static readonly PROJECT = 9; - public static readonly LINE_COMMENT = 10; - public static readonly MULTILINE_COMMENT = 11; - public static readonly WS = 12; - public static readonly PIPE = 13; - public static readonly STRING = 14; - public static readonly INTEGER_LITERAL = 15; - public static readonly DECIMAL_LITERAL = 16; - public static readonly BY = 17; - public static readonly AND = 18; - public static readonly ASSIGN = 19; - public static readonly COMMA = 20; - public static readonly DOT = 21; - public static readonly LP = 22; - public static readonly OPENING_BRACKET = 23; - public static readonly CLOSING_BRACKET = 24; - public static readonly NOT = 25; - public static readonly NULL = 26; - public static readonly OR = 27; - public static readonly RP = 28; - public static readonly BOOLEAN_VALUE = 29; - public static readonly COMPARISON_OPERATOR = 30; - public static readonly PLUS = 31; - public static readonly MINUS = 32; - public static readonly ASTERISK = 33; - public static readonly SLASH = 34; - public static readonly PERCENT = 35; - public static readonly ORDERING = 36; - public static readonly NULLS_ORDERING = 37; - public static readonly NULLS_ORDERING_DIRECTION = 38; - public static readonly UNARY_FUNCTION = 39; - public static readonly UNQUOTED_IDENTIFIER = 40; - public static readonly QUOTED_IDENTIFIER = 41; - public static readonly EXPR_LINE_COMMENT = 42; - public static readonly EXPR_MULTILINE_COMMENT = 43; - public static readonly EXPR_WS = 44; - public static readonly SRC_UNQUOTED_IDENTIFIER = 45; - public static readonly SRC_QUOTED_IDENTIFIER = 46; - public static readonly SRC_LINE_COMMENT = 47; - public static readonly SRC_MULTILINE_COMMENT = 48; - public static readonly SRC_WS = 49; - public static readonly EXPRESSION = 1; - public static readonly SOURCE_IDENTIFIERS = 2; + public static readonly DISSECT = 1; + public static readonly GROK = 2; + public static readonly EVAL = 3; + public static readonly EXPLAIN = 4; + public static readonly FROM = 5; + public static readonly ROW = 6; + public static readonly STATS = 7; + public static readonly WHERE = 8; + public static readonly SORT = 9; + public static readonly MV_EXPAND = 10; + public static readonly LIMIT = 11; + public static readonly PROJECT = 12; + public static readonly DROP = 13; + public static readonly RENAME = 14; + public static readonly SHOW = 15; + public static readonly ENRICH = 16; + public static readonly KEEP = 17; + public static readonly LINE_COMMENT = 18; + public static readonly MULTILINE_COMMENT = 19; + public static readonly WS = 20; + public static readonly EXPLAIN_WS = 21; + public static readonly EXPLAIN_LINE_COMMENT = 22; + public static readonly EXPLAIN_MULTILINE_COMMENT = 23; + public static readonly PIPE = 24; + public static readonly STRING = 25; + public static readonly INTEGER_LITERAL = 26; + public static readonly DECIMAL_LITERAL = 27; + public static readonly BY = 28; + public static readonly DATE_LITERAL = 29; + public static readonly AND = 30; + public static readonly ASSIGN = 31; + public static readonly COMMA = 32; + public static readonly DOT = 33; + public static readonly LP = 34; + public static readonly OPENING_BRACKET = 35; + public static readonly CLOSING_BRACKET = 36; + public static readonly NOT = 37; + public static readonly LIKE = 38; + public static readonly RLIKE = 39; + public static readonly IN = 40; + public static readonly IS = 41; + public static readonly AS = 42; + public static readonly NULL = 43; + public static readonly OR = 44; + public static readonly RP = 45; + public static readonly UNDERSCORE = 46; + public static readonly INFO = 47; + public static readonly FUNCTIONS = 48; + public static readonly BOOLEAN_VALUE = 49; + public static readonly COMPARISON_OPERATOR = 50; + public static readonly PLUS = 51; + public static readonly MINUS = 52; + public static readonly ASTERISK = 53; + public static readonly SLASH = 54; + public static readonly PERCENT = 55; + public static readonly TEN = 56; + public static readonly ORDERING = 57; + public static readonly NULLS_ORDERING = 58; + public static readonly NULLS_ORDERING_DIRECTION = 59; + public static readonly MATH_FUNCTION = 60; + public static readonly UNARY_FUNCTION = 61; + public static readonly WHERE_FUNCTIONS = 62; + public static readonly UNQUOTED_IDENTIFIER = 63; + public static readonly QUOTED_IDENTIFIER = 64; + public static readonly EXPR_LINE_COMMENT = 65; + public static readonly EXPR_MULTILINE_COMMENT = 66; + public static readonly EXPR_WS = 67; + public static readonly METADATA = 68; + public static readonly SRC_UNQUOTED_IDENTIFIER = 69; + public static readonly SRC_QUOTED_IDENTIFIER = 70; + public static readonly SRC_LINE_COMMENT = 71; + public static readonly SRC_MULTILINE_COMMENT = 72; + public static readonly SRC_WS = 73; + public static readonly ON = 74; + public static readonly WITH = 75; + public static readonly ENR_UNQUOTED_IDENTIFIER = 76; + public static readonly ENR_QUOTED_IDENTIFIER = 77; + public static readonly ENR_LINE_COMMENT = 78; + public static readonly ENR_MULTILINE_COMMENT = 79; + public static readonly ENR_WS = 80; + public static readonly EXPLAIN_PIPE = 81; + public static readonly EXPLAIN_MODE = 1; + public static readonly EXPRESSION = 2; + public static readonly SOURCE_IDENTIFIERS = 3; + public static readonly ENRICH_IDENTIFIERS = 4; // tslint:disable:no-trailing-whitespace public static readonly channelNames: string[] = [ @@ -76,40 +110,58 @@ export class esql_lexer extends Lexer { // tslint:disable:no-trailing-whitespace public static readonly modeNames: string[] = [ - "DEFAULT_MODE", "EXPRESSION", "SOURCE_IDENTIFIERS", + "DEFAULT_MODE", "EXPLAIN_MODE", "EXPRESSION", "SOURCE_IDENTIFIERS", "ENRICH_IDENTIFIERS", ]; public static readonly ruleNames: string[] = [ - "EVAL", "EXPLAIN", "FROM", "ROW", "STATS", "WHERE", "SORT", "LIMIT", "PROJECT", - "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "PIPE", "DIGIT", "LETTER", - "ESCAPE_SEQUENCE", "UNESCAPED_CHARS", "EXPONENT", "STRING", "INTEGER_LITERAL", - "DECIMAL_LITERAL", "BY", "AND", "ASSIGN", "COMMA", "DOT", "LP", "OPENING_BRACKET", - "CLOSING_BRACKET", "NOT", "NULL", "OR", "RP", "BOOLEAN_VALUE", "COMPARISON_OPERATOR", - "PLUS", "MINUS", "ASTERISK", "SLASH", "PERCENT", "ORDERING", "NULLS_ORDERING", - "NULLS_ORDERING_DIRECTION", "UNARY_FUNCTION", "UNQUOTED_IDENTIFIER", "QUOTED_IDENTIFIER", - "EXPR_LINE_COMMENT", "EXPR_MULTILINE_COMMENT", "EXPR_WS", "SRC_PIPE", - "SRC_CLOSING_BRACKET", "SRC_COMMA", "SRC_ASSIGN", "SRC_UNQUOTED_IDENTIFIER", - "SRC_UNQUOTED_IDENTIFIER_PART", "SRC_QUOTED_IDENTIFIER", "SRC_LINE_COMMENT", - "SRC_MULTILINE_COMMENT", "SRC_WS", + "DISSECT", "GROK", "EVAL", "EXPLAIN", "FROM", "ROW", "STATS", "WHERE", + "SORT", "MV_EXPAND", "LIMIT", "PROJECT", "DROP", "RENAME", "SHOW", "ENRICH", + "KEEP", "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "EXPLAIN_OPENING_BRACKET", + "EXPLAIN_PIPE", "EXPLAIN_WS", "EXPLAIN_LINE_COMMENT", "EXPLAIN_MULTILINE_COMMENT", + "PIPE", "DIGIT", "LETTER", "ESCAPE_SEQUENCE", "UNESCAPED_CHARS", "EXPONENT", + "STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "DATE_LITERAL", + "AND", "ASSIGN", "COMMA", "DOT", "LP", "OPENING_BRACKET", "CLOSING_BRACKET", + "NOT", "LIKE", "RLIKE", "IN", "IS", "AS", "NULL", "OR", "RP", "UNDERSCORE", + "INFO", "FUNCTIONS", "BOOLEAN_VALUE", "COMPARISON_OPERATOR", "PLUS", "MINUS", + "ASTERISK", "SLASH", "PERCENT", "TEN", "ORDERING", "NULLS_ORDERING", "NULLS_ORDERING_DIRECTION", + "MATH_FUNCTION", "UNARY_FUNCTION", "WHERE_FUNCTIONS", "UNQUOTED_IDENTIFIER", + "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", "EXPR_MULTILINE_COMMENT", "EXPR_WS", + "SRC_PIPE", "SRC_OPENING_BRACKET", "SRC_CLOSING_BRACKET", "SRC_COMMA", + "SRC_ASSIGN", "METADATA", "SRC_UNQUOTED_IDENTIFIER", "SRC_UNQUOTED_IDENTIFIER_PART", + "SRC_QUOTED_IDENTIFIER", "SRC_LINE_COMMENT", "SRC_MULTILINE_COMMENT", + "SRC_WS", "ON", "WITH", "ENR_PIPE", "ENR_CLOSING_BRACKET", "ENR_COMMA", + "ENR_ASSIGN", "ENR_UNQUOTED_IDENTIFIER", "ENR_UNQUOTED_IDENTIFIER_PART", + "ENR_QUOTED_IDENTIFIER", "ENR_LINE_COMMENT", "ENR_MULTILINE_COMMENT", + "ENR_WS", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", + "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", ]; private static readonly _LITERAL_NAMES: Array = [ - undefined, "'eval'", "'explain'", "'from'", "'row'", "'stats'", "'where'", - "'sort'", "'limit'", "'project'", undefined, undefined, undefined, undefined, - undefined, undefined, undefined, "'by'", "'and'", undefined, undefined, - "'.'", "'('", "'['", "']'", "'not'", "'null'", "'or'", "')'", undefined, - undefined, "'+'", "'-'", "'*'", "'/'", "'%'", undefined, "'nulls'", + undefined, undefined, undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined, undefined, undefined, + "'by'", undefined, "'and'", undefined, undefined, "'.'", "'('", undefined, + "']'", undefined, undefined, undefined, undefined, undefined, undefined, + undefined, "'or'", "')'", "'_'", "'info'", "'functions'", undefined, undefined, + "'+'", "'-'", "'*'", "'/'", "'%'", "'10'", undefined, "'nulls'", ]; private static readonly _SYMBOLIC_NAMES: Array = [ - undefined, "EVAL", "EXPLAIN", "FROM", "ROW", "STATS", "WHERE", "SORT", - "LIMIT", "PROJECT", "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "PIPE", - "STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "AND", "ASSIGN", - "COMMA", "DOT", "LP", "OPENING_BRACKET", "CLOSING_BRACKET", "NOT", "NULL", - "OR", "RP", "BOOLEAN_VALUE", "COMPARISON_OPERATOR", "PLUS", "MINUS", "ASTERISK", - "SLASH", "PERCENT", "ORDERING", "NULLS_ORDERING", "NULLS_ORDERING_DIRECTION", - "UNARY_FUNCTION", "UNQUOTED_IDENTIFIER", "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", - "EXPR_MULTILINE_COMMENT", "EXPR_WS", "SRC_UNQUOTED_IDENTIFIER", "SRC_QUOTED_IDENTIFIER", - "SRC_LINE_COMMENT", "SRC_MULTILINE_COMMENT", "SRC_WS", + undefined, "DISSECT", "GROK", "EVAL", "EXPLAIN", "FROM", "ROW", "STATS", + "WHERE", "SORT", "MV_EXPAND", "LIMIT", "PROJECT", "DROP", "RENAME", "SHOW", + "ENRICH", "KEEP", "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "EXPLAIN_WS", + "EXPLAIN_LINE_COMMENT", "EXPLAIN_MULTILINE_COMMENT", "PIPE", "STRING", + "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "DATE_LITERAL", "AND", "ASSIGN", + "COMMA", "DOT", "LP", "OPENING_BRACKET", "CLOSING_BRACKET", "NOT", "LIKE", + "RLIKE", "IN", "IS", "AS", "NULL", "OR", "RP", "UNDERSCORE", "INFO", "FUNCTIONS", + "BOOLEAN_VALUE", "COMPARISON_OPERATOR", "PLUS", "MINUS", "ASTERISK", "SLASH", + "PERCENT", "TEN", "ORDERING", "NULLS_ORDERING", "NULLS_ORDERING_DIRECTION", + "MATH_FUNCTION", "UNARY_FUNCTION", "WHERE_FUNCTIONS", "UNQUOTED_IDENTIFIER", + "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", "EXPR_MULTILINE_COMMENT", "EXPR_WS", + "METADATA", "SRC_UNQUOTED_IDENTIFIER", "SRC_QUOTED_IDENTIFIER", "SRC_LINE_COMMENT", + "SRC_MULTILINE_COMMENT", "SRC_WS", "ON", "WITH", "ENR_UNQUOTED_IDENTIFIER", + "ENR_QUOTED_IDENTIFIER", "ENR_LINE_COMMENT", "ENR_MULTILINE_COMMENT", + "ENR_WS", "EXPLAIN_PIPE", ]; public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(esql_lexer._LITERAL_NAMES, esql_lexer._SYMBOLIC_NAMES, []); @@ -141,266 +193,682 @@ export class esql_lexer extends Lexer { // @Override public get modeNames(): string[] { return esql_lexer.modeNames; } - public static readonly _serializedATN: string = - "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x023\u0215\b\x01" + - "\b\x01\b\x01\x04\x02\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04" + - "\x06\t\x06\x04\x07\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f" + - "\t\f\x04\r\t\r\x04\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11" + - "\x04\x12\t\x12\x04\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04\x16\t\x16" + - "\x04\x17\t\x17\x04\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B" + - "\x04\x1C\t\x1C\x04\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04 \t \x04!" + - "\t!\x04\"\t\"\x04#\t#\x04$\t$\x04%\t%\x04&\t&\x04\'\t\'\x04(\t(\x04)\t" + - ")\x04*\t*\x04+\t+\x04,\t,\x04-\t-\x04.\t.\x04/\t/\x040\t0\x041\t1\x04" + - "2\t2\x043\t3\x044\t4\x045\t5\x046\t6\x047\t7\x048\t8\x049\t9\x04:\t:\x04" + - ";\t;\x04<\t<\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03" + - "\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" + - "\x03\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x05\x03" + - "\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x06\x03\x06\x03\x06\x03\x06\x03" + - "\x06\x03\x06\x03\x06\x03\x06\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03" + - "\x07\x03\x07\x03\x07\x03\b\x03\b\x03\b\x03\b\x03\b\x03\b\x03\b\x03\t\x03" + - "\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03\n\x03\n\x03\n\x03\n\x03\n\x03" + - "\n\x03\n\x03\n\x03\n\x03\n\x03\v\x03\v\x03\v\x03\v\x07\v\xC7\n\v\f\v\x0E" + - "\v\xCA\v\v\x03\v\x05\v\xCD\n\v\x03\v\x05\v\xD0\n\v\x03\v\x03\v\x03\f\x03" + - "\f\x03\f\x03\f\x03\f\x07\f\xD9\n\f\f\f\x0E\f\xDC\v\f\x03\f\x03\f\x03\f" + - "\x03\f\x03\f\x03\r\x06\r\xE4\n\r\r\r\x0E\r\xE5\x03\r\x03\r\x03\x0E\x03" + - "\x0E\x03\x0E\x03\x0E\x03\x0F\x03\x0F\x03\x10\x03\x10\x03\x11\x03\x11\x03" + - "\x11\x03\x12\x03\x12\x03\x13\x03\x13\x05\x13\xF9\n\x13\x03\x13\x06\x13" + - "\xFC\n\x13\r\x13\x0E\x13\xFD\x03\x14\x03\x14\x03\x14\x07\x14\u0103\n\x14" + - "\f\x14\x0E\x14\u0106\v\x14\x03\x14\x03\x14\x03\x14\x03\x14\x03\x14\x03" + - "\x14\x07\x14\u010E\n\x14\f\x14\x0E\x14\u0111\v\x14\x03\x14\x03\x14\x03" + - "\x14\x03\x14\x03\x14\x05\x14\u0118\n\x14\x03\x14\x05\x14\u011B\n\x14\x05" + - "\x14\u011D\n\x14\x03\x15\x06\x15\u0120\n\x15\r\x15\x0E\x15\u0121\x03\x16" + - "\x06\x16\u0125\n\x16\r\x16\x0E\x16\u0126\x03\x16\x03\x16\x07\x16\u012B" + - "\n\x16\f\x16\x0E\x16\u012E\v\x16\x03\x16\x03\x16\x06\x16\u0132\n\x16\r" + - "\x16\x0E\x16\u0133\x03\x16\x06\x16\u0137\n\x16\r\x16\x0E\x16\u0138\x03" + - "\x16\x03\x16\x07\x16\u013D\n\x16\f\x16\x0E\x16\u0140\v\x16\x05\x16\u0142" + - "\n\x16\x03\x16\x03\x16\x03\x16\x03\x16\x06\x16\u0148\n\x16\r\x16\x0E\x16" + - "\u0149\x03\x16\x03\x16\x05\x16\u014E\n\x16\x03\x17\x03\x17\x03\x17\x03" + - "\x18\x03\x18\x03\x18\x03\x18\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1B\x03" + - "\x1B\x03\x1C\x03\x1C\x03\x1D\x03\x1D\x03\x1D\x03\x1D\x03\x1E\x03\x1E\x03" + - "\x1E\x03\x1E\x03\x1E\x03\x1F\x03\x1F\x03\x1F\x03\x1F\x03 \x03 \x03 \x03" + - " \x03 \x03!\x03!\x03!\x03\"\x03\"\x03#\x03#\x03#\x03#\x03#\x03#\x03#\x03" + - "#\x03#\x05#\u017F\n#\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03" + - "$\x05$\u018B\n$\x03%\x03%\x03&\x03&\x03\'\x03\'\x03(\x03(\x03)\x03)\x03" + - "*\x03*\x03*\x03*\x03*\x03*\x03*\x05*\u019E\n*\x03+\x03+\x03+\x03+\x03" + - "+\x03+\x03,\x03,\x03,\x03,\x03,\x03,\x03,\x03,\x03,\x05,\u01AF\n,\x03" + - "-\x03-\x03-\x03-\x03-\x03-\x03-\x03-\x03-\x03-\x03-\x03-\x03-\x03-\x03" + - "-\x03-\x03-\x05-\u01C2\n-\x03.\x03.\x05.\u01C6\n.\x03.\x03.\x03.\x07." + - "\u01CB\n.\f.\x0E.\u01CE\v.\x03/\x03/\x03/\x03/\x07/\u01D4\n/\f/\x0E/\u01D7" + - "\v/\x03/\x03/\x030\x030\x030\x030\x031\x031\x031\x031\x032\x032\x032\x03" + - "2\x033\x033\x033\x033\x033\x034\x034\x034\x034\x034\x034\x035\x035\x03" + - "5\x035\x036\x036\x036\x036\x037\x067\u01FB\n7\r7\x0E7\u01FC\x038\x068" + - "\u0200\n8\r8\x0E8\u0201\x038\x038\x058\u0206\n8\x039\x039\x03:\x03:\x03" + - ":\x03:\x03;\x03;\x03;\x03;\x03<\x03<\x03<\x03<\x04\xDA\u010F\x02\x02=" + - "\x05\x02\x03\x07\x02\x04\t\x02\x05\v\x02\x06\r\x02\x07\x0F\x02\b\x11\x02" + - "\t\x13\x02\n\x15\x02\v\x17\x02\f\x19\x02\r\x1B\x02\x0E\x1D\x02\x0F\x1F" + - "\x02\x02!\x02\x02#\x02\x02%\x02\x02\'\x02\x02)\x02\x10+\x02\x11-\x02\x12" + - "/\x02\x131\x02\x143\x02\x155\x02\x167\x02\x179\x02\x18;\x02\x19=\x02\x1A" + - "?\x02\x1BA\x02\x1CC\x02\x1DE\x02\x1EG\x02\x1FI\x02 K\x02!M\x02\"O\x02" + - "#Q\x02$S\x02%U\x02&W\x02\'Y\x02([\x02)]\x02*_\x02+a\x02,c\x02-e\x02.g" + - "\x02\x02i\x02\x02k\x02\x02m\x02\x02o\x02/q\x02\x02s\x020u\x021w\x022y" + - "\x023\x05\x02\x03\x04\r\x04\x02\f\f\x0F\x0F\x05\x02\v\f\x0F\x0F\"\"\x03" + - "\x022;\x04\x02C\\c|\x07\x02$$^^ppttvv\x06\x02\f\f\x0F\x0F$$^^\x04\x02" + - "GGgg\x04\x02--//\x03\x02bb\f\x02\v\f\x0F\x0F\"\"..11??]]__bb~~\x04\x02" + - ",,11\x02\u023A\x02\x05\x03\x02\x02\x02\x02\x07\x03\x02\x02\x02\x02\t\x03" + - "\x02\x02\x02\x02\v\x03\x02\x02\x02\x02\r\x03\x02\x02\x02\x02\x0F\x03\x02" + - "\x02\x02\x02\x11\x03\x02\x02\x02\x02\x13\x03\x02\x02\x02\x02\x15\x03\x02" + - "\x02\x02\x02\x17\x03\x02\x02\x02\x02\x19\x03\x02\x02\x02\x02\x1B\x03\x02" + - "\x02\x02\x03\x1D\x03\x02\x02\x02\x03)\x03\x02\x02\x02\x03+\x03\x02\x02" + - "\x02\x03-\x03\x02\x02\x02\x03/\x03\x02\x02\x02\x031\x03\x02\x02\x02\x03" + - "3\x03\x02\x02\x02\x035\x03\x02\x02\x02\x037\x03\x02\x02\x02\x039\x03\x02" + - "\x02\x02\x03;\x03\x02\x02\x02\x03=\x03\x02\x02\x02\x03?\x03\x02\x02\x02" + - "\x03A\x03\x02\x02\x02\x03C\x03\x02\x02\x02\x03E\x03\x02\x02\x02\x03G\x03" + - "\x02\x02\x02\x03I\x03\x02\x02\x02\x03K\x03\x02\x02\x02\x03M\x03\x02\x02" + - "\x02\x03O\x03\x02\x02\x02\x03Q\x03\x02\x02\x02\x03S\x03\x02\x02\x02\x03" + - "U\x03\x02\x02\x02\x03W\x03\x02\x02\x02\x03Y\x03\x02\x02\x02\x03[\x03\x02" + - "\x02\x02\x03]\x03\x02\x02\x02\x03_\x03\x02\x02\x02\x03a\x03\x02\x02\x02" + - "\x03c\x03\x02\x02\x02\x03e\x03\x02\x02\x02\x04g\x03\x02\x02\x02\x04i\x03" + - "\x02\x02\x02\x04k\x03\x02\x02\x02\x04m\x03\x02\x02\x02\x04o\x03\x02\x02" + - "\x02\x04s\x03\x02\x02\x02\x04u\x03\x02\x02\x02\x04w\x03\x02\x02\x02\x04" + - "y\x03\x02\x02\x02\x05{\x03\x02\x02\x02\x07\x82\x03\x02\x02\x02\t\x8C\x03" + - "\x02\x02\x02\v\x93\x03\x02\x02\x02\r\x99\x03\x02\x02\x02\x0F\xA1\x03\x02" + - "\x02\x02\x11\xA9\x03\x02\x02\x02\x13\xB0\x03\x02\x02\x02\x15\xB8\x03\x02" + - "\x02\x02\x17\xC2\x03\x02\x02\x02\x19\xD3\x03\x02\x02\x02\x1B\xE3\x03\x02" + - "\x02\x02\x1D\xE9\x03\x02\x02\x02\x1F\xED\x03\x02\x02\x02!\xEF\x03\x02" + - "\x02\x02#\xF1\x03\x02\x02\x02%\xF4\x03\x02\x02\x02\'\xF6\x03\x02\x02\x02" + - ")\u011C\x03\x02\x02\x02+\u011F\x03\x02\x02\x02-\u014D\x03\x02\x02\x02" + - "/\u014F\x03\x02\x02\x021\u0152\x03\x02\x02\x023\u0156\x03\x02\x02\x02" + - "5\u0158\x03\x02\x02\x027\u015A\x03\x02\x02\x029\u015C\x03\x02\x02\x02" + - ";\u015E\x03\x02\x02\x02=\u0162\x03\x02\x02\x02?\u0167\x03\x02\x02\x02" + - "A\u016B\x03\x02\x02\x02C\u0170\x03\x02\x02\x02E\u0173\x03\x02\x02\x02" + - "G\u017E\x03\x02\x02\x02I\u018A\x03\x02\x02\x02K\u018C\x03\x02\x02\x02" + - "M\u018E\x03\x02\x02\x02O\u0190\x03\x02\x02\x02Q\u0192\x03\x02\x02\x02" + - "S\u0194\x03\x02\x02\x02U\u019D\x03\x02\x02\x02W\u019F\x03\x02\x02\x02" + - "Y\u01AE\x03\x02\x02\x02[\u01C1\x03\x02\x02\x02]\u01C5\x03\x02\x02\x02" + - "_\u01CF\x03\x02\x02\x02a\u01DA\x03\x02\x02\x02c\u01DE\x03\x02\x02\x02" + - "e\u01E2\x03\x02\x02\x02g\u01E6\x03\x02\x02\x02i\u01EB\x03\x02\x02\x02" + - "k\u01F1\x03\x02\x02\x02m\u01F5\x03\x02\x02\x02o\u01FA\x03\x02\x02\x02" + - "q\u0205\x03\x02\x02\x02s\u0207\x03\x02\x02\x02u\u0209\x03\x02\x02\x02" + - "w\u020D\x03\x02\x02\x02y\u0211\x03\x02\x02\x02{|\x07g\x02\x02|}\x07x\x02" + - "\x02}~\x07c\x02\x02~\x7F\x07n\x02\x02\x7F\x80\x03\x02\x02\x02\x80\x81" + - "\b\x02\x02\x02\x81\x06\x03\x02\x02\x02\x82\x83\x07g\x02\x02\x83\x84\x07" + - "z\x02\x02\x84\x85\x07r\x02\x02\x85\x86\x07n\x02\x02\x86\x87\x07c\x02\x02" + - "\x87\x88\x07k\x02\x02\x88\x89\x07p\x02\x02\x89\x8A\x03\x02\x02\x02\x8A" + - "\x8B\b\x03\x02\x02\x8B\b\x03\x02\x02\x02\x8C\x8D\x07h\x02\x02\x8D\x8E" + - "\x07t\x02\x02\x8E\x8F\x07q\x02\x02\x8F\x90\x07o\x02\x02\x90\x91\x03\x02" + - "\x02\x02\x91\x92\b\x04\x03\x02\x92\n\x03\x02\x02\x02\x93\x94\x07t\x02" + - "\x02\x94\x95\x07q\x02\x02\x95\x96\x07y\x02\x02\x96\x97\x03\x02\x02\x02" + - "\x97\x98\b\x05\x02\x02\x98\f\x03\x02\x02\x02\x99\x9A\x07u\x02\x02\x9A" + - "\x9B\x07v\x02\x02\x9B\x9C\x07c\x02\x02\x9C\x9D\x07v\x02\x02\x9D\x9E\x07" + - "u\x02\x02\x9E\x9F\x03\x02\x02\x02\x9F\xA0\b\x06\x02\x02\xA0\x0E\x03\x02" + - "\x02\x02\xA1\xA2\x07y\x02\x02\xA2\xA3\x07j\x02\x02\xA3\xA4\x07g\x02\x02" + - "\xA4\xA5\x07t\x02\x02\xA5\xA6\x07g\x02\x02\xA6\xA7\x03\x02\x02\x02\xA7" + - "\xA8\b\x07\x02\x02\xA8\x10\x03\x02\x02\x02\xA9\xAA\x07u\x02\x02\xAA\xAB" + - "\x07q\x02\x02\xAB\xAC\x07t\x02\x02\xAC\xAD\x07v\x02\x02\xAD\xAE\x03\x02" + - "\x02\x02\xAE\xAF\b\b\x02\x02\xAF\x12\x03\x02\x02\x02\xB0\xB1\x07n\x02" + - "\x02\xB1\xB2\x07k\x02\x02\xB2\xB3\x07o\x02\x02\xB3\xB4\x07k\x02\x02\xB4" + - "\xB5\x07v\x02\x02\xB5\xB6\x03\x02\x02\x02\xB6\xB7\b\t\x02\x02\xB7\x14" + - "\x03\x02\x02\x02\xB8\xB9\x07r\x02\x02\xB9\xBA\x07t\x02\x02\xBA\xBB\x07" + - "q\x02\x02\xBB\xBC\x07l\x02\x02\xBC\xBD\x07g\x02\x02\xBD\xBE\x07e\x02\x02" + - "\xBE\xBF\x07v\x02\x02\xBF\xC0\x03\x02\x02\x02\xC0\xC1\b\n\x03\x02\xC1" + - "\x16\x03\x02\x02\x02\xC2\xC3\x071\x02\x02\xC3\xC4\x071\x02\x02\xC4\xC8" + - "\x03\x02\x02\x02\xC5\xC7\n\x02\x02\x02\xC6\xC5\x03\x02\x02\x02\xC7\xCA" + - "\x03\x02\x02\x02\xC8\xC6\x03\x02\x02\x02\xC8\xC9\x03\x02\x02\x02\xC9\xCC" + - "\x03\x02\x02\x02\xCA\xC8\x03\x02\x02\x02\xCB\xCD\x07\x0F\x02\x02\xCC\xCB" + - "\x03\x02\x02\x02\xCC\xCD\x03\x02\x02\x02\xCD\xCF\x03\x02\x02\x02\xCE\xD0" + - "\x07\f\x02\x02\xCF\xCE\x03\x02\x02\x02\xCF\xD0\x03\x02\x02\x02\xD0\xD1" + - "\x03\x02\x02\x02\xD1\xD2\b\v\x04\x02\xD2\x18\x03\x02\x02\x02\xD3\xD4\x07" + - "1\x02\x02\xD4\xD5\x07,\x02\x02\xD5\xDA\x03\x02\x02\x02\xD6\xD9\x05\x19" + - "\f\x02\xD7\xD9\v\x02\x02\x02\xD8\xD6\x03\x02\x02\x02\xD8\xD7\x03\x02\x02" + - "\x02\xD9\xDC\x03\x02\x02\x02\xDA\xDB\x03\x02\x02\x02\xDA\xD8\x03\x02\x02" + - "\x02\xDB\xDD\x03\x02\x02\x02\xDC\xDA\x03\x02\x02\x02\xDD\xDE\x07,\x02" + - "\x02\xDE\xDF\x071\x02\x02\xDF\xE0\x03\x02\x02\x02\xE0\xE1\b\f\x04\x02" + - "\xE1\x1A\x03\x02\x02\x02\xE2\xE4\t\x03\x02\x02\xE3\xE2\x03\x02\x02\x02" + - "\xE4\xE5\x03\x02\x02\x02\xE5\xE3\x03\x02\x02\x02\xE5\xE6\x03\x02\x02\x02" + - "\xE6\xE7\x03\x02\x02\x02\xE7\xE8\b\r\x04\x02\xE8\x1C\x03\x02\x02\x02\xE9" + - "\xEA\x07~\x02\x02\xEA\xEB\x03\x02\x02\x02\xEB\xEC\b\x0E\x05\x02\xEC\x1E" + - "\x03\x02\x02\x02\xED\xEE\t\x04\x02\x02\xEE \x03\x02\x02\x02\xEF\xF0\t" + - "\x05\x02\x02\xF0\"\x03\x02\x02\x02\xF1\xF2\x07^\x02\x02\xF2\xF3\t\x06" + - "\x02\x02\xF3$\x03\x02\x02\x02\xF4\xF5\n\x07\x02\x02\xF5&\x03\x02\x02\x02" + - "\xF6\xF8\t\b\x02\x02\xF7\xF9\t\t\x02\x02\xF8\xF7\x03\x02\x02\x02\xF8\xF9" + - "\x03\x02\x02\x02\xF9\xFB\x03\x02\x02\x02\xFA\xFC\x05\x1F\x0F\x02\xFB\xFA" + - "\x03\x02\x02\x02\xFC\xFD\x03\x02\x02\x02\xFD\xFB\x03\x02\x02\x02\xFD\xFE" + - "\x03\x02\x02\x02\xFE(\x03\x02\x02\x02\xFF\u0104\x07$\x02\x02\u0100\u0103" + - "\x05#\x11\x02\u0101\u0103\x05%\x12\x02\u0102\u0100\x03\x02\x02\x02\u0102" + - "\u0101\x03\x02\x02\x02\u0103\u0106\x03\x02\x02\x02\u0104\u0102\x03\x02" + - "\x02\x02\u0104\u0105\x03\x02\x02\x02\u0105\u0107\x03\x02\x02\x02\u0106" + - "\u0104\x03\x02\x02\x02\u0107\u011D\x07$\x02\x02\u0108\u0109\x07$\x02\x02" + - "\u0109\u010A\x07$\x02\x02\u010A\u010B\x07$\x02\x02\u010B\u010F\x03\x02" + - "\x02\x02\u010C\u010E\n\x02\x02\x02\u010D\u010C\x03\x02\x02\x02\u010E\u0111" + - "\x03\x02\x02\x02\u010F\u0110\x03\x02\x02\x02\u010F\u010D\x03\x02\x02\x02" + - "\u0110\u0112\x03\x02\x02\x02\u0111\u010F\x03\x02\x02\x02\u0112\u0113\x07" + - "$\x02\x02\u0113\u0114\x07$\x02\x02\u0114\u0115\x07$\x02\x02\u0115\u0117" + - "\x03\x02\x02\x02\u0116\u0118\x07$\x02\x02\u0117\u0116\x03\x02\x02\x02" + - "\u0117\u0118\x03\x02\x02\x02\u0118\u011A\x03\x02\x02\x02\u0119\u011B\x07" + - "$\x02\x02\u011A\u0119\x03\x02\x02\x02\u011A\u011B\x03\x02\x02\x02\u011B" + - "\u011D\x03\x02\x02\x02\u011C\xFF\x03\x02\x02\x02\u011C\u0108\x03\x02\x02" + - "\x02\u011D*\x03\x02\x02\x02\u011E\u0120\x05\x1F\x0F\x02\u011F\u011E\x03" + - "\x02\x02\x02\u0120\u0121\x03\x02\x02\x02\u0121\u011F\x03\x02\x02\x02\u0121" + - "\u0122\x03\x02\x02\x02\u0122,\x03\x02\x02\x02\u0123\u0125\x05\x1F\x0F" + - "\x02\u0124\u0123\x03\x02\x02\x02\u0125\u0126\x03\x02\x02\x02\u0126\u0124" + - "\x03\x02\x02\x02\u0126\u0127\x03\x02\x02\x02\u0127\u0128\x03\x02\x02\x02" + - "\u0128\u012C\x057\x1B\x02\u0129\u012B\x05\x1F\x0F\x02\u012A\u0129\x03" + - "\x02\x02\x02\u012B\u012E\x03\x02\x02\x02\u012C\u012A\x03\x02\x02\x02\u012C" + - "\u012D\x03\x02\x02\x02\u012D\u014E\x03\x02\x02\x02\u012E\u012C\x03\x02" + - "\x02\x02\u012F\u0131\x057\x1B\x02\u0130\u0132\x05\x1F\x0F\x02\u0131\u0130" + - "\x03\x02\x02\x02\u0132\u0133\x03\x02\x02\x02\u0133\u0131\x03\x02\x02\x02" + - "\u0133\u0134\x03\x02\x02\x02\u0134\u014E\x03\x02\x02\x02\u0135\u0137\x05" + - "\x1F\x0F\x02\u0136\u0135\x03\x02\x02\x02\u0137\u0138\x03\x02\x02\x02\u0138" + - "\u0136\x03\x02\x02\x02\u0138\u0139\x03\x02\x02\x02\u0139\u0141\x03\x02" + - "\x02\x02\u013A\u013E\x057\x1B\x02\u013B\u013D\x05\x1F\x0F\x02\u013C\u013B" + - "\x03\x02\x02\x02\u013D\u0140\x03\x02\x02\x02\u013E\u013C\x03\x02\x02\x02" + - "\u013E\u013F\x03\x02\x02\x02\u013F\u0142\x03\x02\x02\x02\u0140\u013E\x03" + - "\x02\x02\x02\u0141\u013A\x03\x02\x02\x02\u0141\u0142\x03\x02\x02\x02\u0142" + - "\u0143\x03\x02\x02\x02\u0143\u0144\x05\'\x13\x02\u0144\u014E\x03\x02\x02" + - "\x02\u0145\u0147\x057\x1B\x02\u0146\u0148\x05\x1F\x0F\x02\u0147\u0146" + - "\x03\x02\x02\x02\u0148\u0149\x03\x02\x02\x02\u0149\u0147\x03\x02\x02\x02" + - "\u0149\u014A\x03\x02\x02\x02\u014A\u014B\x03\x02\x02\x02\u014B\u014C\x05" + - "\'\x13\x02\u014C\u014E\x03\x02\x02\x02\u014D\u0124\x03\x02\x02\x02\u014D" + - "\u012F\x03\x02\x02\x02\u014D\u0136\x03\x02\x02\x02\u014D\u0145\x03\x02" + - "\x02\x02\u014E.\x03\x02\x02\x02\u014F\u0150\x07d\x02\x02\u0150\u0151\x07" + - "{\x02\x02\u01510\x03\x02\x02\x02\u0152\u0153\x07c\x02\x02\u0153\u0154" + - "\x07p\x02\x02\u0154\u0155\x07f\x02\x02\u01552\x03\x02\x02\x02\u0156\u0157" + - "\x07?\x02\x02\u01574\x03\x02\x02\x02\u0158\u0159\x07.\x02\x02\u01596\x03" + - "\x02\x02\x02\u015A\u015B\x070\x02\x02\u015B8\x03\x02\x02\x02\u015C\u015D" + - "\x07*\x02\x02\u015D:\x03\x02\x02\x02\u015E\u015F\x07]\x02\x02\u015F\u0160" + - "\x03\x02\x02\x02\u0160\u0161\b\x1D\x06\x02\u0161<\x03\x02\x02\x02\u0162" + - "\u0163\x07_\x02\x02\u0163\u0164\x03\x02\x02\x02\u0164\u0165\b\x1E\x05" + - "\x02\u0165\u0166\b\x1E\x05\x02\u0166>\x03\x02\x02\x02\u0167\u0168\x07" + - "p\x02\x02\u0168\u0169\x07q\x02\x02\u0169\u016A\x07v\x02\x02\u016A@\x03" + - "\x02\x02\x02\u016B\u016C\x07p\x02\x02\u016C\u016D\x07w\x02\x02\u016D\u016E" + - "\x07n\x02\x02\u016E\u016F\x07n\x02\x02\u016FB\x03\x02\x02\x02\u0170\u0171" + - "\x07q\x02\x02\u0171\u0172\x07t\x02\x02\u0172D\x03\x02\x02\x02\u0173\u0174" + - "\x07+\x02\x02\u0174F\x03\x02\x02\x02\u0175\u0176\x07v\x02\x02\u0176\u0177" + - "\x07t\x02\x02\u0177\u0178\x07w\x02\x02\u0178\u017F\x07g\x02\x02\u0179" + - "\u017A\x07h\x02\x02\u017A\u017B\x07c\x02\x02\u017B\u017C\x07n\x02\x02" + - "\u017C\u017D\x07u\x02\x02\u017D\u017F\x07g\x02\x02\u017E\u0175\x03\x02" + - "\x02\x02\u017E\u0179\x03\x02\x02\x02\u017FH\x03\x02\x02\x02\u0180\u0181" + - "\x07?\x02\x02\u0181\u018B\x07?\x02\x02\u0182\u0183\x07#\x02\x02\u0183" + - "\u018B\x07?\x02\x02\u0184\u018B\x07>\x02\x02\u0185\u0186\x07>\x02\x02" + - "\u0186\u018B\x07?\x02\x02\u0187\u018B\x07@\x02\x02\u0188\u0189\x07@\x02" + - "\x02\u0189\u018B\x07?\x02\x02\u018A\u0180\x03\x02\x02\x02\u018A\u0182" + - "\x03\x02\x02\x02\u018A\u0184\x03\x02\x02\x02\u018A\u0185\x03\x02\x02\x02" + - "\u018A\u0187\x03\x02\x02\x02\u018A\u0188\x03\x02\x02\x02\u018BJ\x03\x02" + - "\x02\x02\u018C\u018D\x07-\x02\x02\u018DL\x03\x02\x02\x02\u018E\u018F\x07" + - "/\x02\x02\u018FN\x03\x02\x02\x02\u0190\u0191\x07,\x02\x02\u0191P\x03\x02" + - "\x02\x02\u0192\u0193\x071\x02\x02\u0193R\x03\x02\x02\x02\u0194\u0195\x07" + - "\'\x02\x02\u0195T\x03\x02\x02\x02\u0196\u0197\x07c\x02\x02\u0197\u0198" + - "\x07u\x02\x02\u0198\u019E\x07e\x02\x02\u0199\u019A\x07f\x02\x02\u019A" + - "\u019B\x07g\x02\x02\u019B\u019C\x07u\x02\x02\u019C\u019E\x07e\x02\x02" + - "\u019D\u0196\x03\x02\x02\x02\u019D\u0199\x03\x02\x02\x02\u019EV\x03\x02" + - "\x02\x02\u019F\u01A0\x07p\x02\x02\u01A0\u01A1\x07w\x02\x02\u01A1\u01A2" + - "\x07n\x02\x02\u01A2\u01A3\x07n\x02\x02\u01A3\u01A4\x07u\x02\x02\u01A4" + - "X\x03\x02\x02\x02\u01A5\u01A6\x07h\x02\x02\u01A6\u01A7\x07k\x02\x02\u01A7" + - "\u01A8\x07t\x02\x02\u01A8\u01A9\x07u\x02\x02\u01A9\u01AF\x07v\x02\x02" + - "\u01AA\u01AB\x07n\x02\x02\u01AB\u01AC\x07c\x02\x02\u01AC\u01AD\x07u\x02" + - "\x02\u01AD\u01AF\x07v\x02\x02\u01AE\u01A5\x03\x02\x02\x02\u01AE\u01AA" + - "\x03\x02\x02\x02\u01AFZ\x03\x02\x02\x02\u01B0\u01B1\x07t\x02\x02\u01B1" + - "\u01B2\x07q\x02\x02\u01B2\u01B3\x07w\x02\x02\u01B3\u01B4\x07p\x02\x02" + - "\u01B4\u01C2\x07f\x02\x02\u01B5\u01B6\x07c\x02\x02\u01B6\u01B7\x07x\x02" + - "\x02\u01B7\u01C2\x07i\x02\x02\u01B8\u01B9\x07o\x02\x02\u01B9\u01BA\x07" + - "k\x02\x02\u01BA\u01C2\x07p\x02\x02\u01BB\u01BC\x07o\x02\x02\u01BC\u01BD" + - "\x07c\x02\x02\u01BD\u01C2\x07z\x02\x02\u01BE\u01BF\x07u\x02\x02\u01BF" + - "\u01C0\x07w\x02\x02\u01C0\u01C2\x07o\x02\x02\u01C1\u01B0\x03\x02\x02\x02" + - "\u01C1\u01B5\x03\x02\x02\x02\u01C1\u01B8\x03\x02\x02\x02\u01C1\u01BB\x03" + - "\x02\x02\x02\u01C1\u01BE\x03\x02\x02\x02\u01C2\\\x03\x02\x02\x02\u01C3" + - "\u01C6\x05!\x10\x02\u01C4\u01C6\x07a\x02\x02\u01C5\u01C3\x03\x02\x02\x02" + - "\u01C5\u01C4\x03\x02\x02\x02\u01C6\u01CC\x03\x02\x02\x02\u01C7\u01CB\x05" + - "!\x10\x02\u01C8\u01CB\x05\x1F\x0F\x02\u01C9\u01CB\x07a\x02\x02\u01CA\u01C7" + - "\x03\x02\x02\x02\u01CA\u01C8\x03\x02\x02\x02\u01CA\u01C9\x03\x02\x02\x02" + - "\u01CB\u01CE\x03\x02\x02\x02\u01CC\u01CA\x03\x02\x02\x02\u01CC\u01CD\x03" + - "\x02\x02\x02\u01CD^\x03\x02\x02\x02\u01CE\u01CC\x03\x02\x02\x02\u01CF" + - "\u01D5\x07b\x02\x02\u01D0\u01D4\n\n\x02\x02\u01D1\u01D2\x07b\x02\x02\u01D2" + - "\u01D4\x07b\x02\x02\u01D3\u01D0\x03\x02\x02\x02\u01D3\u01D1\x03\x02\x02" + - "\x02\u01D4\u01D7\x03\x02\x02\x02\u01D5\u01D3\x03\x02\x02\x02\u01D5\u01D6" + - "\x03\x02\x02\x02\u01D6\u01D8\x03\x02\x02\x02\u01D7\u01D5\x03\x02\x02\x02" + - "\u01D8\u01D9\x07b\x02\x02\u01D9`\x03\x02\x02\x02\u01DA\u01DB\x05\x17\v" + - "\x02\u01DB\u01DC\x03\x02\x02\x02\u01DC\u01DD\b0\x04\x02\u01DDb\x03\x02" + - "\x02\x02\u01DE\u01DF\x05\x19\f\x02\u01DF\u01E0\x03\x02\x02\x02\u01E0\u01E1" + - "\b1\x04\x02\u01E1d\x03\x02\x02\x02\u01E2\u01E3\x05\x1B\r\x02\u01E3\u01E4" + - "\x03\x02\x02\x02\u01E4\u01E5\b2\x04\x02\u01E5f\x03\x02\x02\x02\u01E6\u01E7" + - "\x07~\x02\x02\u01E7\u01E8\x03\x02\x02\x02\u01E8\u01E9\b3\x07\x02\u01E9" + - "\u01EA\b3\x05\x02\u01EAh\x03\x02\x02\x02\u01EB\u01EC\x07_\x02\x02\u01EC" + - "\u01ED\x03\x02\x02\x02\u01ED\u01EE\b4\x05\x02\u01EE\u01EF\b4\x05\x02\u01EF" + - "\u01F0\b4\b\x02\u01F0j\x03\x02\x02\x02\u01F1\u01F2\x07.\x02\x02\u01F2" + - "\u01F3\x03\x02\x02\x02\u01F3\u01F4\b5\t\x02\u01F4l\x03\x02\x02\x02\u01F5" + - "\u01F6\x07?\x02\x02\u01F6\u01F7\x03\x02\x02\x02\u01F7\u01F8\b6\n\x02\u01F8" + - "n\x03\x02\x02\x02\u01F9\u01FB\x05q8\x02\u01FA\u01F9\x03\x02\x02\x02\u01FB" + - "\u01FC\x03\x02\x02\x02\u01FC\u01FA\x03\x02\x02\x02\u01FC\u01FD\x03\x02" + - "\x02\x02\u01FDp\x03\x02\x02\x02\u01FE\u0200\n\v\x02\x02\u01FF\u01FE\x03" + - "\x02\x02\x02\u0200\u0201\x03\x02\x02\x02\u0201\u01FF\x03\x02\x02\x02\u0201" + - "\u0202\x03\x02\x02\x02\u0202\u0206\x03\x02\x02\x02\u0203\u0204\x071\x02" + - "\x02\u0204\u0206\n\f\x02\x02\u0205\u01FF\x03\x02\x02\x02\u0205\u0203\x03" + - "\x02\x02\x02\u0206r\x03\x02\x02\x02\u0207\u0208\x05_/\x02\u0208t\x03\x02" + - "\x02\x02\u0209\u020A\x05\x17\v\x02\u020A\u020B\x03\x02\x02\x02\u020B\u020C" + - "\b:\x04\x02\u020Cv\x03\x02\x02\x02\u020D\u020E\x05\x19\f\x02\u020E\u020F" + - "\x03\x02\x02\x02\u020F\u0210\b;\x04\x02\u0210x\x03\x02\x02\x02\u0211\u0212" + - "\x05\x1B\r\x02\u0212\u0213\x03\x02\x02\x02\u0213\u0214\b<\x04\x02\u0214" + - "z\x03\x02\x02\x02)\x02\x03\x04\xC8\xCC\xCF\xD8\xDA\xE5\xF8\xFD\u0102\u0104" + - "\u010F\u0117\u011A\u011C\u0121\u0126\u012C\u0133\u0138\u013E\u0141\u0149" + - "\u014D\u017E\u018A\u019D\u01AE\u01C1\u01C5\u01CA\u01CC\u01D3\u01D5\u01FC" + - "\u0201\u0205\v\x07\x03\x02\x07\x04\x02\x02\x03\x02\x06\x02\x02\x07\x02" + - "\x02\t\x0F\x02\t\x1A\x02\t\x16\x02\t\x15\x02"; + private static readonly _serializedATNSegments: number = 3; + private static readonly _serializedATNSegment0: string = + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x02S\u0574\b\x01" + + "\b\x01\b\x01\b\x01\b\x01\x04\x02\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04" + + "\x05\t\x05\x04\x06\t\x06\x04\x07\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04" + + "\v\t\v\x04\f\t\f\x04\r\t\r\x04\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04" + + "\x11\t\x11\x04\x12\t\x12\x04\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04" + + "\x16\t\x16\x04\x17\t\x17\x04\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04" + + "\x1B\t\x1B\x04\x1C\t\x1C\x04\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04" + + " \t \x04!\t!\x04\"\t\"\x04#\t#\x04$\t$\x04%\t%\x04&\t&\x04\'\t\'\x04(" + + "\t(\x04)\t)\x04*\t*\x04+\t+\x04,\t,\x04-\t-\x04.\t.\x04/\t/\x040\t0\x04" + + "1\t1\x042\t2\x043\t3\x044\t4\x045\t5\x046\t6\x047\t7\x048\t8\x049\t9\x04" + + ":\t:\x04;\t;\x04<\t<\x04=\t=\x04>\t>\x04?\t?\x04@\t@\x04A\tA\x04B\tB\x04" + + "C\tC\x04D\tD\x04E\tE\x04F\tF\x04G\tG\x04H\tH\x04I\tI\x04J\tJ\x04K\tK\x04" + + "L\tL\x04M\tM\x04N\tN\x04O\tO\x04P\tP\x04Q\tQ\x04R\tR\x04S\tS\x04T\tT\x04" + + "U\tU\x04V\tV\x04W\tW\x04X\tX\x04Y\tY\x04Z\tZ\x04[\t[\x04\\\t\\\x04]\t" + + "]\x04^\t^\x04_\t_\x04`\t`\x04a\ta\x04b\tb\x04c\tc\x04d\td\x04e\te\x04" + + "f\tf\x04g\tg\x04h\th\x04i\ti\x04j\tj\x04k\tk\x04l\tl\x04m\tm\x04n\tn\x04" + + "o\to\x04p\tp\x04q\tq\x04r\tr\x04s\ts\x04t\tt\x04u\tu\x04v\tv\x04w\tw\x04" + + "x\tx\x04y\ty\x04z\tz\x04{\t{\x04|\t|\x04}\t}\x03\x02\x03\x02\x03\x02\x03" + + "\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x03\x03\x03\x03" + + "\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x04\x03\x04\x03\x04\x03" + + "\x04\x03\x04\x03\x04\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03" + + "\x05\x03\x05\x03\x05\x03\x05\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03" + + "\x06\x03\x06\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\b\x03" + + "\b\x03\b\x03\b\x03\b\x03\b\x03\b\x03\b\x03\t\x03\t\x03\t\x03\t\x03\t\x03" + + "\t\x03\t\x03\t\x03\n\x03\n\x03\n\x03\n\x03\n\x03\n\x03\n\x03\v\x03\v\x03" + + "\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\f\x03\f\x03" + + "\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\r\x03\r\x03\r\x03\r\x03\r\x03\r\x03" + + "\r\x03\r\x03\r\x03\r\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03" + + "\x0E\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03" + + "\x0F\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x11\x03" + + "\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03\x12\x03" + + "\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x13\x03\x13\x03\x13\x03" + + "\x13\x07\x13\u018F\n\x13\f\x13\x0E\x13\u0192\v\x13\x03\x13\x05\x13\u0195" + + "\n\x13\x03\x13\x05\x13\u0198\n\x13\x03\x13\x03\x13\x03\x14\x03\x14\x03" + + "\x14\x03\x14\x03\x14\x07\x14\u01A1\n\x14\f\x14\x0E\x14\u01A4\v\x14\x03" + + "\x14\x03\x14\x03\x14\x03\x14\x03\x14\x03\x15\x06\x15\u01AC\n\x15\r\x15" + + "\x0E\x15\u01AD\x03\x15\x03\x15\x03\x16\x03\x16\x03\x16\x03\x16\x03\x16" + + "\x03\x17\x03\x17\x03\x17\x03\x17\x03\x17\x03\x18\x03\x18\x03\x18\x03\x18" + + "\x03\x19\x03\x19\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1B" + + "\x03\x1B\x03\x1B\x03\x1B\x03\x1C\x03\x1C\x03\x1D\x03\x1D\x03\x1E\x03\x1E" + + "\x03\x1E\x03\x1F\x03\x1F\x03 \x03 \x05 \u01D7\n \x03 \x06 \u01DA\n \r" + + " \x0E \u01DB\x03!\x03!\x03!\x07!\u01E1\n!\f!\x0E!\u01E4\v!\x03!\x03!\x03" + + "!\x03!\x03!\x03!\x07!\u01EC\n!\f!\x0E!\u01EF\v!\x03!\x03!\x03!\x03!\x03" + + "!\x05!\u01F6\n!\x03!\x05!\u01F9\n!\x05!\u01FB\n!\x03\"\x06\"\u01FE\n\"" + + "\r\"\x0E\"\u01FF\x03#\x06#\u0203\n#\r#\x0E#\u0204\x03#\x03#\x07#\u0209" + + "\n#\f#\x0E#\u020C\v#\x03#\x03#\x06#\u0210\n#\r#\x0E#\u0211\x03#\x06#\u0215" + + "\n#\r#\x0E#\u0216\x03#\x03#\x07#\u021B\n#\f#\x0E#\u021E\v#\x05#\u0220" + + "\n#\x03#\x03#\x03#\x03#\x06#\u0226\n#\r#\x0E#\u0227\x03#\x03#\x05#\u022C" + + "\n#\x03$\x03$\x03$\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03" + + "%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03%\x03" + + "%\x03%\x03%\x03%\x05%\u024D\n%\x03&\x03&\x03&\x03&\x03\'\x03\'\x03(\x03" + + "(\x03)\x03)\x03*\x03*\x03+\x03+\x03+\x03+\x03+\x03,\x03,\x03,\x03,\x03" + + ",\x03-\x03-\x03-\x03-\x03.\x03.\x03.\x03.\x03.\x03/\x03/\x03/\x03/\x03" + + "/\x03/\x030\x030\x030\x031\x031\x031\x032\x032\x032\x033\x033\x033\x03" + + "3\x033\x034\x034\x034\x035\x035\x036\x036\x037\x037\x037\x037\x037\x03" + + "8\x038\x038\x038\x038\x038\x038\x038\x038\x038\x039\x039\x039\x039\x03" + + "9\x039\x039\x039\x039\x059\u02A1\n9\x03:\x03:\x03:\x03:\x03:\x03:\x03" + + ":\x03:\x03:\x03:\x05:\u02AD\n:\x03;\x03;\x03<\x03<\x03=\x03=\x03>\x03" + + ">\x03?\x03?\x03@\x03@\x03@\x03A\x03A\x03A\x03A\x03A\x03A\x03A\x05A\u02C3" + + "\nA\x03B\x03B\x03B\x03B\x03B\x03B\x03C\x03C\x03C\x03C\x03C\x03C\x03C\x03" + + "C\x03C\x05C\u02D4\nC\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03D\x03" + + "D\x03D\x03D\x05D\u0440\nD\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03" + + "E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03" + + "E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03" + + "E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03" + + "E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03" + + "E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03E\x03" + + "E\x03E\x03E\x05E\u0493\nE\x03F\x03F\x03F\x03F\x03F\x03F\x03F\x03F\x03" + + "F\x03F\x03F\x03G\x03G\x03G\x03G\x03G\x07G\u04A5\nG\fG\x0EG\u04A8\vG\x03" + + "G\x03G\x03G\x03G\x03G\x06G\u04AF\nG\rG\x0EG\u04B0\x05G\u04B3\nG\x03H\x03" + + "H\x03H\x03H\x07H\u04B9\nH\fH\x0EH\u04BC\vH\x03H\x03H\x03I\x03I\x03I\x03" + + "I\x03J\x03J\x03J\x03J\x03K\x03K\x03K\x03K\x03L\x03L\x03L\x03L\x03L\x03" + + "M\x03M\x03M\x03M\x03M\x03M\x03N\x03N\x03N\x03N\x03N\x03N\x03O\x03O\x03" + + "O\x03O\x03P\x03P\x03P\x03P\x03Q\x03Q\x03Q\x03Q\x03Q\x03Q\x03Q\x03Q\x03" + + "Q\x03R\x06R\u04EF\nR\rR\x0ER\u04F0\x03S\x06S\u04F4\nS\rS\x0ES\u04F5\x03" + + "S\x03S\x05S\u04FA\nS\x03T\x03T\x03U\x03U\x03U\x03U\x03V\x03V\x03V\x03" + + "V\x03W\x03W\x03W\x03W\x03X\x03X\x03X\x03Y\x03Y\x03Y\x03Y\x03Y\x03Z\x03" + + "Z\x03Z\x03Z\x03Z\x03[\x03[\x03[\x03[\x03[\x03[\x03\\\x03\\\x03\\\x03\\" + + "\x03]\x03]\x03]\x03]\x03^\x06^\u0526\n^\r^\x0E^\u0527\x03_\x06_\u052B" + + "\n_\r_\x0E_\u052C\x03_\x03_\x05_\u0531\n_\x03`\x03`\x03a\x03a\x03a\x03" + + "a\x03b\x03b\x03b\x03b\x03c\x03c\x03c\x03c\x03d\x03d\x03e\x03e\x03f\x03" + + "f\x03g\x03g\x03h\x03h\x03i\x03i\x03j\x03j\x03k\x03k\x03l\x03l\x03m\x03" + + "m\x03n\x03n\x03o\x03o\x03p\x03p\x03q\x03q\x03r\x03r\x03s\x03s\x03t\x03" + + "t\x03u\x03u\x03v\x03v\x03w\x03w\x03x\x03x\x03y\x03y\x03z\x03z\x03{\x03" + + "{\x03|\x03|\x03}\x03}\x04\u01A2\u01ED\x02\x02~\x07\x02\x03\t\x02\x04\v" + + "\x02\x05\r\x02\x06\x0F\x02\x07\x11\x02\b\x13\x02\t\x15\x02\n\x17\x02\v" + + "\x19\x02\f\x1B\x02\r\x1D\x02\x0E\x1F\x02\x0F!\x02\x10#\x02\x11%\x02\x12" + + "\'\x02\x13)\x02\x14+\x02\x15-\x02\x16/\x02\x021\x02S3\x02\x175\x02\x18" + + "7\x02\x199\x02\x1A;\x02\x02=\x02\x02?\x02\x02A\x02\x02C\x02\x02E\x02\x1B" + + "G\x02\x1CI\x02\x1DK\x02\x1EM\x02\x1FO\x02 Q\x02!S\x02\"U\x02#W\x02$Y\x02" + + "%[\x02&]\x02\'_\x02(a\x02)c\x02*e\x02+g\x02,i\x02-k\x02.m\x02/o\x020q" + + "\x021s\x022u\x023w\x024y\x025{\x026}\x027\x7F\x028\x81\x029\x83\x02:\x85" + + "\x02;\x87\x02<\x89\x02=\x8B\x02>\x8D\x02?\x8F\x02@\x91\x02A\x93\x02B\x95" + + "\x02C\x97\x02D\x99\x02E\x9B\x02\x02\x9D\x02\x02\x9F\x02\x02\xA1\x02\x02" + + "\xA3\x02\x02\xA5\x02F\xA7\x02G\xA9\x02\x02\xAB\x02H\xAD\x02I\xAF\x02J" + + "\xB1\x02K\xB3\x02L\xB5\x02M\xB7\x02\x02\xB9\x02\x02\xBB\x02\x02\xBD\x02" + + "\x02\xBF\x02N\xC1\x02\x02\xC3\x02O\xC5\x02P\xC7\x02Q\xC9\x02R\xCB\x02" + + "\x02\xCD\x02\x02\xCF\x02\x02\xD1\x02\x02\xD3\x02\x02\xD5\x02\x02\xD7\x02" + + "\x02\xD9\x02\x02\xDB\x02\x02\xDD\x02\x02\xDF\x02\x02\xE1\x02\x02\xE3\x02" + + "\x02\xE5\x02\x02\xE7\x02\x02\xE9\x02\x02\xEB\x02\x02\xED\x02\x02\xEF\x02" + + "\x02\xF1\x02\x02\xF3\x02\x02\xF5\x02\x02\xF7\x02\x02\xF9\x02\x02\xFB\x02" + + "\x02\xFD\x02\x02\x07\x02\x03\x04\x05\x06\'\x04\x02\f\f\x0F\x0F\x05\x02" + + "\v\f\x0F\x0F\"\"\x03\x022;\x04\x02C\\c|\x07\x02$$^^ppttvv\x06\x02\f\f" + + "\x0F\x0F$$^^\x04\x02GGgg\x04\x02--//\x04\x02BBaa\x03\x02bb\f\x02\v\f\x0F" + + "\x0F\"\"..11??]]__bb~~\x04\x02,,11\x04\x02CCcc\x04\x02DDdd\x04\x02EEe" + + "e\x04\x02FFff\x04\x02HHhh\x04\x02IIii\x04\x02JJjj\x04\x02KKkk\x04\x02" + + "LLll\x04\x02MMmm\x04\x02NNnn\x04\x02OOoo\x04\x02PPpp\x04\x02QQqq\x04\x02" + + "RRrr\x04\x02SSss\x04\x02TTtt\x04\x02UUuu\x04\x02VVvv\x04\x02WWww\x04\x02" + + "XXxx\x04\x02YYyy\x04\x02ZZzz\x04\x02[[{{\x04\x02\\\\||\x02\u05B8\x02\x07" + + "\x03\x02\x02\x02\x02\t\x03\x02\x02\x02\x02\v\x03\x02\x02\x02\x02\r\x03" + + "\x02\x02\x02\x02\x0F\x03\x02\x02\x02\x02\x11\x03\x02\x02\x02\x02\x13\x03" + + "\x02\x02\x02\x02\x15\x03\x02\x02\x02\x02\x17\x03\x02\x02\x02\x02\x19\x03" + + "\x02\x02\x02\x02\x1B\x03\x02\x02\x02\x02\x1D\x03\x02\x02\x02\x02\x1F\x03" + + "\x02\x02\x02\x02!\x03\x02\x02\x02\x02#\x03\x02\x02\x02\x02%\x03\x02\x02" + + "\x02\x02\'\x03\x02\x02\x02\x02)\x03\x02\x02\x02\x02+\x03\x02\x02\x02\x02" + + "-\x03\x02\x02\x02\x03/\x03\x02\x02\x02\x031\x03\x02\x02\x02\x033\x03\x02" + + "\x02\x02\x035\x03\x02\x02\x02\x037\x03\x02\x02\x02\x049\x03\x02\x02\x02" + + "\x04E\x03\x02\x02\x02\x04G\x03\x02\x02\x02\x04I\x03\x02\x02\x02\x04K\x03" + + "\x02\x02\x02\x04M\x03\x02\x02\x02\x04O\x03\x02\x02\x02\x04Q\x03\x02\x02" + + "\x02\x04S\x03\x02\x02\x02\x04U\x03\x02\x02\x02\x04W\x03\x02\x02\x02\x04" + + "Y\x03\x02\x02\x02\x04[\x03\x02\x02\x02\x04]\x03\x02\x02\x02\x04_\x03\x02" + + "\x02\x02\x04a\x03\x02\x02\x02\x04c\x03\x02\x02\x02\x04e\x03\x02\x02\x02" + + "\x04g\x03\x02\x02\x02\x04i\x03\x02\x02\x02\x04k\x03\x02\x02\x02\x04m\x03" + + "\x02\x02\x02\x04o\x03\x02\x02\x02\x04q\x03\x02\x02\x02\x04s\x03\x02\x02" + + "\x02\x04u\x03\x02\x02\x02\x04w\x03\x02\x02\x02\x04y\x03\x02\x02\x02\x04" + + "{\x03\x02\x02\x02\x04}\x03\x02\x02\x02\x04\x7F\x03\x02\x02\x02\x04\x81" + + "\x03\x02\x02\x02\x04\x83\x03\x02\x02\x02\x04\x85\x03\x02\x02\x02\x04\x87" + + "\x03\x02\x02\x02\x04\x89\x03\x02\x02\x02\x04\x8B\x03\x02\x02\x02\x04\x8D" + + "\x03\x02\x02\x02\x04\x8F\x03\x02\x02\x02\x04\x91\x03\x02\x02\x02\x04\x93" + + "\x03\x02\x02\x02\x04\x95\x03\x02\x02\x02\x04\x97\x03\x02\x02\x02\x04\x99" + + "\x03\x02\x02\x02\x05\x9B\x03\x02\x02\x02\x05\x9D\x03\x02\x02\x02\x05\x9F" + + "\x03\x02\x02\x02\x05\xA1\x03\x02\x02\x02\x05\xA3\x03\x02\x02\x02\x05\xA5" + + "\x03\x02\x02\x02\x05\xA7\x03\x02\x02\x02\x05\xAB\x03\x02\x02\x02\x05\xAD" + + "\x03\x02\x02\x02\x05\xAF\x03\x02\x02\x02\x05\xB1\x03\x02\x02\x02\x06\xB3" + + "\x03\x02\x02\x02\x06\xB5\x03\x02\x02\x02\x06\xB7\x03\x02\x02\x02\x06\xB9" + + "\x03\x02\x02\x02\x06\xBB\x03\x02\x02\x02\x06\xBD\x03\x02\x02\x02\x06\xBF" + + "\x03\x02\x02\x02\x06\xC3\x03\x02\x02\x02\x06\xC5\x03\x02\x02\x02\x06\xC7" + + "\x03\x02\x02\x02\x06\xC9\x03\x02\x02\x02\x07\xFF\x03\x02\x02\x02\t\u0109" + + "\x03\x02\x02\x02\v\u0110\x03\x02\x02\x02\r\u0117\x03\x02\x02\x02\x0F\u0121" + + "\x03\x02\x02\x02\x11\u0128\x03\x02\x02\x02\x13\u012E\x03\x02\x02\x02\x15" + + "\u0136\x03\x02\x02\x02\x17\u013E\x03\x02\x02\x02\x19\u0145\x03\x02\x02" + + "\x02\x1B\u0151\x03\x02\x02\x02\x1D\u0159\x03\x02\x02\x02\x1F\u0163\x03" + + "\x02\x02\x02!\u016A\x03\x02\x02\x02#\u0173\x03\x02\x02\x02%\u017A\x03" + + "\x02\x02\x02\'\u0183\x03\x02\x02\x02)\u018A\x03\x02\x02\x02+\u019B\x03" + + "\x02\x02\x02-\u01AB\x03\x02\x02\x02/\u01B1\x03\x02\x02\x021\u01B6\x03" + + "\x02\x02\x023\u01BB\x03\x02\x02\x025\u01BF\x03\x02\x02\x027\u01C3\x03" + + "\x02\x02\x029\u01C7\x03\x02\x02\x02;\u01CB\x03\x02\x02\x02=\u01CD\x03" + + "\x02\x02\x02?\u01CF\x03\x02\x02\x02A\u01D2\x03\x02\x02\x02C\u01D4\x03" + + "\x02\x02\x02E\u01FA\x03\x02\x02\x02G\u01FD\x03\x02\x02\x02I\u022B\x03" + + "\x02\x02\x02K\u022D\x03\x02\x02\x02M\u024C\x03\x02\x02\x02O\u024E\x03" + + "\x02\x02\x02Q\u0252\x03\x02\x02\x02S\u0254\x03\x02\x02\x02U\u0256\x03" + + "\x02\x02\x02W\u0258\x03\x02\x02\x02Y\u025A\x03\x02\x02\x02[\u025F\x03" + + "\x02\x02\x02]\u0264\x03\x02\x02\x02_\u0268\x03\x02\x02\x02a\u026D\x03" + + "\x02\x02\x02c\u0273\x03\x02\x02\x02e\u0276\x03\x02\x02\x02g\u0279\x03" + + "\x02\x02\x02i\u027C\x03\x02\x02\x02k\u0281\x03\x02\x02\x02m\u0284\x03" + + "\x02\x02\x02o\u0286\x03\x02\x02\x02q\u0288\x03\x02\x02\x02s\u028D\x03" + + "\x02\x02\x02u\u02A0\x03\x02\x02\x02w\u02AC\x03\x02\x02\x02y\u02AE\x03" + + "\x02\x02\x02{\u02B0\x03\x02\x02\x02}\u02B2\x03\x02\x02\x02\x7F\u02B4\x03" + + "\x02\x02\x02\x81\u02B6\x03\x02\x02\x02\x83\u02B8\x03\x02\x02\x02\x85\u02C2" + + "\x03\x02\x02\x02\x87\u02C4\x03\x02\x02\x02\x89\u02D3\x03\x02\x02\x02\x8B" + + "\u043F\x03\x02\x02\x02\x8D\u0492\x03\x02\x02\x02\x8F\u0494\x03\x02\x02" + + "\x02\x91\u04B2\x03\x02\x02\x02\x93\u04B4\x03\x02\x02\x02\x95\u04BF\x03" + + "\x02\x02\x02\x97\u04C3\x03\x02\x02\x02\x99\u04C7\x03\x02\x02\x02\x9B\u04CB" + + "\x03\x02\x02\x02\x9D\u04D0\x03\x02\x02\x02\x9F\u04D6\x03\x02\x02\x02\xA1" + + "\u04DC\x03\x02\x02\x02\xA3\u04E0\x03\x02\x02\x02\xA5\u04E4\x03\x02\x02" + + "\x02\xA7\u04EE\x03\x02\x02\x02\xA9\u04F9\x03\x02\x02\x02\xAB\u04FB\x03" + + "\x02\x02\x02\xAD\u04FD\x03\x02\x02\x02\xAF\u0501\x03\x02\x02\x02\xB1\u0505" + + "\x03\x02\x02\x02\xB3\u0509\x03\x02\x02\x02\xB5\u050C\x03\x02\x02\x02\xB7" + + "\u0511\x03\x02\x02\x02\xB9\u0516\x03\x02\x02\x02\xBB\u051C\x03\x02\x02" + + "\x02\xBD\u0520\x03\x02\x02\x02\xBF\u0525\x03\x02\x02\x02\xC1\u0530\x03" + + "\x02\x02\x02\xC3\u0532\x03\x02\x02\x02\xC5\u0534\x03\x02\x02\x02\xC7\u0538" + + "\x03\x02\x02\x02\xC9\u053C\x03\x02\x02\x02\xCB\u0540\x03\x02\x02\x02\xCD" + + "\u0542\x03\x02\x02\x02\xCF\u0544\x03\x02\x02\x02\xD1\u0546\x03\x02\x02" + + "\x02\xD3\u0548\x03\x02\x02\x02\xD5\u054A\x03\x02\x02\x02\xD7\u054C\x03" + + "\x02\x02\x02\xD9\u054E\x03\x02\x02\x02\xDB\u0550\x03\x02\x02\x02\xDD\u0552" + + "\x03\x02\x02\x02\xDF\u0554\x03\x02\x02\x02\xE1\u0556\x03\x02\x02\x02\xE3" + + "\u0558\x03\x02\x02\x02\xE5\u055A\x03\x02\x02\x02\xE7\u055C\x03\x02\x02" + + "\x02\xE9\u055E\x03\x02\x02\x02\xEB\u0560\x03\x02\x02\x02\xED\u0562\x03" + + "\x02\x02\x02\xEF\u0564\x03\x02\x02\x02\xF1\u0566\x03\x02\x02\x02\xF3\u0568" + + "\x03\x02\x02\x02\xF5\u056A\x03\x02\x02\x02\xF7\u056C\x03\x02\x02\x02\xF9" + + "\u056E\x03\x02\x02\x02\xFB\u0570\x03\x02\x02\x02\xFD\u0572\x03\x02\x02" + + "\x02\xFF\u0100\x05\xD1g\x02\u0100\u0101\x05\xDBl\x02\u0101\u0102\x05\xEF" + + "v\x02\u0102\u0103\x05\xEFv\x02\u0103\u0104\x05\xD3h\x02\u0104\u0105\x05" + + "\xCFf\x02\u0105\u0106\x05\xF1w\x02\u0106\u0107\x03\x02\x02\x02\u0107\u0108" + + "\b\x02\x02\x02\u0108\b\x03\x02\x02\x02\u0109\u010A\x05\xD7j\x02\u010A" + + "\u010B\x05\xEDu\x02\u010B\u010C\x05\xE7r\x02\u010C\u010D\x05\xDFn\x02" + + "\u010D\u010E\x03\x02\x02\x02\u010E\u010F\b\x03\x02\x02\u010F\n\x03\x02" + + "\x02\x02\u0110\u0111\x05\xD3h\x02\u0111\u0112\x05\xF5y\x02\u0112\u0113" + + "\x05\xCBd\x02\u0113\u0114\x05\xE1o\x02\u0114\u0115\x03\x02\x02\x02\u0115" + + "\u0116\b\x04\x02\x02\u0116\f\x03\x02\x02\x02\u0117\u0118\x05\xD3h\x02" + + "\u0118\u0119\x05\xF9{\x02\u0119\u011A\x05\xE9s\x02\u011A\u011B\x05\xE1" + + "o\x02\u011B\u011C\x05\xCBd\x02\u011C\u011D\x05\xDBl\x02\u011D\u011E\x05" + + "\xE5q\x02\u011E\u011F\x03\x02\x02\x02\u011F\u0120\b\x05\x03\x02\u0120" + + "\x0E\x03\x02\x02\x02\u0121\u0122\x05\xD5i\x02\u0122\u0123\x05\xEDu\x02" + + "\u0123\u0124\x05"; + private static readonly _serializedATNSegment1: string = + "\xE7r\x02\u0124\u0125\x05\xE3p\x02\u0125\u0126\x03\x02\x02\x02\u0126\u0127" + + "\b\x06\x04\x02\u0127\x10\x03\x02\x02\x02\u0128\u0129\x05\xEDu\x02\u0129" + + "\u012A\x05\xE7r\x02\u012A\u012B\x05\xF7z\x02\u012B\u012C\x03\x02\x02\x02" + + "\u012C\u012D\b\x07\x02\x02\u012D\x12\x03\x02\x02\x02\u012E\u012F\x05\xEF" + + "v\x02\u012F\u0130\x05\xF1w\x02\u0130\u0131\x05\xCBd\x02\u0131\u0132\x05" + + "\xF1w\x02\u0132\u0133\x05\xEFv\x02\u0133\u0134\x03\x02\x02\x02\u0134\u0135" + + "\b\b\x02\x02\u0135\x14\x03\x02\x02\x02\u0136\u0137\x05\xF7z\x02\u0137" + + "\u0138\x05\xD9k\x02\u0138\u0139\x05\xD3h\x02\u0139\u013A\x05\xEDu\x02" + + "\u013A\u013B\x05\xD3h\x02\u013B\u013C\x03\x02\x02\x02\u013C\u013D\b\t" + + "\x02\x02\u013D\x16\x03\x02\x02\x02\u013E\u013F\x05\xEFv\x02\u013F\u0140" + + "\x05\xE7r\x02\u0140\u0141\x05\xEDu\x02\u0141\u0142\x05\xF1w\x02\u0142" + + "\u0143\x03\x02\x02\x02\u0143\u0144\b\n\x02\x02\u0144\x18\x03\x02\x02\x02" + + "\u0145\u0146\x05\xE3p\x02\u0146\u0147\x05\xF5y\x02\u0147\u0148\x05o6\x02" + + "\u0148\u0149\x05\xD3h\x02\u0149\u014A\x05\xF9{\x02\u014A\u014B\x05\xE9" + + "s\x02\u014B\u014C\x05\xCBd\x02\u014C\u014D\x05\xE5q\x02\u014D\u014E\x05" + + "\xD1g\x02\u014E\u014F\x03\x02\x02\x02\u014F\u0150\b\v\x02\x02\u0150\x1A" + + "\x03\x02\x02\x02\u0151\u0152\x05\xE1o\x02\u0152\u0153\x05\xDBl\x02\u0153" + + "\u0154\x05\xE3p\x02\u0154\u0155\x05\xDBl\x02\u0155\u0156\x05\xF1w\x02" + + "\u0156\u0157\x03\x02\x02\x02\u0157\u0158\b\f\x02\x02\u0158\x1C\x03\x02" + + "\x02\x02\u0159\u015A\x05\xE9s\x02\u015A\u015B\x05\xEDu\x02\u015B\u015C" + + "\x05\xE7r\x02\u015C\u015D\x05\xDDm\x02\u015D\u015E\x05\xD3h\x02\u015E" + + "\u015F\x05\xCFf\x02\u015F\u0160\x05\xF1w\x02\u0160\u0161\x03\x02\x02\x02" + + "\u0161\u0162\b\r\x02\x02\u0162\x1E\x03\x02\x02\x02\u0163\u0164\x05\xD1" + + "g\x02\u0164\u0165\x05\xEDu\x02\u0165\u0166\x05\xE7r\x02\u0166\u0167\x05" + + "\xE9s\x02\u0167\u0168\x03\x02\x02\x02\u0168\u0169\b\x0E\x02\x02\u0169" + + " \x03\x02\x02\x02\u016A\u016B\x05\xEDu\x02\u016B\u016C\x05\xD3h\x02\u016C" + + "\u016D\x05\xE5q\x02\u016D\u016E\x05\xCBd\x02\u016E\u016F\x05\xE3p\x02" + + "\u016F\u0170\x05\xD3h\x02\u0170\u0171\x03\x02\x02\x02\u0171\u0172\b\x0F" + + "\x02\x02\u0172\"\x03\x02\x02\x02\u0173\u0174\x05\xEFv\x02\u0174\u0175" + + "\x05\xD9k\x02\u0175\u0176\x05\xE7r\x02\u0176\u0177\x05\xF7z\x02\u0177" + + "\u0178\x03\x02\x02\x02\u0178\u0179\b\x10\x02\x02\u0179$\x03\x02\x02\x02" + + "\u017A\u017B\x05\xD3h\x02\u017B\u017C\x05\xE5q\x02\u017C\u017D\x05\xED" + + "u\x02\u017D\u017E\x05\xDBl\x02\u017E\u017F\x05\xCFf\x02\u017F\u0180\x05" + + "\xD9k\x02\u0180\u0181\x03\x02\x02\x02\u0181\u0182\b\x11\x05\x02\u0182" + + "&\x03\x02\x02\x02\u0183\u0184\x05\xDFn\x02\u0184\u0185\x05\xD3h\x02\u0185" + + "\u0186\x05\xD3h\x02\u0186\u0187\x05\xE9s\x02\u0187\u0188\x03\x02\x02\x02" + + "\u0188\u0189\b\x12\x02\x02\u0189(\x03\x02\x02\x02\u018A\u018B\x071\x02" + + "\x02\u018B\u018C\x071\x02\x02\u018C\u0190\x03\x02\x02\x02\u018D\u018F" + + "\n\x02\x02\x02\u018E\u018D\x03\x02\x02\x02\u018F\u0192\x03\x02\x02\x02" + + "\u0190\u018E\x03\x02\x02\x02\u0190\u0191\x03\x02\x02\x02\u0191\u0194\x03" + + "\x02\x02\x02\u0192\u0190\x03\x02\x02\x02\u0193\u0195\x07\x0F\x02\x02\u0194" + + "\u0193\x03\x02\x02\x02\u0194\u0195\x03\x02\x02\x02\u0195\u0197\x03\x02" + + "\x02\x02\u0196\u0198\x07\f\x02\x02\u0197\u0196\x03\x02\x02\x02\u0197\u0198" + + "\x03\x02\x02\x02\u0198\u0199\x03\x02\x02\x02\u0199\u019A\b\x13\x06\x02" + + "\u019A*\x03\x02\x02\x02\u019B\u019C\x071\x02\x02\u019C\u019D\x07,\x02" + + "\x02\u019D\u01A2\x03\x02\x02\x02\u019E\u01A1\x05+\x14\x02\u019F\u01A1" + + "\v\x02\x02\x02\u01A0\u019E\x03\x02\x02\x02\u01A0\u019F\x03\x02\x02\x02" + + "\u01A1\u01A4\x03\x02\x02\x02\u01A2\u01A3\x03\x02\x02\x02\u01A2\u01A0\x03" + + "\x02\x02\x02\u01A3\u01A5\x03\x02\x02\x02\u01A4\u01A2\x03\x02\x02\x02\u01A5" + + "\u01A6\x07,\x02\x02\u01A6\u01A7\x071\x02\x02\u01A7\u01A8\x03\x02\x02\x02" + + "\u01A8\u01A9\b\x14\x06\x02\u01A9,\x03\x02\x02\x02\u01AA\u01AC\t\x03\x02" + + "\x02\u01AB\u01AA\x03\x02\x02\x02\u01AC\u01AD\x03\x02\x02\x02\u01AD\u01AB" + + "\x03\x02\x02\x02\u01AD\u01AE\x03\x02\x02\x02\u01AE\u01AF\x03\x02\x02\x02" + + "\u01AF\u01B0\b\x15\x06\x02\u01B0.\x03\x02\x02\x02\u01B1\u01B2\x07]\x02" + + "\x02\u01B2\u01B3\x03\x02\x02\x02\u01B3\u01B4\b\x16\x07\x02\u01B4\u01B5" + + "\b\x16\b\x02\u01B50\x03\x02\x02\x02\u01B6\u01B7\x07~\x02\x02\u01B7\u01B8" + + "\x03\x02\x02\x02\u01B8\u01B9\b\x17\t\x02\u01B9\u01BA\b\x17\n\x02\u01BA" + + "2\x03\x02\x02\x02\u01BB\u01BC\x05-\x15\x02\u01BC\u01BD\x03\x02\x02\x02" + + "\u01BD\u01BE\b\x18\x06\x02\u01BE4\x03\x02\x02\x02\u01BF\u01C0\x05)\x13" + + "\x02\u01C0\u01C1\x03\x02\x02\x02\u01C1\u01C2\b\x19\x06\x02\u01C26\x03" + + "\x02\x02\x02\u01C3\u01C4\x05+\x14\x02\u01C4\u01C5\x03\x02\x02\x02\u01C5" + + "\u01C6\b\x1A\x06\x02\u01C68\x03\x02\x02\x02\u01C7\u01C8\x07~\x02\x02\u01C8" + + "\u01C9\x03\x02\x02\x02\u01C9\u01CA\b\x1B\n\x02\u01CA:\x03\x02\x02\x02" + + "\u01CB\u01CC\t\x04\x02\x02\u01CC<\x03\x02\x02\x02\u01CD\u01CE\t\x05\x02" + + "\x02\u01CE>\x03\x02\x02\x02\u01CF\u01D0\x07^\x02\x02\u01D0\u01D1\t\x06" + + "\x02\x02\u01D1@\x03\x02\x02\x02\u01D2\u01D3\n\x07\x02\x02\u01D3B\x03\x02" + + "\x02\x02\u01D4\u01D6\t\b\x02\x02\u01D5\u01D7\t\t\x02\x02\u01D6\u01D5\x03" + + "\x02\x02\x02\u01D6\u01D7\x03\x02\x02\x02\u01D7\u01D9\x03\x02\x02\x02\u01D8" + + "\u01DA\x05;\x1C\x02\u01D9\u01D8\x03\x02\x02\x02\u01DA\u01DB\x03\x02\x02" + + "\x02\u01DB\u01D9\x03\x02\x02\x02\u01DB\u01DC\x03\x02\x02\x02\u01DCD\x03" + + "\x02\x02\x02\u01DD\u01E2\x07$\x02\x02\u01DE\u01E1\x05?\x1E\x02\u01DF\u01E1" + + "\x05A\x1F\x02\u01E0\u01DE\x03\x02\x02\x02\u01E0\u01DF\x03\x02\x02\x02" + + "\u01E1\u01E4\x03\x02\x02\x02\u01E2\u01E0\x03\x02\x02\x02\u01E2\u01E3\x03" + + "\x02\x02\x02\u01E3\u01E5\x03\x02\x02\x02\u01E4\u01E2\x03\x02\x02\x02\u01E5" + + "\u01FB\x07$\x02\x02\u01E6\u01E7\x07$\x02\x02\u01E7\u01E8\x07$\x02\x02" + + "\u01E8\u01E9\x07$\x02\x02\u01E9\u01ED\x03\x02\x02\x02\u01EA\u01EC\n\x02" + + "\x02\x02\u01EB\u01EA\x03\x02\x02\x02\u01EC\u01EF\x03\x02\x02\x02\u01ED" + + "\u01EE\x03\x02\x02\x02\u01ED\u01EB\x03\x02\x02\x02\u01EE\u01F0\x03\x02" + + "\x02\x02\u01EF\u01ED\x03\x02\x02\x02\u01F0\u01F1\x07$\x02\x02\u01F1\u01F2" + + "\x07$\x02\x02\u01F2\u01F3\x07$\x02\x02\u01F3\u01F5\x03\x02\x02\x02\u01F4" + + "\u01F6\x07$\x02\x02\u01F5\u01F4\x03\x02\x02\x02\u01F5\u01F6\x03\x02\x02" + + "\x02\u01F6\u01F8\x03\x02\x02\x02\u01F7\u01F9\x07$\x02\x02\u01F8\u01F7" + + "\x03\x02\x02\x02\u01F8\u01F9\x03\x02\x02\x02\u01F9\u01FB\x03\x02\x02\x02" + + "\u01FA\u01DD\x03\x02\x02\x02\u01FA\u01E6\x03\x02\x02\x02\u01FBF\x03\x02" + + "\x02\x02\u01FC\u01FE\x05;\x1C\x02\u01FD\u01FC\x03\x02\x02\x02\u01FE\u01FF" + + "\x03\x02\x02\x02\u01FF\u01FD\x03\x02\x02\x02\u01FF\u0200\x03\x02\x02\x02" + + "\u0200H\x03\x02\x02\x02\u0201\u0203\x05;\x1C\x02\u0202\u0201\x03\x02\x02" + + "\x02\u0203\u0204\x03\x02\x02\x02\u0204\u0202\x03\x02\x02\x02\u0204\u0205" + + "\x03\x02\x02\x02\u0205\u0206\x03\x02\x02\x02\u0206\u020A\x05U)\x02\u0207" + + "\u0209\x05;\x1C\x02\u0208\u0207\x03\x02\x02\x02\u0209\u020C\x03\x02\x02" + + "\x02\u020A\u0208\x03\x02\x02\x02\u020A\u020B\x03\x02\x02\x02\u020B\u022C" + + "\x03\x02\x02\x02\u020C\u020A\x03\x02\x02\x02\u020D\u020F\x05U)\x02\u020E" + + "\u0210\x05;\x1C\x02\u020F\u020E\x03\x02\x02\x02\u0210\u0211\x03\x02\x02" + + "\x02\u0211\u020F\x03\x02\x02\x02\u0211\u0212\x03\x02\x02\x02\u0212\u022C" + + "\x03\x02\x02\x02\u0213\u0215\x05;\x1C\x02\u0214\u0213\x03\x02\x02\x02" + + "\u0215\u0216\x03\x02\x02\x02\u0216\u0214\x03\x02\x02\x02\u0216\u0217\x03" + + "\x02\x02\x02\u0217\u021F\x03\x02\x02\x02\u0218\u021C\x05U)\x02\u0219\u021B" + + "\x05;\x1C\x02\u021A\u0219\x03\x02\x02\x02\u021B\u021E\x03\x02\x02\x02" + + "\u021C\u021A\x03\x02\x02\x02\u021C\u021D\x03\x02\x02\x02\u021D\u0220\x03" + + "\x02\x02\x02\u021E\u021C\x03\x02\x02\x02\u021F\u0218\x03\x02\x02\x02\u021F" + + "\u0220\x03\x02\x02\x02\u0220\u0221\x03\x02\x02\x02\u0221\u0222\x05C \x02" + + "\u0222\u022C\x03\x02\x02\x02\u0223\u0225\x05U)\x02\u0224\u0226\x05;\x1C" + + "\x02\u0225\u0224\x03\x02\x02\x02\u0226\u0227\x03\x02\x02\x02\u0227\u0225" + + "\x03\x02\x02\x02\u0227\u0228\x03\x02\x02\x02\u0228\u0229\x03\x02\x02\x02" + + "\u0229\u022A\x05C \x02\u022A\u022C\x03\x02\x02\x02\u022B\u0202\x03\x02" + + "\x02\x02\u022B\u020D\x03\x02\x02\x02\u022B\u0214\x03\x02\x02\x02\u022B" + + "\u0223\x03\x02\x02\x02\u022CJ\x03\x02\x02\x02\u022D\u022E\x07d\x02\x02" + + "\u022E\u022F\x07{\x02\x02\u022FL\x03\x02\x02\x02\u0230\u0231\x07{\x02" + + "\x02\u0231\u0232\x07g\x02\x02\u0232\u0233\x07c\x02\x02\u0233\u024D\x07" + + "t\x02\x02\u0234\u0235\x07o\x02\x02\u0235\u0236\x07q\x02\x02\u0236\u0237" + + "\x07p\x02\x02\u0237\u0238\x07v\x02\x02\u0238\u024D\x07j\x02\x02\u0239" + + "\u023A\x07f\x02\x02\u023A\u023B\x07c\x02\x02\u023B\u024D\x07{\x02\x02" + + "\u023C\u023D\x07u\x02\x02\u023D\u023E\x07g\x02\x02\u023E\u023F\x07e\x02" + + "\x02\u023F\u0240\x07q\x02\x02\u0240\u0241\x07p\x02\x02\u0241\u024D\x07" + + "f\x02\x02\u0242\u0243\x07o\x02\x02\u0243\u0244\x07k\x02\x02\u0244\u0245" + + "\x07p\x02\x02\u0245\u0246\x07w\x02\x02\u0246\u0247\x07v\x02\x02\u0247" + + "\u024D\x07g\x02\x02\u0248\u0249\x07j\x02\x02\u0249\u024A\x07q\x02\x02" + + "\u024A\u024B\x07w\x02\x02\u024B\u024D\x07t\x02\x02\u024C\u0230\x03\x02" + + "\x02\x02\u024C\u0234\x03\x02\x02\x02\u024C\u0239\x03\x02\x02\x02\u024C" + + "\u023C\x03\x02\x02\x02\u024C\u0242\x03\x02\x02\x02\u024C\u0248\x03\x02" + + "\x02\x02\u024DN\x03\x02\x02\x02\u024E\u024F\x07c\x02\x02\u024F\u0250\x07" + + "p\x02\x02\u0250\u0251\x07f\x02\x02\u0251P\x03\x02\x02\x02\u0252\u0253" + + "\x07?\x02\x02\u0253R\x03\x02\x02\x02\u0254\u0255\x07.\x02\x02\u0255T\x03" + + "\x02\x02\x02\u0256\u0257\x070\x02\x02\u0257V\x03\x02\x02\x02\u0258\u0259" + + "\x07*\x02\x02\u0259X\x03\x02\x02\x02\u025A\u025B\x07]\x02\x02\u025B\u025C" + + "\x03\x02\x02\x02\u025C\u025D\b+\x02\x02\u025D\u025E\b+\x02\x02\u025EZ" + + "\x03\x02\x02\x02\u025F\u0260\x07_\x02\x02\u0260\u0261\x03\x02\x02\x02" + + "\u0261\u0262\b,\n\x02\u0262\u0263\b,\n\x02\u0263\\\x03\x02\x02\x02\u0264" + + "\u0265\x05\xE5q\x02\u0265\u0266\x05\xE7r\x02\u0266\u0267\x05\xF1w\x02" + + "\u0267^\x03\x02\x02\x02\u0268\u0269\x05\xE1o\x02\u0269\u026A\x05\xDBl" + + "\x02\u026A\u026B\x05\xDFn\x02\u026B\u026C\x05\xD3h\x02\u026C`\x03\x02" + + "\x02\x02\u026D\u026E\x05\xEDu\x02\u026E\u026F\x05\xE1o\x02\u026F\u0270" + + "\x05\xDBl\x02\u0270\u0271\x05\xDFn\x02\u0271\u0272\x05\xD3h\x02\u0272" + + "b\x03\x02\x02\x02\u0273\u0274\x05\xDBl\x02\u0274\u0275\x05\xE5q\x02\u0275" + + "d\x03\x02\x02\x02\u0276\u0277\x05\xDBl\x02\u0277\u0278\x05\xEFv\x02\u0278" + + "f\x03\x02\x02\x02\u0279\u027A\x05\xCBd\x02\u027A\u027B\x05\xEFv\x02\u027B" + + "h\x03\x02\x02\x02\u027C\u027D\x05\xE5q\x02\u027D\u027E\x05\xF3x\x02\u027E" + + "\u027F\x05\xE1o\x02\u027F\u0280\x05\xE1o\x02\u0280j\x03\x02\x02\x02\u0281" + + "\u0282\x07q\x02\x02\u0282\u0283\x07t\x02\x02\u0283l\x03\x02\x02\x02\u0284" + + "\u0285\x07+\x02\x02\u0285n\x03\x02\x02\x02\u0286\u0287\x07a\x02\x02\u0287" + + "p\x03\x02\x02\x02\u0288\u0289\x07k\x02\x02\u0289\u028A\x07p\x02\x02\u028A" + + "\u028B\x07h\x02\x02\u028B\u028C\x07q\x02\x02\u028Cr\x03\x02\x02\x02\u028D" + + "\u028E\x07h\x02\x02\u028E\u028F\x07w\x02\x02\u028F\u0290\x07p\x02\x02" + + "\u0290\u0291\x07e\x02\x02\u0291\u0292\x07v\x02\x02\u0292\u0293\x07k\x02" + + "\x02\u0293\u0294\x07q\x02\x02\u0294\u0295\x07p\x02\x02\u0295\u0296\x07" + + "u\x02\x02\u0296t\x03\x02\x02\x02\u0297\u0298\x07v\x02\x02\u0298\u0299" + + "\x07t\x02\x02\u0299\u029A\x07w\x02\x02\u029A\u02A1\x07g\x02\x02\u029B" + + "\u029C\x07h\x02\x02\u029C\u029D\x07c\x02\x02\u029D\u029E\x07n\x02\x02" + + "\u029E\u029F\x07u\x02\x02\u029F\u02A1\x07g\x02\x02\u02A0\u0297\x03\x02" + + "\x02\x02\u02A0\u029B\x03\x02\x02\x02\u02A1v\x03\x02\x02\x02\u02A2\u02A3" + + "\x07?\x02\x02\u02A3\u02AD\x07?\x02\x02\u02A4\u02A5\x07#\x02\x02\u02A5" + + "\u02AD\x07?\x02\x02\u02A6\u02AD\x07>\x02\x02\u02A7\u02A8\x07>\x02\x02" + + "\u02A8\u02AD\x07?\x02\x02\u02A9\u02AD\x07@\x02\x02\u02AA\u02AB\x07@\x02" + + "\x02\u02AB\u02AD\x07?\x02\x02\u02AC\u02A2\x03\x02\x02\x02\u02AC\u02A4" + + "\x03\x02\x02\x02\u02AC\u02A6\x03\x02\x02\x02\u02AC\u02A7\x03\x02\x02\x02" + + "\u02AC\u02A9\x03\x02\x02\x02\u02AC\u02AA\x03\x02\x02\x02\u02ADx\x03\x02" + + "\x02\x02\u02AE\u02AF\x07-\x02\x02\u02AFz\x03\x02\x02\x02\u02B0\u02B1\x07" + + "/\x02\x02\u02B1|\x03\x02\x02\x02\u02B2\u02B3\x07,\x02\x02\u02B3~\x03\x02" + + "\x02\x02\u02B4\u02B5\x071\x02\x02\u02B5\x80\x03\x02\x02\x02\u02B6\u02B7" + + "\x07\'\x02\x02\u02B7\x82\x03\x02\x02\x02\u02B8\u02B9\x073\x02\x02\u02B9" + + "\u02BA\x072\x02\x02\u02BA\x84\x03\x02\x02\x02\u02BB\u02BC\x07c\x02\x02" + + "\u02BC\u02BD\x07u\x02\x02\u02BD\u02C3\x07e\x02\x02\u02BE\u02BF\x07f\x02" + + "\x02\u02BF\u02C0\x07g\x02\x02\u02C0\u02C1\x07u\x02\x02\u02C1\u02C3\x07" + + "e\x02\x02\u02C2\u02BB\x03\x02\x02\x02\u02C2\u02BE\x03\x02\x02\x02\u02C3" + + "\x86\x03\x02\x02\x02\u02C4\u02C5\x07p\x02\x02\u02C5\u02C6\x07w\x02\x02" + + "\u02C6\u02C7\x07n\x02\x02\u02C7\u02C8\x07n\x02\x02\u02C8\u02C9\x07u\x02" + + "\x02\u02C9\x88\x03\x02\x02\x02\u02CA\u02CB\x07h\x02\x02\u02CB\u02CC\x07" + + "k\x02\x02\u02CC\u02CD\x07t\x02\x02\u02CD\u02CE\x07u\x02\x02\u02CE\u02D4" + + "\x07v\x02\x02\u02CF\u02D0\x07n\x02\x02\u02D0\u02D1\x07c\x02\x02\u02D1" + + "\u02D2\x07u\x02\x02\u02D2\u02D4\x07v\x02\x02\u02D3\u02CA\x03\x02\x02\x02" + + "\u02D3\u02CF\x03\x02\x02\x02\u02D4\x8A\x03\x02\x02\x02\u02D5\u02D6\x05" + + "\xEDu\x02\u02D6\u02D7\x05\xE7r\x02\u02D7\u02D8\x05\xF3x\x02\u02D8\u02D9" + + "\x05\xE5q\x02\u02D9\u02DA\x05\xD1g\x02\u02DA\u0440\x03\x02\x02\x02\u02DB" + + "\u02DC\x05\xCBd\x02\u02DC\u02DD\x05\xCDe\x02\u02DD\u02DE\x05\xEFv\x02" + + "\u02DE\u0440\x03\x02\x02\x02\u02DF\u02E0\x05\xE9s\x02\u02E0\u02E1\x05" + + "\xE7r\x02\u02E1\u02E2\x05\xF7z\x02\u02E2\u0440\x03\x02\x02\x02\u02E3\u02E4" + + "\x05\xE1o\x02\u02E4\u02E5\x05\xE7r\x02\u02E5\u02E6\x05\xD7j\x02\u02E6" + + "\u02E7\x05\x83@\x02\u02E7\u0440\x03\x02\x02\x02\u02E8\u02E9\x05\xE9s\x02" + + "\u02E9\u02EA\x05\xDBl\x02\u02EA\u0440\x03\x02\x02\x02\u02EB\u02EC\x05" + + "\xF1w\x02\u02EC\u02ED\x05\xCBd\x02\u02ED\u02EE\x05\xF3x\x02\u02EE\u0440" + + "\x03\x02\x02\x02\u02EF\u0440\x05\xD3h\x02\u02F0\u02F1\x05\xEFv\x02\u02F1" + + "\u02F2\x05\xF3x\x02\u02F2\u02F3\x05\xCDe\x02\u02F3\u02F4\x05\xEFv\x02" + + "\u02F4\u02F5\x05\xF1w\x02\u02F5\u02F6\x05\xEDu\x02\u02F6\u02F7\x05\xDB" + + "l\x02\u02F7\u02F8\x05\xE5q\x02\u02F8\u02F9\x05\xD7j\x02\u02F9\u0440\x03" + + "\x02\x02\x02\u02FA\u02FB\x05\xF1w\x02\u02FB\u02FC\x05\xEDu\x02\u02FC\u02FD" + + "\x05\xDBl\x02\u02FD\u02FE\x05\xE3p\x02\u02FE\u0440\x03\x02\x02\x02\u02FF" + + "\u0300\x05\xCFf\x02\u0300\u0301\x05\xE7r\x02\u0301\u0302\x05\xE5q\x02" + + "\u0302\u0303\x05\xCFf\x02\u0303\u0304\x05\xCBd\x02\u0304\u0305\x05\xF1" + + "w\x02\u0305\u0440\x03\x02\x02\x02\u0306\u0307\x05\xEFv\x02\u0307\u0308" + + "\x05\xF1w\x02\u0308\u0309\x05\xCBd\x02\u0309\u030A\x05\xEDu\x02\u030A" + + "\u030B\x05\xF1w\x02\u030B\u030C\x05\xEFv\x02\u030C\u030D\x05o6\x02\u030D" + + "\u030E\x05\xF7z\x02\u030E\u030F\x05\xDBl\x02\u030F\u0310\x05\xF1w\x02" + + "\u0310\u0311\x05\xD9k\x02\u0311\u0440\x03\x02\x02\x02\u0312\u0313\x05" + + "\xD1g\x02\u0313\u0314\x05\xCBd\x02\u0314\u0315\x05\xF1w\x02\u0315\u0316" + + "\x05\xD3h\x02\u0316\u0317\x05o6\x02\u0317\u0318\x05\xD5i\x02\u0318\u0319" + + "\x05\xE7r\x02\u0319\u031A\x05\xEDu\x02\u031A\u031B\x05\xE3p\x02\u031B" + + "\u031C\x05\xCBd\x02\u031C\u031D\x05\xF1w\x02\u031D\u0440\x03\x02\x02\x02" + + "\u031E\u031F\x05\xD1g\x02\u031F\u0320\x05\xCBd\x02\u0320\u0321\x05\xF1" + + "w\x02\u0321\u0322\x05\xD3h\x02\u0322\u0323\x05o6\x02\u0323\u0324\x05\xF1" + + "w\x02\u0324\u0325\x05\xEDu\x02\u0325\u0326\x05\xF3x\x02\u0326\u0327\x05" + + "\xE5q\x02\u0327\u0328\x05\xCFf\x02\u0328\u0440\x03\x02\x02\x02\u0329\u032A" + + "\x05\xD1g\x02\u032A\u032B\x05\xCBd\x02\u032B\u032C\x05\xF1w\x02\u032C" + + "\u032D\x05\xD3h\x02\u032D\u032E\x05o6\x02\u032E\u032F\x05\xE9s\x02\u032F" + + "\u0330\x05\xCBd\x02\u0330\u0331\x05\xEDu\x02\u0331\u0332\x05\xEFv\x02" + + "\u0332\u0333\x05\xD3h\x02\u0333\u0440\x03\x02\x02\x02\u0334\u0335\x05" + + "\xCBd\x02\u0335\u0336\x05\xF3x\x02\u0336\u0337\x05\xF1w\x02\u0337\u0338" + + "\x05\xE7r\x02\u0338\u0339\x05o6\x02\u0339\u033A\x05\xCDe\x02\u033A\u033B" + + "\x05\xF3x\x02\u033B\u033C\x05\xCFf\x02\u033C\u033D\x05\xDFn\x02\u033D" + + "\u033E\x05\xD3h\x02\u033E\u033F\x05\xF1w\x02\u033F\u0440\x03\x02\x02\x02" + + "\u0340\u0341\x05\xDBl\x02\u0341\u0342\x05\xEFv\x02\u0342\u0343\x05o6\x02" + + "\u0343\u0344\x05\xD5i\x02\u0344\u0345\x05\xDBl\x02\u0345\u0346\x05\xE5" + + "q\x02\u0346\u0347\x05\xDBl\x02\u0347\u0348\x05\xF1w\x02\u0348\u0349\x05" + + "\xD3h\x02\u0349\u0440\x03\x02\x02\x02\u034A\u034B\x05\xDBl\x02\u034B\u034C" + + "\x05\xEFv\x02\u034C\u034D\x05o6\x02\u034D\u034E\x05\xDBl\x02\u034E\u034F" + + "\x05\xE5q\x02\u034F\u0350\x05\xD5i\x02\u0350\u0351\x05\xDBl\x02\u0351" + + "\u0352\x05\xE5q\x02\u0352\u0353\x05\xDBl\x02\u0353\u0354\x05\xF1w\x02" + + "\u0354\u0355\x05\xD3h\x02\u0355\u0440\x03\x02\x02\x02\u0356\u0357\x05" + + "\xCFf\x02\u0357\u0358\x05\xCBd\x02\u0358\u0359\x05\xEFv\x02\u0359\u035A" + + "\x05\xD3h\x02\u035A\u0440\x03\x02\x02\x02\u035B\u035C\x05\xE1o\x02\u035C" + + "\u035D\x05\xD3h\x02\u035D\u035E\x05\xE5q\x02\u035E\u035F\x05\xD7j\x02" + + "\u035F\u0360\x05\xF1w\x02\u0360\u0361\x05\xD9k\x02\u0361\u0440\x03\x02" + + "\x02\x02\u0362\u0363\x05\xE3p\x02\u0363\u0364\x05\xF5y\x02\u0364\u0365" + + "\x05o6\x02\u0365\u0366\x05\xE3p\x02\u0366\u0367\x05\xCBd\x02\u0367\u0368" + + "\x05\xF9{\x02\u0368\u0440\x03\x02\x02\x02\u0369\u036A\x05\xE3p\x02\u036A" + + "\u036B\x05\xF5y\x02\u036B\u036C\x05o6\x02\u036C\u036D\x05\xE3p\x02\u036D" + + "\u036E\x05\xDBl\x02\u036E\u036F\x05\xE5q\x02\u036F\u0440\x03\x02\x02\x02" + + "\u0370\u0371\x05\xE3p\x02\u0371\u0372\x05\xF5y\x02\u0372\u0373\x05o6\x02" + + "\u0373\u0374\x05\xCBd\x02\u0374\u0375\x05\xF5y\x02\u0375\u0376\x05\xD7" + + "j\x02\u0376\u0440\x03\x02\x02\x02\u0377\u0378\x05\xE3p\x02\u0378\u0379" + + "\x05\xF5y\x02\u0379\u037A\x05o6\x02\u037A\u037B\x05\xEFv\x02\u037B\u037C" + + "\x05\xF3x\x02\u037C\u037D\x05\xE3p\x02\u037D\u0440\x03\x02\x02\x02\u037E" + + "\u037F\x05\xE3p\x02\u037F\u0380\x05\xF5y\x02\u0380\u0381\x05o6\x02\u0381" + + "\u0382\x05\xCFf\x02\u0382\u0383\x05\xE7r\x02\u0383\u0384\x05\xF3x\x02" + + "\u0384\u0385\x05\xE5q\x02\u0385\u0386\x05\xF1w\x02\u0386\u0440\x03\x02" + + "\x02\x02\u0387\u0388\x05\xE3p\x02\u0388\u0389\x05\xF5y\x02\u0389\u038A" + + "\x05o6\x02\u038A\u038B\x05\xCFf\x02\u038B\u038C\x05\xE7r\x02\u038C\u038D" + + "\x05\xE5q\x02\u038D\u038E\x05\xCFf\x02\u038E\u038F\x05\xCBd\x02\u038F" + + "\u0390\x05\xF1w\x02\u0390\u0440\x03\x02\x02\x02\u0391\u0392\x05\xE3p\x02" + + "\u0392\u0393\x05\xF5y\x02\u0393\u0394\x05o6\x02\u0394\u0395\x05\xDDm\x02" + + "\u0395\u0396\x05\xE7r\x02\u0396\u0397\x05\xDBl\x02\u0397\u0398\x05\xE5" + + "q\x02\u0398\u0440\x03\x02\x02\x02\u0399\u039A\x05\xE3p\x02\u039A\u039B" + + "\x05\xF5y\x02\u039B\u039C\x05o6\x02\u039C\u039D\x05\xE3p\x02\u039D\u039E" + + "\x05\xD3h\x02\u039E\u039F\x05\xD1g\x02\u039F\u03A0\x05\xDBl\x02\u03A0" + + "\u03A1\x05\xCBd\x02\u03A1\u03A2\x05\xE5q\x02\u03A2\u0440\x03\x02\x02\x02" + + "\u03A3\u03A4\x05\xE3p\x02\u03A4\u03A5\x05\xF5y\x02\u03A5\u03A6\x05o6\x02" + + "\u03A6\u03A7\x05\xD1g\x02\u03A7\u03A8\x05\xD3h\x02\u03A8\u03A9\x05\xD1" + + "g\x02\u03A9\u03AA\x05\xF3x\x02\u03AA\u03AB\x05\xE9s\x02\u03AB\u03AC\x05" + + "\xD3h\x02\u03AC\u0440\x03\x02\x02\x02\u03AD\u03AE\x05\xE3p\x02\u03AE\u03AF" + + "\x05\xD3h\x02\u03AF\u03B0\x05\xF1w\x02\u03B0\u03B1\x05\xCBd\x02\u03B1" + + "\u03B2\x05\xD1g\x02\u03B2\u03B3\x05\xCBd\x02\u03B3\u03B4\x05\xF1w\x02" + + "\u03B4\u03B5\x05\xCBd\x02\u03B5\u0440\x03\x02\x02\x02\u03B6\u03B7\x05" + + "\xEFv\x02\u03B7\u03B8\x05\xE9s\x02\u03B8\u03B9\x05\xE1o\x02\u03B9\u03BA" + + "\x05\xDBl\x02\u03BA\u03BB\x05\xF1w\x02\u03BB\u0440\x03\x02\x02\x02\u03BC" + + "\u03BD\x05\xF1w\x02\u03BD\u03BE\x05\xE7r\x02\u03BE\u03BF\x05o6\x02\u03BF" + + "\u03C0\x05\xEFv\x02\u03C0\u03C1\x05\xF1w\x02\u03C1\u03C2\x05\xEDu\x02" + + "\u03C2\u03C3\x05\xDBl\x02\u03C3\u03C4\x05\xE5q\x02\u03C4\u03C5\x05\xD7" + + "j\x02\u03C5\u0440\x03\x02\x02\x02\u03C6\u03C7\x05\xF1w\x02\u03C7\u03C8" + + "\x05\xE7r\x02\u03C8\u03C9\x05o6\x02\u03C9\u03CA\x05\xEFv\x02\u03CA\u03CB" + + "\x05\xF1w\x02\u03CB\u03CC\x05\xEDu\x02\u03CC\u0440\x03\x02\x02\x02\u03CD" + + "\u03CE\x05\xF1w\x02\u03CE\u03CF\x05\xE7r\x02\u03CF\u03D0\x05o6\x02\u03D0" + + "\u03D1\x05\xCDe\x02\u03D1\u03D2\x05\xE7r\x02\u03D2\u03D3\x05\xE7r\x02" + + "\u03D3\u03D4\x05\xE1o\x02\u03D4\u0440\x03\x02\x02\x02\u03D5\u03D6\x05" + + "\xF1w\x02\u03D6\u03D7\x05\xE7r\x02\u03D7\u03D8\x05o6\x02\u03D8\u03D9\x05" + + "\xCDe\x02\u03D9\u03DA\x05\xE7r\x02\u03DA\u03DB\x05\xE7r\x02\u03DB\u03DC" + + "\x05\xE1o\x02\u03DC\u03DD\x05\xD3h\x02\u03DD\u03DE\x05\xCBd\x02\u03DE" + + "\u03DF\x05\xE5q\x02\u03DF\u0440\x03\x02\x02\x02\u03E0\u03E1\x05\xF1w\x02" + + "\u03E1\u03E2\x05\xE7r\x02\u03E2\u03E3\x05o6\x02\u03E3\u03E4\x05\xD1g\x02" + + "\u03E4\u03E5\x05\xCBd\x02\u03E5\u03E6\x05\xF1w\x02\u03E6\u03E7\x05\xD3" + + "h\x02\u03E7\u03E8\x05\xF1w\x02\u03E8\u03E9\x05\xDBl\x02\u03E9\u03EA\x05" + + "\xE3p\x02\u03EA\u03EB\x05\xD3h\x02\u03EB\u0440\x03\x02\x02\x02\u03EC\u03ED" + + "\x05\xF1w\x02\u03ED\u03EE\x05\xE7r\x02\u03EE\u03EF\x05o6\x02\u03EF\u03F0" + + "\x05\xD1g\x02\u03F0\u03F1\x05\xF1w\x02\u03F1\u0440\x03\x02\x02\x02\u03F2" + + "\u03F3\x05\xF1w\x02\u03F3\u03F4\x05\xE7r\x02\u03F4\u03F5\x05o6\x02\u03F5" + + "\u03F6\x05\xD1g\x02\u03F6\u03F7\x05\xCDe\x02\u03F7\u03F8\x05\xE1o\x02" + + "\u03F8\u0440\x03\x02\x02\x02\u03F9\u03FA\x05\xF1w\x02\u03FA\u03FB\x05" + + "\xE7r\x02\u03FB\u03FC\x05o6\x02\u03FC\u03FD\x05\xD1g\x02\u03FD\u03FE\x05" + + "\xE7r\x02\u03FE\u03FF\x05\xF3x\x02\u03FF\u0400\x05\xCDe\x02\u0400\u0401" + + "\x05\xE1o\x02\u0401\u0402\x05\xD3h\x02\u0402\u0440\x03\x02\x02\x02\u0403" + + "\u0404\x05\xF1w\x02\u0404\u0405\x05\xE7r\x02\u0405\u0406\x05o6\x02\u0406" + + "\u0407\x05\xDBl\x02\u0407\u0408\x05\xE5q\x02\u0408\u0409\x05\xF1w\x02" + + "\u0409\u0440\x03\x02\x02\x02\u040A\u040B\x05\xF1w\x02\u040B\u040C\x05" + + "\xE7r\x02\u040C\u040D\x05o6\x02\u040D\u040E\x05\xDBl\x02\u040E\u040F\x05" + + "\xE5q\x02\u040F\u0410\x05\xF1w\x02\u0410\u0411\x05\xD3h\x02\u0411\u0412" + + "\x05\xD7j\x02\u0412\u0413\x05\xD3h\x02\u0413\u0414\x05\xEDu\x02\u0414" + + "\u0440\x03\x02\x02\x02\u0415\u0416\x05\xF1w\x02\u0416\u0417\x05\xE7r\x02" + + "\u0417\u0418\x05o6\x02\u0418\u0419\x05\xE1o\x02\u0419\u041A\x05\xE7r\x02" + + "\u041A\u041B\x05\xE5q\x02\u041B\u041C\x05\xD7j\x02\u041C\u0440\x03\x02" + + "\x02\x02\u041D\u041E\x05\xF1w\x02\u041E\u041F\x05\xE7r\x02\u041F\u0420" + + "\x05o6\x02\u0420\u0421\x05\xDBl\x02\u0421\u0422\x05\xE9s\x02\u0422\u0440" + + "\x03\x02\x02\x02\u0423\u0424\x05\xF1w\x02\u0424\u0425\x05\xE7r\x02\u0425" + + "\u0426\x05o6\x02\u0426\u0427\x05\xF5y\x02\u0427\u0428\x05\xD3h\x02\u0428" + + "\u0429\x05\xEDu\x02\u0429\u042A\x05\xEFv\x02\u042A\u042B\x05\xDBl\x02" + + "\u042B\u042C\x05\xE7r\x02\u042C\u042D\x05\xE5q\x02\u042D\u0440\x03\x02" + + "\x02\x02\u042E\u042F\x05\xF1w\x02\u042F\u0430\x05\xE7r\x02\u0430\u0431" + + "\x05o6\x02\u0431\u0432\x05\xF3x\x02\u0432\u0433\x05\xE5q\x02\u0433\u0434" + + "\x05\xEFv\x02\u0434\u0435\x05\xDBl\x02\u0435\u0436\x05\xD7j\x02\u0436" + + "\u0437\x05\xE5q\x02\u0437\u0438\x05\xD3h\x02\u0438\u0439\x05\xD1g\x02" + + "\u0439\u043A\x05o6\x02\u043A\u043B\x05\xE1o\x02\u043B\u043C\x05\xE7r\x02" + + "\u043C\u043D\x05\xE5q\x02\u043D\u043E\x05\xD7j\x02\u043E\u0440\x03\x02" + + "\x02"; + private static readonly _serializedATNSegment2: string = + "\x02\u043F\u02D5\x03\x02\x02\x02\u043F\u02DB\x03\x02\x02\x02\u043F\u02DF" + + "\x03\x02\x02\x02\u043F\u02E3\x03\x02\x02\x02\u043F\u02E8\x03\x02\x02\x02" + + "\u043F\u02EB\x03\x02\x02\x02\u043F\u02EF\x03\x02\x02\x02\u043F\u02F0\x03" + + "\x02\x02\x02\u043F\u02FA\x03\x02\x02\x02\u043F\u02FF\x03\x02\x02\x02\u043F" + + "\u0306\x03\x02\x02\x02\u043F\u0312\x03\x02\x02\x02\u043F\u031E\x03\x02" + + "\x02\x02\u043F\u0329\x03\x02\x02\x02\u043F\u0334\x03\x02\x02\x02\u043F" + + "\u0340\x03\x02\x02\x02\u043F\u034A\x03\x02\x02\x02\u043F\u0356\x03\x02" + + "\x02\x02\u043F\u035B\x03\x02\x02\x02\u043F\u0362\x03\x02\x02\x02\u043F" + + "\u0369\x03\x02\x02\x02\u043F\u0370\x03\x02\x02\x02\u043F\u0377\x03\x02" + + "\x02\x02\u043F\u037E\x03\x02\x02\x02\u043F\u0387\x03\x02\x02\x02\u043F" + + "\u0391\x03\x02\x02\x02\u043F\u0399\x03\x02\x02\x02\u043F\u03A3\x03\x02" + + "\x02\x02\u043F\u03AD\x03\x02\x02\x02\u043F\u03B6\x03\x02\x02\x02\u043F" + + "\u03BC\x03\x02\x02\x02\u043F\u03C6\x03\x02\x02\x02\u043F\u03CD\x03\x02" + + "\x02\x02\u043F\u03D5\x03\x02\x02\x02\u043F\u03E0\x03\x02\x02\x02\u043F" + + "\u03EC\x03\x02\x02\x02\u043F\u03F2\x03\x02\x02\x02\u043F\u03F9\x03\x02" + + "\x02\x02\u043F\u0403\x03\x02\x02\x02\u043F\u040A\x03\x02\x02\x02\u043F" + + "\u0415\x03\x02\x02\x02\u043F\u041D\x03\x02\x02\x02\u043F\u0423\x03\x02" + + "\x02\x02\u043F\u042E\x03\x02\x02\x02\u0440\x8C\x03\x02\x02\x02\u0441\u0442" + + "\x05\xCBd\x02\u0442\u0443\x05\xF5y\x02\u0443\u0444\x05\xD7j\x02\u0444" + + "\u0493\x03\x02\x02\x02\u0445\u0446\x05\xE3p\x02\u0446\u0447\x05\xDBl\x02" + + "\u0447\u0448\x05\xE5q\x02\u0448\u0493\x03\x02\x02\x02\u0449\u044A\x05" + + "\xE3p\x02\u044A\u044B\x05\xCBd\x02\u044B\u044C\x05\xF9{\x02\u044C\u0493" + + "\x03\x02\x02\x02\u044D\u044E\x05\xEFv\x02\u044E\u044F\x05\xF3x\x02\u044F" + + "\u0450\x05\xE3p\x02\u0450\u0493\x03\x02\x02\x02\u0451\u0452\x05\xCFf\x02" + + "\u0452\u0453\x05\xE7r\x02\u0453\u0454\x05\xF3x\x02\u0454\u0455\x05\xE5" + + "q\x02\u0455\u0456\x05\xF1w\x02\u0456\u0493\x03\x02\x02\x02\u0457\u0458" + + "\x05\xCFf\x02\u0458\u0459\x05\xE7r\x02\u0459\u045A\x05\xF3x\x02\u045A" + + "\u045B\x05\xE5q\x02\u045B\u045C\x05\xF1w\x02\u045C\u045D\x05o6\x02\u045D" + + "\u045E\x05\xD1g\x02\u045E\u045F\x05\xDBl\x02\u045F\u0460\x05\xEFv\x02" + + "\u0460\u0461\x05\xF1w\x02\u0461\u0462\x05\xDBl\x02\u0462\u0463\x05\xE5" + + "q\x02\u0463\u0464\x05\xCFf\x02\u0464\u0465\x05\xF1w\x02\u0465\u0493\x03" + + "\x02\x02\x02\u0466\u0467\x05\xE9s\x02\u0467\u0468\x05\xD3h\x02\u0468\u0469" + + "\x05\xEDu\x02\u0469\u046A\x05\xCFf\x02\u046A\u046B\x05\xD3h\x02\u046B" + + "\u046C\x05\xE5q\x02\u046C\u046D\x05\xF1w\x02\u046D\u046E\x05\xDBl\x02" + + "\u046E\u046F\x05\xE1o\x02\u046F\u0470\x05\xD3h\x02\u0470\u0493\x03\x02" + + "\x02\x02\u0471\u0472\x05\xE3p\x02\u0472\u0473\x05\xD3h\x02\u0473\u0474" + + "\x05\xD1g\x02\u0474\u0475\x05\xDBl\x02\u0475\u0476\x05\xCBd\x02\u0476" + + "\u0477\x05\xE5q\x02\u0477\u0493\x03\x02\x02\x02\u0478\u0479\x05\xE3p\x02" + + "\u0479\u047A\x05\xD3h\x02\u047A\u047B\x05\xD1g\x02\u047B\u047C\x05\xDB" + + "l\x02\u047C\u047D\x05\xCBd\x02\u047D\u047E\x05\xE5q\x02\u047E\u047F\x05" + + "o6\x02\u047F\u0480\x05\xCBd\x02\u0480\u0481\x05\xCDe\x02\u0481\u0482\x05" + + "\xEFv\x02\u0482\u0483\x05\xE7r\x02\u0483\u0484\x05\xE1o\x02\u0484\u0485" + + "\x05\xF3x\x02\u0485\u0486\x05\xF1w\x02\u0486\u0487\x05\xD3h\x02\u0487" + + "\u0488\x05o6\x02\u0488\u0489\x05\xD1g\x02\u0489\u048A\x05\xD3h\x02\u048A" + + "\u048B\x05\xF5y\x02\u048B\u048C\x05\xDBl\x02\u048C\u048D\x05\xCBd\x02" + + "\u048D\u048E\x05\xF1w\x02\u048E\u048F\x05\xDBl\x02\u048F\u0490\x05\xE7" + + "r\x02\u0490\u0491\x05\xE5q\x02\u0491\u0493\x03\x02\x02\x02\u0492\u0441" + + "\x03\x02\x02\x02\u0492\u0445\x03\x02\x02\x02\u0492\u0449\x03\x02\x02\x02" + + "\u0492\u044D\x03\x02\x02\x02\u0492\u0451\x03\x02\x02\x02\u0492\u0457\x03" + + "\x02\x02\x02\u0492\u0466\x03\x02\x02\x02\u0492\u0471\x03\x02\x02\x02\u0492" + + "\u0478\x03\x02\x02\x02\u0493\x8E\x03\x02\x02\x02\u0494\u0495\x05\xCFf" + + "\x02\u0495\u0496\x05\xDBl\x02\u0496\u0497\x05\xD1g\x02\u0497\u0498\x05" + + "\xEDu\x02\u0498\u0499\x05o6\x02\u0499\u049A\x05\xE3p\x02\u049A\u049B\x05" + + "\xCBd\x02\u049B\u049C\x05\xF1w\x02\u049C\u049D\x05\xCFf\x02\u049D\u049E" + + "\x05\xD9k\x02\u049E\x90\x03\x02\x02\x02\u049F\u04A6\x05=\x1D\x02\u04A0" + + "\u04A5\x05=\x1D\x02\u04A1\u04A5\x05;\x1C\x02\u04A2\u04A5\x07a\x02\x02" + + "\u04A3\u04A5\x05}=\x02\u04A4\u04A0\x03\x02\x02\x02\u04A4\u04A1\x03\x02" + + "\x02\x02\u04A4\u04A2\x03\x02\x02\x02\u04A4\u04A3\x03\x02\x02\x02\u04A5" + + "\u04A8\x03\x02\x02\x02\u04A6\u04A4\x03\x02\x02\x02\u04A6\u04A7\x03\x02" + + "\x02\x02\u04A7\u04B3\x03\x02\x02\x02\u04A8\u04A6\x03\x02\x02\x02\u04A9" + + "\u04AE\t\n\x02\x02\u04AA\u04AF\x05=\x1D\x02\u04AB\u04AF\x05;\x1C\x02\u04AC" + + "\u04AF\x07a\x02\x02\u04AD\u04AF\x05}=\x02\u04AE\u04AA\x03\x02\x02\x02" + + "\u04AE\u04AB\x03\x02\x02\x02\u04AE\u04AC\x03\x02\x02\x02\u04AE\u04AD\x03" + + "\x02\x02\x02\u04AF\u04B0\x03\x02\x02\x02\u04B0\u04AE\x03\x02\x02\x02\u04B0" + + "\u04B1\x03\x02\x02\x02\u04B1\u04B3\x03\x02\x02\x02\u04B2\u049F\x03\x02" + + "\x02\x02\u04B2\u04A9\x03\x02\x02\x02\u04B3\x92\x03\x02\x02\x02\u04B4\u04BA" + + "\x07b\x02\x02\u04B5\u04B9\n\v\x02\x02\u04B6\u04B7\x07b\x02\x02\u04B7\u04B9" + + "\x07b\x02\x02\u04B8\u04B5\x03\x02\x02\x02\u04B8\u04B6\x03\x02\x02\x02" + + "\u04B9\u04BC\x03\x02\x02\x02\u04BA\u04B8\x03\x02\x02\x02\u04BA\u04BB\x03" + + "\x02\x02\x02\u04BB\u04BD\x03\x02\x02\x02\u04BC\u04BA\x03\x02\x02\x02\u04BD" + + "\u04BE\x07b\x02\x02\u04BE\x94\x03\x02\x02\x02\u04BF\u04C0\x05)\x13\x02" + + "\u04C0\u04C1\x03\x02\x02\x02\u04C1\u04C2\bI\x06\x02\u04C2\x96\x03\x02" + + "\x02\x02\u04C3\u04C4\x05+\x14\x02\u04C4\u04C5\x03\x02\x02\x02\u04C5\u04C6" + + "\bJ\x06\x02\u04C6\x98\x03\x02\x02\x02\u04C7\u04C8\x05-\x15\x02\u04C8\u04C9" + + "\x03\x02\x02\x02\u04C9\u04CA\bK\x06\x02\u04CA\x9A\x03\x02\x02\x02\u04CB" + + "\u04CC\x07~\x02\x02\u04CC\u04CD\x03\x02\x02\x02\u04CD\u04CE\bL\t\x02\u04CE" + + "\u04CF\bL\n\x02\u04CF\x9C\x03\x02\x02\x02\u04D0\u04D1\x07]\x02\x02\u04D1" + + "\u04D2\x03\x02\x02\x02\u04D2\u04D3\bM\x07\x02\u04D3\u04D4\bM\x04\x02\u04D4" + + "\u04D5\bM\x04\x02\u04D5\x9E\x03\x02\x02\x02\u04D6\u04D7\x07_\x02\x02\u04D7" + + "\u04D8\x03\x02\x02\x02\u04D8\u04D9\bN\n\x02\u04D9\u04DA\bN\n\x02\u04DA" + + "\u04DB\bN\v\x02\u04DB\xA0\x03\x02\x02\x02\u04DC\u04DD\x07.\x02\x02\u04DD" + + "\u04DE\x03\x02\x02\x02\u04DE\u04DF\bO\f\x02\u04DF\xA2\x03\x02\x02\x02" + + "\u04E0\u04E1\x07?\x02\x02\u04E1\u04E2\x03\x02\x02\x02\u04E2\u04E3\bP\r" + + "\x02\u04E3\xA4\x03\x02\x02\x02\u04E4\u04E5\x05\xE3p\x02\u04E5\u04E6\x05" + + "\xD3h\x02\u04E6\u04E7\x05\xF1w\x02\u04E7\u04E8\x05\xCBd\x02\u04E8\u04E9" + + "\x05\xD1g\x02\u04E9\u04EA\x05\xCBd\x02\u04EA\u04EB\x05\xF1w\x02\u04EB" + + "\u04EC\x05\xCBd\x02\u04EC\xA6\x03\x02\x02\x02\u04ED\u04EF\x05\xA9S\x02" + + "\u04EE\u04ED\x03\x02\x02\x02\u04EF\u04F0\x03\x02\x02\x02\u04F0\u04EE\x03" + + "\x02\x02\x02\u04F0\u04F1\x03\x02\x02\x02\u04F1\xA8\x03\x02\x02\x02\u04F2" + + "\u04F4\n\f\x02\x02\u04F3\u04F2\x03\x02\x02\x02\u04F4\u04F5\x03\x02\x02" + + "\x02\u04F5\u04F3\x03\x02\x02\x02\u04F5\u04F6\x03\x02\x02\x02\u04F6\u04FA" + + "\x03\x02\x02\x02\u04F7\u04F8\x071\x02\x02\u04F8\u04FA\n\r\x02\x02\u04F9" + + "\u04F3\x03\x02\x02\x02\u04F9\u04F7\x03\x02\x02\x02\u04FA\xAA\x03\x02\x02" + + "\x02\u04FB\u04FC\x05\x93H\x02\u04FC\xAC\x03\x02\x02\x02\u04FD\u04FE\x05" + + ")\x13\x02\u04FE\u04FF\x03\x02\x02\x02\u04FF\u0500\bU\x06\x02\u0500\xAE" + + "\x03\x02\x02\x02\u0501\u0502\x05+\x14\x02\u0502\u0503\x03\x02\x02\x02" + + "\u0503\u0504\bV\x06\x02\u0504\xB0\x03\x02\x02\x02\u0505\u0506\x05-\x15" + + "\x02\u0506\u0507\x03\x02\x02\x02\u0507\u0508\bW\x06\x02\u0508\xB2\x03" + + "\x02\x02\x02\u0509\u050A\x05\xE7r\x02\u050A\u050B\x05\xE5q\x02\u050B\xB4" + + "\x03\x02\x02\x02\u050C\u050D\x05\xF7z\x02\u050D\u050E\x05\xDBl\x02\u050E" + + "\u050F\x05\xF1w\x02\u050F\u0510\x05\xD9k\x02\u0510\xB6\x03\x02\x02\x02" + + "\u0511\u0512\x07~\x02\x02\u0512\u0513\x03\x02\x02\x02\u0513\u0514\bZ\t" + + "\x02\u0514\u0515\bZ\n\x02\u0515\xB8\x03\x02\x02\x02\u0516\u0517\x07_\x02" + + "\x02\u0517\u0518\x03\x02\x02\x02\u0518\u0519\b[\n\x02\u0519\u051A\b[\n" + + "\x02\u051A\u051B\b[\v\x02\u051B\xBA\x03\x02\x02\x02\u051C\u051D\x07.\x02" + + "\x02\u051D\u051E\x03\x02\x02\x02\u051E\u051F\b\\\f\x02\u051F\xBC\x03\x02" + + "\x02\x02\u0520\u0521\x07?\x02\x02\u0521\u0522\x03\x02\x02\x02\u0522\u0523" + + "\b]\r\x02\u0523\xBE\x03\x02\x02\x02\u0524\u0526\x05\xC1_\x02\u0525\u0524" + + "\x03\x02\x02\x02\u0526\u0527\x03\x02\x02\x02\u0527\u0525\x03\x02\x02\x02" + + "\u0527\u0528\x03\x02\x02\x02\u0528\xC0\x03\x02\x02\x02\u0529\u052B\n\f" + + "\x02\x02\u052A\u0529\x03\x02\x02\x02\u052B\u052C\x03\x02\x02\x02\u052C" + + "\u052A\x03\x02\x02\x02\u052C\u052D\x03\x02\x02\x02\u052D\u0531\x03\x02" + + "\x02\x02\u052E\u052F\x071\x02\x02\u052F\u0531\n\r\x02\x02\u0530\u052A" + + "\x03\x02\x02\x02\u0530\u052E\x03\x02\x02\x02\u0531\xC2\x03\x02\x02\x02" + + "\u0532\u0533\x05\x93H\x02\u0533\xC4\x03\x02\x02\x02\u0534\u0535\x05)\x13" + + "\x02\u0535\u0536\x03\x02\x02\x02\u0536\u0537\ba\x06\x02\u0537\xC6\x03" + + "\x02\x02\x02\u0538\u0539\x05+\x14\x02\u0539\u053A\x03\x02\x02\x02\u053A" + + "\u053B\bb\x06\x02\u053B\xC8\x03\x02\x02\x02\u053C\u053D\x05-\x15\x02\u053D" + + "\u053E\x03\x02\x02\x02\u053E\u053F\bc\x06\x02\u053F\xCA\x03\x02\x02\x02" + + "\u0540\u0541\t\x0E\x02\x02\u0541\xCC\x03\x02\x02\x02\u0542\u0543\t\x0F" + + "\x02\x02\u0543\xCE\x03\x02\x02\x02\u0544\u0545\t\x10\x02\x02\u0545\xD0" + + "\x03\x02\x02\x02\u0546\u0547\t\x11\x02\x02\u0547\xD2\x03\x02\x02\x02\u0548" + + "\u0549\t\b\x02\x02\u0549\xD4\x03\x02\x02\x02\u054A\u054B\t\x12\x02\x02" + + "\u054B\xD6\x03\x02\x02\x02\u054C\u054D\t\x13\x02\x02\u054D\xD8\x03\x02" + + "\x02\x02\u054E\u054F\t\x14\x02\x02\u054F\xDA\x03\x02\x02\x02\u0550\u0551" + + "\t\x15\x02\x02\u0551\xDC\x03\x02\x02\x02\u0552\u0553\t\x16\x02\x02\u0553" + + "\xDE\x03\x02\x02\x02\u0554\u0555\t\x17\x02\x02\u0555\xE0\x03\x02\x02\x02" + + "\u0556\u0557\t\x18\x02\x02\u0557\xE2\x03\x02\x02\x02\u0558\u0559\t\x19" + + "\x02\x02\u0559\xE4\x03\x02\x02\x02\u055A\u055B\t\x1A\x02\x02\u055B\xE6" + + "\x03\x02\x02\x02\u055C\u055D\t\x1B\x02\x02\u055D\xE8\x03\x02\x02\x02\u055E" + + "\u055F\t\x1C\x02\x02\u055F\xEA\x03\x02\x02\x02\u0560\u0561\t\x1D\x02\x02" + + "\u0561\xEC\x03\x02\x02\x02\u0562\u0563\t\x1E\x02\x02\u0563\xEE\x03\x02" + + "\x02\x02\u0564\u0565\t\x1F\x02\x02\u0565\xF0\x03\x02\x02\x02\u0566\u0567" + + "\t \x02\x02\u0567\xF2\x03\x02\x02\x02\u0568\u0569\t!\x02\x02\u0569\xF4" + + "\x03\x02\x02\x02\u056A\u056B\t\"\x02\x02\u056B\xF6\x03\x02\x02\x02\u056C" + + "\u056D\t#\x02\x02\u056D\xF8\x03\x02\x02\x02\u056E\u056F\t$\x02\x02\u056F" + + "\xFA\x03\x02\x02\x02\u0570\u0571\t%\x02\x02\u0571\xFC\x03\x02\x02\x02" + + "\u0572\u0573\t&\x02\x02\u0573\xFE\x03\x02\x02\x022\x02\x03\x04\x05\x06" + + "\u0190\u0194\u0197\u01A0\u01A2\u01AD\u01D6\u01DB\u01E0\u01E2\u01ED\u01F5" + + "\u01F8\u01FA\u01FF\u0204\u020A\u0211\u0216\u021C\u021F\u0227\u022B\u024C" + + "\u02A0\u02AC\u02C2\u02D3\u043F\u0492\u04A4\u04A6\u04AE\u04B0\u04B2\u04B8" + + "\u04BA\u04F0\u04F5\u04F9\u0527\u052C\u0530\x0E\x07\x04\x02\x07\x03\x02" + + "\x07\x05\x02\x07\x06\x02\x02\x03\x02\t%\x02\x07\x02\x02\t\x1A\x02\x06" + + "\x02\x02\t&\x02\t\"\x02\t!\x02"; + public static readonly _serializedATN: string = Utils.join( + [ + esql_lexer._serializedATNSegment0, + esql_lexer._serializedATNSegment1, + esql_lexer._serializedATNSegment2, + ], + "", + ); public static __ATN: ATN; public static get _ATN(): ATN { if (!esql_lexer.__ATN) { diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser.g4 b/packages/kbn-monaco/src/esql/antlr/esql_parser.g4 index 6196874af91bd..b062dc4f140e7 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser.g4 +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser.g4 @@ -23,19 +23,50 @@ sourceCommand : explainCommand | fromCommand | rowCommand + | showCommand ; processingCommand : evalCommand | limitCommand | projectCommand + | keepCommand + | renameCommand + | dropCommand + | dissectCommand + | grokCommand | sortCommand | statsCommand | whereCommand + | mvExpandCommand + | enrichCommand + ; + +enrichCommand + : ENRICH policyName=enrichIdentifier (ON matchField=enrichFieldIdentifier)? (WITH enrichWithClause (COMMA enrichWithClause)*)? + ; + +enrichWithClause + : (newName=enrichFieldIdentifier ASSIGN)? enrichField=enrichFieldIdentifier + ; + +mvExpandCommand + : MV_EXPAND qualifiedNames ; whereCommand - : WHERE booleanExpression + : WHERE whereBooleanExpression + ; + +whereBooleanExpression + : NOT whereBooleanExpression + | valueExpression + | regexBooleanExpression + | left=whereBooleanExpression operator=AND right=whereBooleanExpression + | left=whereBooleanExpression operator=OR right=whereBooleanExpression + | valueExpression (NOT)? IN LP valueExpression (COMMA valueExpression)* RP + | (NOT)? WHERE_FUNCTIONS LP qualifiedName ((COMMA functionExpressionArgument)*)? RP + | valueExpression IS NOT? NULL ; booleanExpression @@ -45,6 +76,11 @@ booleanExpression | left=booleanExpression operator=OR right=booleanExpression ; +regexBooleanExpression + : valueExpression (NOT)? kind=LIKE pattern=string + | valueExpression (NOT)? kind=RLIKE pattern=string + ; + valueExpression : operatorExpression | comparison @@ -58,9 +94,14 @@ mathFn : functionIdentifier LP (functionExpressionArgument (COMMA functionExpressionArgument)*)? RP ; +mathEvalFn + : mathFunctionIdentifier LP (mathFunctionExpressionArgument (COMMA mathFunctionExpressionArgument)*)? RP + ; + operatorExpression : primaryExpression | mathFn + | mathEvalFn | operator=(MINUS | PLUS) operatorExpression | left=operatorExpression operator=(ASTERISK | SLASH | PERCENT) right=operatorExpression | left=operatorExpression operator=(PLUS | MINUS) right=operatorExpression @@ -86,12 +127,21 @@ field | userVariable ASSIGN booleanExpression ; +enrichFieldIdentifier + : ENR_UNQUOTED_IDENTIFIER + | ENR_QUOTED_IDENTIFIER + ; + userVariable : identifier ; fromCommand - : FROM sourceIdentifier (COMMA sourceIdentifier)* + : FROM sourceIdentifier (COMMA sourceIdentifier)* metadata? + ; + +metadata + : OPENING_BRACKET METADATA sourceIdentifier (COMMA sourceIdentifier)* CLOSING_BRACKET ; evalCommand @@ -99,7 +149,7 @@ evalCommand ; statsCommand - : STATS fields (BY qualifiedNames)? + : STATS fields? (BY qualifiedNames)? ; sourceIdentifier @@ -107,9 +157,24 @@ sourceIdentifier | SRC_QUOTED_IDENTIFIER ; +enrichIdentifier + : ENR_UNQUOTED_IDENTIFIER + | ENR_QUOTED_IDENTIFIER + ; + functionExpressionArgument : qualifiedName | string + | number + ; + +mathFunctionExpressionArgument + : qualifiedName + | string + | number + | operatorExpression + | number (DATE_LITERAL) + | comparison ; qualifiedName @@ -123,6 +188,11 @@ qualifiedNames identifier : UNQUOTED_IDENTIFIER | QUOTED_IDENTIFIER + | ASTERISK + ; + +mathFunctionIdentifier + : MATH_FUNCTION ; functionIdentifier @@ -130,10 +200,18 @@ functionIdentifier ; constant - : NULL #nullLiteral - | number #numericLiteral - | booleanValue #booleanLiteral - | string #stringLiteral + : NULL + | numericValue + | booleanValue + | string + | OPENING_BRACKET numericValue (COMMA numericValue)* CLOSING_BRACKET + | OPENING_BRACKET booleanValue (COMMA booleanValue)* CLOSING_BRACKET + | OPENING_BRACKET string (COMMA string)* CLOSING_BRACKET + ; + +numericValue + : decimalValue + | integerValue ; limitCommand @@ -149,12 +227,44 @@ orderExpression ; projectCommand - : PROJECT projectClause (COMMA projectClause)* + : PROJECT qualifiedNames + ; + +keepCommand + : KEEP qualifiedNames + ; + + +dropCommand + : DROP qualifiedNames + ; + +renameVariable + : identifier (DOT identifier)* + ; + +renameCommand + : RENAME renameClause (COMMA renameClause)* + ; + +renameClause + : qualifiedName AS renameVariable ; -projectClause - : sourceIdentifier - | newName=sourceIdentifier ASSIGN oldName=sourceIdentifier +dissectCommand + : DISSECT qualifiedNames string commandOptions? + ; + +grokCommand + : GROK qualifiedNames string + ; + +commandOptions + : commandOption (COMMA commandOption)* + ; + +commandOption + : identifier ASSIGN constant ; booleanValue @@ -166,6 +276,14 @@ number | INTEGER_LITERAL #integerLiteral ; +decimalValue + : DECIMAL_LITERAL + ; + +integerValue + : INTEGER_LITERAL + ; + string : STRING ; @@ -181,3 +299,8 @@ explainCommand subqueryExpression : OPENING_BRACKET query CLOSING_BRACKET ; + +showCommand + : SHOW INFO + | SHOW FUNCTIONS + ; diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser.interp b/packages/kbn-monaco/src/esql/antlr/esql_parser.interp index 39dc1a09fb8ba..8c0671e0d0a59 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser.interp +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser.interp @@ -1,14 +1,25 @@ token literal names: null -'eval' -'explain' -'from' -'row' -'stats' -'where' -'sort' -'limit' -'project' +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null null null null @@ -17,17 +28,26 @@ null null null 'by' +null 'and' null null '.' '(' -'[' +null ']' -'not' -'null' +null +null +null +null +null +null +null 'or' ')' +'_' +'info' +'functions' null null '+' @@ -35,6 +55,7 @@ null '*' '/' '%' +'10' null 'nulls' null @@ -49,9 +70,22 @@ null null null null +null +null +null +null +null +null +null +null +null +null +null token symbolic names: null +DISSECT +GROK EVAL EXPLAIN FROM @@ -59,16 +93,26 @@ ROW STATS WHERE SORT +MV_EXPAND LIMIT PROJECT +DROP +RENAME +SHOW +ENRICH +KEEP LINE_COMMENT MULTILINE_COMMENT WS +EXPLAIN_WS +EXPLAIN_LINE_COMMENT +EXPLAIN_MULTILINE_COMMENT PIPE STRING INTEGER_LITERAL DECIMAL_LITERAL BY +DATE_LITERAL AND ASSIGN COMMA @@ -77,9 +121,17 @@ LP OPENING_BRACKET CLOSING_BRACKET NOT +LIKE +RLIKE +IN +IS +AS NULL OR RP +UNDERSCORE +INFO +FUNCTIONS BOOLEAN_VALUE COMPARISON_OPERATOR PLUS @@ -87,59 +139,94 @@ MINUS ASTERISK SLASH PERCENT +TEN ORDERING NULLS_ORDERING NULLS_ORDERING_DIRECTION +MATH_FUNCTION UNARY_FUNCTION +WHERE_FUNCTIONS UNQUOTED_IDENTIFIER QUOTED_IDENTIFIER EXPR_LINE_COMMENT EXPR_MULTILINE_COMMENT EXPR_WS +METADATA SRC_UNQUOTED_IDENTIFIER SRC_QUOTED_IDENTIFIER SRC_LINE_COMMENT SRC_MULTILINE_COMMENT SRC_WS +ON +WITH +ENR_UNQUOTED_IDENTIFIER +ENR_QUOTED_IDENTIFIER +ENR_LINE_COMMENT +ENR_MULTILINE_COMMENT +ENR_WS +EXPLAIN_PIPE rule names: singleStatement query sourceCommand processingCommand +enrichCommand +enrichWithClause +mvExpandCommand whereCommand +whereBooleanExpression booleanExpression +regexBooleanExpression valueExpression comparison mathFn +mathEvalFn operatorExpression primaryExpression rowCommand fields field +enrichFieldIdentifier userVariable fromCommand +metadata evalCommand statsCommand sourceIdentifier +enrichIdentifier functionExpressionArgument +mathFunctionExpressionArgument qualifiedName qualifiedNames identifier +mathFunctionIdentifier functionIdentifier constant +numericValue limitCommand sortCommand orderExpression projectCommand -projectClause +keepCommand +dropCommand +renameVariable +renameCommand +renameClause +dissectCommand +grokCommand +commandOptions +commandOption booleanValue number +decimalValue +integerValue string comparisonOperator explainCommand subqueryExpression +showCommand atn: -[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 3, 51, 307, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 84, 10, 3, 12, 3, 14, 3, 87, 11, 3, 3, 4, 3, 4, 3, 4, 5, 4, 92, 10, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 5, 5, 100, 10, 5, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 5, 7, 109, 10, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 7, 7, 117, 10, 7, 12, 7, 14, 7, 120, 11, 7, 3, 8, 3, 8, 5, 8, 124, 10, 8, 3, 9, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 7, 10, 135, 10, 10, 12, 10, 14, 10, 138, 11, 10, 5, 10, 140, 10, 10, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 5, 11, 149, 10, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 7, 11, 157, 10, 11, 12, 11, 14, 11, 160, 11, 11, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 7, 12, 173, 10, 12, 12, 12, 14, 12, 176, 11, 12, 5, 12, 178, 10, 12, 3, 12, 3, 12, 5, 12, 182, 10, 12, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 7, 14, 190, 10, 14, 12, 14, 14, 14, 193, 11, 14, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 5, 15, 200, 10, 15, 3, 16, 3, 16, 3, 17, 3, 17, 3, 17, 3, 17, 7, 17, 208, 10, 17, 12, 17, 14, 17, 211, 11, 17, 3, 18, 3, 18, 3, 18, 3, 19, 3, 19, 3, 19, 3, 19, 5, 19, 220, 10, 19, 3, 20, 3, 20, 3, 21, 3, 21, 5, 21, 226, 10, 21, 3, 22, 3, 22, 3, 22, 7, 22, 231, 10, 22, 12, 22, 14, 22, 234, 11, 22, 3, 23, 3, 23, 3, 23, 7, 23, 239, 10, 23, 12, 23, 14, 23, 242, 11, 23, 3, 24, 3, 24, 3, 25, 3, 25, 3, 26, 3, 26, 3, 26, 3, 26, 5, 26, 252, 10, 26, 3, 27, 3, 27, 3, 27, 3, 28, 3, 28, 3, 28, 3, 28, 7, 28, 261, 10, 28, 12, 28, 14, 28, 264, 11, 28, 3, 29, 3, 29, 5, 29, 268, 10, 29, 3, 29, 3, 29, 5, 29, 272, 10, 29, 3, 30, 3, 30, 3, 30, 3, 30, 7, 30, 278, 10, 30, 12, 30, 14, 30, 281, 11, 30, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 5, 31, 288, 10, 31, 3, 32, 3, 32, 3, 33, 3, 33, 5, 33, 294, 10, 33, 3, 34, 3, 34, 3, 35, 3, 35, 3, 36, 3, 36, 3, 36, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 2, 2, 5, 4, 12, 20, 38, 2, 2, 4, 2, 6, 2, 8, 2, 10, 2, 12, 2, 14, 2, 16, 2, 18, 2, 20, 2, 22, 2, 24, 2, 26, 2, 28, 2, 30, 2, 32, 2, 34, 2, 36, 2, 38, 2, 40, 2, 42, 2, 44, 2, 46, 2, 48, 2, 50, 2, 52, 2, 54, 2, 56, 2, 58, 2, 60, 2, 62, 2, 64, 2, 66, 2, 68, 2, 70, 2, 72, 2, 2, 6, 3, 2, 33, 34, 3, 2, 35, 37, 3, 2, 47, 48, 3, 2, 42, 43, 2, 309, 2, 74, 3, 2, 2, 2, 4, 77, 3, 2, 2, 2, 6, 91, 3, 2, 2, 2, 8, 99, 3, 2, 2, 2, 10, 101, 3, 2, 2, 2, 12, 108, 3, 2, 2, 2, 14, 123, 3, 2, 2, 2, 16, 125, 3, 2, 2, 2, 18, 129, 3, 2, 2, 2, 20, 148, 3, 2, 2, 2, 22, 181, 3, 2, 2, 2, 24, 183, 3, 2, 2, 2, 26, 186, 3, 2, 2, 2, 28, 199, 3, 2, 2, 2, 30, 201, 3, 2, 2, 2, 32, 203, 3, 2, 2, 2, 34, 212, 3, 2, 2, 2, 36, 215, 3, 2, 2, 2, 38, 221, 3, 2, 2, 2, 40, 225, 3, 2, 2, 2, 42, 227, 3, 2, 2, 2, 44, 235, 3, 2, 2, 2, 46, 243, 3, 2, 2, 2, 48, 245, 3, 2, 2, 2, 50, 251, 3, 2, 2, 2, 52, 253, 3, 2, 2, 2, 54, 256, 3, 2, 2, 2, 56, 265, 3, 2, 2, 2, 58, 273, 3, 2, 2, 2, 60, 287, 3, 2, 2, 2, 62, 289, 3, 2, 2, 2, 64, 293, 3, 2, 2, 2, 66, 295, 3, 2, 2, 2, 68, 297, 3, 2, 2, 2, 70, 299, 3, 2, 2, 2, 72, 302, 3, 2, 2, 2, 74, 75, 5, 4, 3, 2, 75, 76, 7, 2, 2, 3, 76, 3, 3, 2, 2, 2, 77, 78, 8, 3, 1, 2, 78, 79, 5, 6, 4, 2, 79, 85, 3, 2, 2, 2, 80, 81, 12, 3, 2, 2, 81, 82, 7, 15, 2, 2, 82, 84, 5, 8, 5, 2, 83, 80, 3, 2, 2, 2, 84, 87, 3, 2, 2, 2, 85, 83, 3, 2, 2, 2, 85, 86, 3, 2, 2, 2, 86, 5, 3, 2, 2, 2, 87, 85, 3, 2, 2, 2, 88, 92, 5, 70, 36, 2, 89, 92, 5, 32, 17, 2, 90, 92, 5, 24, 13, 2, 91, 88, 3, 2, 2, 2, 91, 89, 3, 2, 2, 2, 91, 90, 3, 2, 2, 2, 92, 7, 3, 2, 2, 2, 93, 100, 5, 34, 18, 2, 94, 100, 5, 52, 27, 2, 95, 100, 5, 58, 30, 2, 96, 100, 5, 54, 28, 2, 97, 100, 5, 36, 19, 2, 98, 100, 5, 10, 6, 2, 99, 93, 3, 2, 2, 2, 99, 94, 3, 2, 2, 2, 99, 95, 3, 2, 2, 2, 99, 96, 3, 2, 2, 2, 99, 97, 3, 2, 2, 2, 99, 98, 3, 2, 2, 2, 100, 9, 3, 2, 2, 2, 101, 102, 7, 8, 2, 2, 102, 103, 5, 12, 7, 2, 103, 11, 3, 2, 2, 2, 104, 105, 8, 7, 1, 2, 105, 106, 7, 27, 2, 2, 106, 109, 5, 12, 7, 6, 107, 109, 5, 14, 8, 2, 108, 104, 3, 2, 2, 2, 108, 107, 3, 2, 2, 2, 109, 118, 3, 2, 2, 2, 110, 111, 12, 4, 2, 2, 111, 112, 7, 20, 2, 2, 112, 117, 5, 12, 7, 5, 113, 114, 12, 3, 2, 2, 114, 115, 7, 29, 2, 2, 115, 117, 5, 12, 7, 4, 116, 110, 3, 2, 2, 2, 116, 113, 3, 2, 2, 2, 117, 120, 3, 2, 2, 2, 118, 116, 3, 2, 2, 2, 118, 119, 3, 2, 2, 2, 119, 13, 3, 2, 2, 2, 120, 118, 3, 2, 2, 2, 121, 124, 5, 20, 11, 2, 122, 124, 5, 16, 9, 2, 123, 121, 3, 2, 2, 2, 123, 122, 3, 2, 2, 2, 124, 15, 3, 2, 2, 2, 125, 126, 5, 20, 11, 2, 126, 127, 5, 68, 35, 2, 127, 128, 5, 20, 11, 2, 128, 17, 3, 2, 2, 2, 129, 130, 5, 48, 25, 2, 130, 139, 7, 24, 2, 2, 131, 136, 5, 40, 21, 2, 132, 133, 7, 22, 2, 2, 133, 135, 5, 40, 21, 2, 134, 132, 3, 2, 2, 2, 135, 138, 3, 2, 2, 2, 136, 134, 3, 2, 2, 2, 136, 137, 3, 2, 2, 2, 137, 140, 3, 2, 2, 2, 138, 136, 3, 2, 2, 2, 139, 131, 3, 2, 2, 2, 139, 140, 3, 2, 2, 2, 140, 141, 3, 2, 2, 2, 141, 142, 7, 30, 2, 2, 142, 19, 3, 2, 2, 2, 143, 144, 8, 11, 1, 2, 144, 149, 5, 22, 12, 2, 145, 149, 5, 18, 10, 2, 146, 147, 9, 2, 2, 2, 147, 149, 5, 20, 11, 5, 148, 143, 3, 2, 2, 2, 148, 145, 3, 2, 2, 2, 148, 146, 3, 2, 2, 2, 149, 158, 3, 2, 2, 2, 150, 151, 12, 4, 2, 2, 151, 152, 9, 3, 2, 2, 152, 157, 5, 20, 11, 5, 153, 154, 12, 3, 2, 2, 154, 155, 9, 2, 2, 2, 155, 157, 5, 20, 11, 4, 156, 150, 3, 2, 2, 2, 156, 153, 3, 2, 2, 2, 157, 160, 3, 2, 2, 2, 158, 156, 3, 2, 2, 2, 158, 159, 3, 2, 2, 2, 159, 21, 3, 2, 2, 2, 160, 158, 3, 2, 2, 2, 161, 182, 5, 50, 26, 2, 162, 182, 5, 42, 22, 2, 163, 164, 7, 24, 2, 2, 164, 165, 5, 12, 7, 2, 165, 166, 7, 30, 2, 2, 166, 182, 3, 2, 2, 2, 167, 168, 5, 46, 24, 2, 168, 177, 7, 24, 2, 2, 169, 174, 5, 12, 7, 2, 170, 171, 7, 22, 2, 2, 171, 173, 5, 12, 7, 2, 172, 170, 3, 2, 2, 2, 173, 176, 3, 2, 2, 2, 174, 172, 3, 2, 2, 2, 174, 175, 3, 2, 2, 2, 175, 178, 3, 2, 2, 2, 176, 174, 3, 2, 2, 2, 177, 169, 3, 2, 2, 2, 177, 178, 3, 2, 2, 2, 178, 179, 3, 2, 2, 2, 179, 180, 7, 30, 2, 2, 180, 182, 3, 2, 2, 2, 181, 161, 3, 2, 2, 2, 181, 162, 3, 2, 2, 2, 181, 163, 3, 2, 2, 2, 181, 167, 3, 2, 2, 2, 182, 23, 3, 2, 2, 2, 183, 184, 7, 6, 2, 2, 184, 185, 5, 26, 14, 2, 185, 25, 3, 2, 2, 2, 186, 191, 5, 28, 15, 2, 187, 188, 7, 22, 2, 2, 188, 190, 5, 28, 15, 2, 189, 187, 3, 2, 2, 2, 190, 193, 3, 2, 2, 2, 191, 189, 3, 2, 2, 2, 191, 192, 3, 2, 2, 2, 192, 27, 3, 2, 2, 2, 193, 191, 3, 2, 2, 2, 194, 200, 5, 12, 7, 2, 195, 196, 5, 30, 16, 2, 196, 197, 7, 21, 2, 2, 197, 198, 5, 12, 7, 2, 198, 200, 3, 2, 2, 2, 199, 194, 3, 2, 2, 2, 199, 195, 3, 2, 2, 2, 200, 29, 3, 2, 2, 2, 201, 202, 5, 46, 24, 2, 202, 31, 3, 2, 2, 2, 203, 204, 7, 5, 2, 2, 204, 209, 5, 38, 20, 2, 205, 206, 7, 22, 2, 2, 206, 208, 5, 38, 20, 2, 207, 205, 3, 2, 2, 2, 208, 211, 3, 2, 2, 2, 209, 207, 3, 2, 2, 2, 209, 210, 3, 2, 2, 2, 210, 33, 3, 2, 2, 2, 211, 209, 3, 2, 2, 2, 212, 213, 7, 3, 2, 2, 213, 214, 5, 26, 14, 2, 214, 35, 3, 2, 2, 2, 215, 216, 7, 7, 2, 2, 216, 219, 5, 26, 14, 2, 217, 218, 7, 19, 2, 2, 218, 220, 5, 44, 23, 2, 219, 217, 3, 2, 2, 2, 219, 220, 3, 2, 2, 2, 220, 37, 3, 2, 2, 2, 221, 222, 9, 4, 2, 2, 222, 39, 3, 2, 2, 2, 223, 226, 5, 42, 22, 2, 224, 226, 5, 66, 34, 2, 225, 223, 3, 2, 2, 2, 225, 224, 3, 2, 2, 2, 226, 41, 3, 2, 2, 2, 227, 232, 5, 46, 24, 2, 228, 229, 7, 23, 2, 2, 229, 231, 5, 46, 24, 2, 230, 228, 3, 2, 2, 2, 231, 234, 3, 2, 2, 2, 232, 230, 3, 2, 2, 2, 232, 233, 3, 2, 2, 2, 233, 43, 3, 2, 2, 2, 234, 232, 3, 2, 2, 2, 235, 240, 5, 42, 22, 2, 236, 237, 7, 22, 2, 2, 237, 239, 5, 42, 22, 2, 238, 236, 3, 2, 2, 2, 239, 242, 3, 2, 2, 2, 240, 238, 3, 2, 2, 2, 240, 241, 3, 2, 2, 2, 241, 45, 3, 2, 2, 2, 242, 240, 3, 2, 2, 2, 243, 244, 9, 5, 2, 2, 244, 47, 3, 2, 2, 2, 245, 246, 7, 41, 2, 2, 246, 49, 3, 2, 2, 2, 247, 252, 7, 28, 2, 2, 248, 252, 5, 64, 33, 2, 249, 252, 5, 62, 32, 2, 250, 252, 5, 66, 34, 2, 251, 247, 3, 2, 2, 2, 251, 248, 3, 2, 2, 2, 251, 249, 3, 2, 2, 2, 251, 250, 3, 2, 2, 2, 252, 51, 3, 2, 2, 2, 253, 254, 7, 10, 2, 2, 254, 255, 7, 17, 2, 2, 255, 53, 3, 2, 2, 2, 256, 257, 7, 9, 2, 2, 257, 262, 5, 56, 29, 2, 258, 259, 7, 22, 2, 2, 259, 261, 5, 56, 29, 2, 260, 258, 3, 2, 2, 2, 261, 264, 3, 2, 2, 2, 262, 260, 3, 2, 2, 2, 262, 263, 3, 2, 2, 2, 263, 55, 3, 2, 2, 2, 264, 262, 3, 2, 2, 2, 265, 267, 5, 12, 7, 2, 266, 268, 7, 38, 2, 2, 267, 266, 3, 2, 2, 2, 267, 268, 3, 2, 2, 2, 268, 271, 3, 2, 2, 2, 269, 270, 7, 39, 2, 2, 270, 272, 7, 40, 2, 2, 271, 269, 3, 2, 2, 2, 271, 272, 3, 2, 2, 2, 272, 57, 3, 2, 2, 2, 273, 274, 7, 11, 2, 2, 274, 279, 5, 60, 31, 2, 275, 276, 7, 22, 2, 2, 276, 278, 5, 60, 31, 2, 277, 275, 3, 2, 2, 2, 278, 281, 3, 2, 2, 2, 279, 277, 3, 2, 2, 2, 279, 280, 3, 2, 2, 2, 280, 59, 3, 2, 2, 2, 281, 279, 3, 2, 2, 2, 282, 288, 5, 38, 20, 2, 283, 284, 5, 38, 20, 2, 284, 285, 7, 21, 2, 2, 285, 286, 5, 38, 20, 2, 286, 288, 3, 2, 2, 2, 287, 282, 3, 2, 2, 2, 287, 283, 3, 2, 2, 2, 288, 61, 3, 2, 2, 2, 289, 290, 7, 31, 2, 2, 290, 63, 3, 2, 2, 2, 291, 294, 7, 18, 2, 2, 292, 294, 7, 17, 2, 2, 293, 291, 3, 2, 2, 2, 293, 292, 3, 2, 2, 2, 294, 65, 3, 2, 2, 2, 295, 296, 7, 16, 2, 2, 296, 67, 3, 2, 2, 2, 297, 298, 7, 32, 2, 2, 298, 69, 3, 2, 2, 2, 299, 300, 7, 4, 2, 2, 300, 301, 5, 72, 37, 2, 301, 71, 3, 2, 2, 2, 302, 303, 7, 25, 2, 2, 303, 304, 5, 4, 3, 2, 304, 305, 7, 26, 2, 2, 305, 73, 3, 2, 2, 2, 31, 85, 91, 99, 108, 116, 118, 123, 136, 139, 148, 156, 158, 174, 177, 181, 191, 199, 209, 219, 225, 232, 240, 251, 262, 267, 271, 279, 287, 293] \ No newline at end of file +[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 3, 83, 594, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 4, 52, 9, 52, 4, 53, 9, 53, 4, 54, 9, 54, 4, 55, 9, 55, 4, 56, 9, 56, 4, 57, 9, 57, 4, 58, 9, 58, 4, 59, 9, 59, 4, 60, 9, 60, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 130, 10, 3, 12, 3, 14, 3, 133, 11, 3, 3, 4, 3, 4, 3, 4, 3, 4, 5, 4, 139, 10, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 5, 5, 154, 10, 5, 3, 6, 3, 6, 3, 6, 3, 6, 5, 6, 160, 10, 6, 3, 6, 3, 6, 3, 6, 3, 6, 7, 6, 166, 10, 6, 12, 6, 14, 6, 169, 11, 6, 5, 6, 171, 10, 6, 3, 7, 3, 7, 3, 7, 5, 7, 176, 10, 7, 3, 7, 3, 7, 3, 8, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 5, 10, 193, 10, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 7, 10, 200, 10, 10, 12, 10, 14, 10, 203, 11, 10, 3, 10, 3, 10, 3, 10, 5, 10, 208, 10, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 7, 10, 215, 10, 10, 12, 10, 14, 10, 218, 11, 10, 5, 10, 220, 10, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 5, 10, 227, 10, 10, 3, 10, 3, 10, 5, 10, 231, 10, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 7, 10, 239, 10, 10, 12, 10, 14, 10, 242, 11, 10, 3, 11, 3, 11, 3, 11, 3, 11, 5, 11, 248, 10, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 7, 11, 256, 10, 11, 12, 11, 14, 11, 259, 11, 11, 3, 12, 3, 12, 5, 12, 263, 10, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 5, 12, 270, 10, 12, 3, 12, 3, 12, 3, 12, 5, 12, 275, 10, 12, 3, 13, 3, 13, 5, 13, 279, 10, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 7, 15, 290, 10, 15, 12, 15, 14, 15, 293, 11, 15, 5, 15, 295, 10, 15, 3, 15, 3, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 7, 16, 304, 10, 16, 12, 16, 14, 16, 307, 11, 16, 5, 16, 309, 10, 16, 3, 16, 3, 16, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 5, 17, 319, 10, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 7, 17, 327, 10, 17, 12, 17, 14, 17, 330, 11, 17, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 7, 18, 343, 10, 18, 12, 18, 14, 18, 346, 11, 18, 5, 18, 348, 10, 18, 3, 18, 3, 18, 5, 18, 352, 10, 18, 3, 19, 3, 19, 3, 19, 3, 20, 3, 20, 3, 20, 7, 20, 360, 10, 20, 12, 20, 14, 20, 363, 11, 20, 3, 21, 3, 21, 3, 21, 3, 21, 3, 21, 5, 21, 370, 10, 21, 3, 22, 3, 22, 3, 23, 3, 23, 3, 24, 3, 24, 3, 24, 3, 24, 7, 24, 380, 10, 24, 12, 24, 14, 24, 383, 11, 24, 3, 24, 5, 24, 386, 10, 24, 3, 25, 3, 25, 3, 25, 3, 25, 3, 25, 7, 25, 393, 10, 25, 12, 25, 14, 25, 396, 11, 25, 3, 25, 3, 25, 3, 26, 3, 26, 3, 26, 3, 27, 3, 27, 5, 27, 405, 10, 27, 3, 27, 3, 27, 5, 27, 409, 10, 27, 3, 28, 3, 28, 3, 29, 3, 29, 3, 30, 3, 30, 3, 30, 5, 30, 418, 10, 30, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 5, 31, 428, 10, 31, 3, 32, 3, 32, 3, 32, 7, 32, 433, 10, 32, 12, 32, 14, 32, 436, 11, 32, 3, 33, 3, 33, 3, 33, 7, 33, 441, 10, 33, 12, 33, 14, 33, 444, 11, 33, 3, 34, 3, 34, 3, 35, 3, 35, 3, 36, 3, 36, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 7, 37, 460, 10, 37, 12, 37, 14, 37, 463, 11, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 7, 37, 471, 10, 37, 12, 37, 14, 37, 474, 11, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 3, 37, 7, 37, 482, 10, 37, 12, 37, 14, 37, 485, 11, 37, 3, 37, 3, 37, 5, 37, 489, 10, 37, 3, 38, 3, 38, 5, 38, 493, 10, 38, 3, 39, 3, 39, 3, 39, 3, 40, 3, 40, 3, 40, 3, 40, 7, 40, 502, 10, 40, 12, 40, 14, 40, 505, 11, 40, 3, 41, 3, 41, 5, 41, 509, 10, 41, 3, 41, 3, 41, 5, 41, 513, 10, 41, 3, 42, 3, 42, 3, 42, 3, 43, 3, 43, 3, 43, 3, 44, 3, 44, 3, 44, 3, 45, 3, 45, 3, 45, 7, 45, 527, 10, 45, 12, 45, 14, 45, 530, 11, 45, 3, 46, 3, 46, 3, 46, 3, 46, 7, 46, 536, 10, 46, 12, 46, 14, 46, 539, 11, 46, 3, 47, 3, 47, 3, 47, 3, 47, 3, 48, 3, 48, 3, 48, 3, 48, 5, 48, 549, 10, 48, 3, 49, 3, 49, 3, 49, 3, 49, 3, 50, 3, 50, 3, 50, 7, 50, 558, 10, 50, 12, 50, 14, 50, 561, 11, 50, 3, 51, 3, 51, 3, 51, 3, 51, 3, 52, 3, 52, 3, 53, 3, 53, 5, 53, 571, 10, 53, 3, 54, 3, 54, 3, 55, 3, 55, 3, 56, 3, 56, 3, 57, 3, 57, 3, 58, 3, 58, 3, 58, 3, 59, 3, 59, 3, 59, 3, 59, 3, 60, 3, 60, 3, 60, 3, 60, 5, 60, 592, 10, 60, 3, 60, 2, 2, 6, 4, 18, 20, 32, 61, 2, 2, 4, 2, 6, 2, 8, 2, 10, 2, 12, 2, 14, 2, 16, 2, 18, 2, 20, 2, 22, 2, 24, 2, 26, 2, 28, 2, 30, 2, 32, 2, 34, 2, 36, 2, 38, 2, 40, 2, 42, 2, 44, 2, 46, 2, 48, 2, 50, 2, 52, 2, 54, 2, 56, 2, 58, 2, 60, 2, 62, 2, 64, 2, 66, 2, 68, 2, 70, 2, 72, 2, 74, 2, 76, 2, 78, 2, 80, 2, 82, 2, 84, 2, 86, 2, 88, 2, 90, 2, 92, 2, 94, 2, 96, 2, 98, 2, 100, 2, 102, 2, 104, 2, 106, 2, 108, 2, 110, 2, 112, 2, 114, 2, 116, 2, 118, 2, 2, 7, 3, 2, 53, 54, 3, 2, 55, 57, 3, 2, 78, 79, 3, 2, 71, 72, 4, 2, 55, 55, 65, 66, 2, 623, 2, 120, 3, 2, 2, 2, 4, 123, 3, 2, 2, 2, 6, 138, 3, 2, 2, 2, 8, 153, 3, 2, 2, 2, 10, 155, 3, 2, 2, 2, 12, 175, 3, 2, 2, 2, 14, 179, 3, 2, 2, 2, 16, 182, 3, 2, 2, 2, 18, 230, 3, 2, 2, 2, 20, 247, 3, 2, 2, 2, 22, 274, 3, 2, 2, 2, 24, 278, 3, 2, 2, 2, 26, 280, 3, 2, 2, 2, 28, 284, 3, 2, 2, 2, 30, 298, 3, 2, 2, 2, 32, 318, 3, 2, 2, 2, 34, 351, 3, 2, 2, 2, 36, 353, 3, 2, 2, 2, 38, 356, 3, 2, 2, 2, 40, 369, 3, 2, 2, 2, 42, 371, 3, 2, 2, 2, 44, 373, 3, 2, 2, 2, 46, 375, 3, 2, 2, 2, 48, 387, 3, 2, 2, 2, 50, 399, 3, 2, 2, 2, 52, 402, 3, 2, 2, 2, 54, 410, 3, 2, 2, 2, 56, 412, 3, 2, 2, 2, 58, 417, 3, 2, 2, 2, 60, 427, 3, 2, 2, 2, 62, 429, 3, 2, 2, 2, 64, 437, 3, 2, 2, 2, 66, 445, 3, 2, 2, 2, 68, 447, 3, 2, 2, 2, 70, 449, 3, 2, 2, 2, 72, 488, 3, 2, 2, 2, 74, 492, 3, 2, 2, 2, 76, 494, 3, 2, 2, 2, 78, 497, 3, 2, 2, 2, 80, 506, 3, 2, 2, 2, 82, 514, 3, 2, 2, 2, 84, 517, 3, 2, 2, 2, 86, 520, 3, 2, 2, 2, 88, 523, 3, 2, 2, 2, 90, 531, 3, 2, 2, 2, 92, 540, 3, 2, 2, 2, 94, 544, 3, 2, 2, 2, 96, 550, 3, 2, 2, 2, 98, 554, 3, 2, 2, 2, 100, 562, 3, 2, 2, 2, 102, 566, 3, 2, 2, 2, 104, 570, 3, 2, 2, 2, 106, 572, 3, 2, 2, 2, 108, 574, 3, 2, 2, 2, 110, 576, 3, 2, 2, 2, 112, 578, 3, 2, 2, 2, 114, 580, 3, 2, 2, 2, 116, 583, 3, 2, 2, 2, 118, 591, 3, 2, 2, 2, 120, 121, 5, 4, 3, 2, 121, 122, 7, 2, 2, 3, 122, 3, 3, 2, 2, 2, 123, 124, 8, 3, 1, 2, 124, 125, 5, 6, 4, 2, 125, 131, 3, 2, 2, 2, 126, 127, 12, 3, 2, 2, 127, 128, 7, 26, 2, 2, 128, 130, 5, 8, 5, 2, 129, 126, 3, 2, 2, 2, 130, 133, 3, 2, 2, 2, 131, 129, 3, 2, 2, 2, 131, 132, 3, 2, 2, 2, 132, 5, 3, 2, 2, 2, 133, 131, 3, 2, 2, 2, 134, 139, 5, 114, 58, 2, 135, 139, 5, 46, 24, 2, 136, 139, 5, 36, 19, 2, 137, 139, 5, 118, 60, 2, 138, 134, 3, 2, 2, 2, 138, 135, 3, 2, 2, 2, 138, 136, 3, 2, 2, 2, 138, 137, 3, 2, 2, 2, 139, 7, 3, 2, 2, 2, 140, 154, 5, 50, 26, 2, 141, 154, 5, 76, 39, 2, 142, 154, 5, 82, 42, 2, 143, 154, 5, 84, 43, 2, 144, 154, 5, 90, 46, 2, 145, 154, 5, 86, 44, 2, 146, 154, 5, 94, 48, 2, 147, 154, 5, 96, 49, 2, 148, 154, 5, 78, 40, 2, 149, 154, 5, 52, 27, 2, 150, 154, 5, 16, 9, 2, 151, 154, 5, 14, 8, 2, 152, 154, 5, 10, 6, 2, 153, 140, 3, 2, 2, 2, 153, 141, 3, 2, 2, 2, 153, 142, 3, 2, 2, 2, 153, 143, 3, 2, 2, 2, 153, 144, 3, 2, 2, 2, 153, 145, 3, 2, 2, 2, 153, 146, 3, 2, 2, 2, 153, 147, 3, 2, 2, 2, 153, 148, 3, 2, 2, 2, 153, 149, 3, 2, 2, 2, 153, 150, 3, 2, 2, 2, 153, 151, 3, 2, 2, 2, 153, 152, 3, 2, 2, 2, 154, 9, 3, 2, 2, 2, 155, 156, 7, 18, 2, 2, 156, 159, 5, 56, 29, 2, 157, 158, 7, 76, 2, 2, 158, 160, 5, 42, 22, 2, 159, 157, 3, 2, 2, 2, 159, 160, 3, 2, 2, 2, 160, 170, 3, 2, 2, 2, 161, 162, 7, 77, 2, 2, 162, 167, 5, 12, 7, 2, 163, 164, 7, 34, 2, 2, 164, 166, 5, 12, 7, 2, 165, 163, 3, 2, 2, 2, 166, 169, 3, 2, 2, 2, 167, 165, 3, 2, 2, 2, 167, 168, 3, 2, 2, 2, 168, 171, 3, 2, 2, 2, 169, 167, 3, 2, 2, 2, 170, 161, 3, 2, 2, 2, 170, 171, 3, 2, 2, 2, 171, 11, 3, 2, 2, 2, 172, 173, 5, 42, 22, 2, 173, 174, 7, 33, 2, 2, 174, 176, 3, 2, 2, 2, 175, 172, 3, 2, 2, 2, 175, 176, 3, 2, 2, 2, 176, 177, 3, 2, 2, 2, 177, 178, 5, 42, 22, 2, 178, 13, 3, 2, 2, 2, 179, 180, 7, 12, 2, 2, 180, 181, 5, 64, 33, 2, 181, 15, 3, 2, 2, 2, 182, 183, 7, 10, 2, 2, 183, 184, 5, 18, 10, 2, 184, 17, 3, 2, 2, 2, 185, 186, 8, 10, 1, 2, 186, 187, 7, 39, 2, 2, 187, 231, 5, 18, 10, 10, 188, 231, 5, 24, 13, 2, 189, 231, 5, 22, 12, 2, 190, 192, 5, 24, 13, 2, 191, 193, 7, 39, 2, 2, 192, 191, 3, 2, 2, 2, 192, 193, 3, 2, 2, 2, 193, 194, 3, 2, 2, 2, 194, 195, 7, 42, 2, 2, 195, 196, 7, 36, 2, 2, 196, 201, 5, 24, 13, 2, 197, 198, 7, 34, 2, 2, 198, 200, 5, 24, 13, 2, 199, 197, 3, 2, 2, 2, 200, 203, 3, 2, 2, 2, 201, 199, 3, 2, 2, 2, 201, 202, 3, 2, 2, 2, 202, 204, 3, 2, 2, 2, 203, 201, 3, 2, 2, 2, 204, 205, 7, 47, 2, 2, 205, 231, 3, 2, 2, 2, 206, 208, 7, 39, 2, 2, 207, 206, 3, 2, 2, 2, 207, 208, 3, 2, 2, 2, 208, 209, 3, 2, 2, 2, 209, 210, 7, 64, 2, 2, 210, 211, 7, 36, 2, 2, 211, 219, 5, 62, 32, 2, 212, 213, 7, 34, 2, 2, 213, 215, 5, 58, 30, 2, 214, 212, 3, 2, 2, 2, 215, 218, 3, 2, 2, 2, 216, 214, 3, 2, 2, 2, 216, 217, 3, 2, 2, 2, 217, 220, 3, 2, 2, 2, 218, 216, 3, 2, 2, 2, 219, 216, 3, 2, 2, 2, 219, 220, 3, 2, 2, 2, 220, 221, 3, 2, 2, 2, 221, 222, 7, 47, 2, 2, 222, 231, 3, 2, 2, 2, 223, 224, 5, 24, 13, 2, 224, 226, 7, 43, 2, 2, 225, 227, 7, 39, 2, 2, 226, 225, 3, 2, 2, 2, 226, 227, 3, 2, 2, 2, 227, 228, 3, 2, 2, 2, 228, 229, 7, 45, 2, 2, 229, 231, 3, 2, 2, 2, 230, 185, 3, 2, 2, 2, 230, 188, 3, 2, 2, 2, 230, 189, 3, 2, 2, 2, 230, 190, 3, 2, 2, 2, 230, 207, 3, 2, 2, 2, 230, 223, 3, 2, 2, 2, 231, 240, 3, 2, 2, 2, 232, 233, 12, 7, 2, 2, 233, 234, 7, 32, 2, 2, 234, 239, 5, 18, 10, 8, 235, 236, 12, 6, 2, 2, 236, 237, 7, 46, 2, 2, 237, 239, 5, 18, 10, 7, 238, 232, 3, 2, 2, 2, 238, 235, 3, 2, 2, 2, 239, 242, 3, 2, 2, 2, 240, 238, 3, 2, 2, 2, 240, 241, 3, 2, 2, 2, 241, 19, 3, 2, 2, 2, 242, 240, 3, 2, 2, 2, 243, 244, 8, 11, 1, 2, 244, 245, 7, 39, 2, 2, 245, 248, 5, 20, 11, 6, 246, 248, 5, 24, 13, 2, 247, 243, 3, 2, 2, 2, 247, 246, 3, 2, 2, 2, 248, 257, 3, 2, 2, 2, 249, 250, 12, 4, 2, 2, 250, 251, 7, 32, 2, 2, 251, 256, 5, 20, 11, 5, 252, 253, 12, 3, 2, 2, 253, 254, 7, 46, 2, 2, 254, 256, 5, 20, 11, 4, 255, 249, 3, 2, 2, 2, 255, 252, 3, 2, 2, 2, 256, 259, 3, 2, 2, 2, 257, 255, 3, 2, 2, 2, 257, 258, 3, 2, 2, 2, 258, 21, 3, 2, 2, 2, 259, 257, 3, 2, 2, 2, 260, 262, 5, 24, 13, 2, 261, 263, 7, 39, 2, 2, 262, 261, 3, 2, 2, 2, 262, 263, 3, 2, 2, 2, 263, 264, 3, 2, 2, 2, 264, 265, 7, 40, 2, 2, 265, 266, 5, 110, 56, 2, 266, 275, 3, 2, 2, 2, 267, 269, 5, 24, 13, 2, 268, 270, 7, 39, 2, 2, 269, 268, 3, 2, 2, 2, 269, 270, 3, 2, 2, 2, 270, 271, 3, 2, 2, 2, 271, 272, 7, 41, 2, 2, 272, 273, 5, 110, 56, 2, 273, 275, 3, 2, 2, 2, 274, 260, 3, 2, 2, 2, 274, 267, 3, 2, 2, 2, 275, 23, 3, 2, 2, 2, 276, 279, 5, 32, 17, 2, 277, 279, 5, 26, 14, 2, 278, 276, 3, 2, 2, 2, 278, 277, 3, 2, 2, 2, 279, 25, 3, 2, 2, 2, 280, 281, 5, 32, 17, 2, 281, 282, 5, 112, 57, 2, 282, 283, 5, 32, 17, 2, 283, 27, 3, 2, 2, 2, 284, 285, 5, 70, 36, 2, 285, 294, 7, 36, 2, 2, 286, 291, 5, 58, 30, 2, 287, 288, 7, 34, 2, 2, 288, 290, 5, 58, 30, 2, 289, 287, 3, 2, 2, 2, 290, 293, 3, 2, 2, 2, 291, 289, 3, 2, 2, 2, 291, 292, 3, 2, 2, 2, 292, 295, 3, 2, 2, 2, 293, 291, 3, 2, 2, 2, 294, 286, 3, 2, 2, 2, 294, 295, 3, 2, 2, 2, 295, 296, 3, 2, 2, 2, 296, 297, 7, 47, 2, 2, 297, 29, 3, 2, 2, 2, 298, 299, 5, 68, 35, 2, 299, 308, 7, 36, 2, 2, 300, 305, 5, 60, 31, 2, 301, 302, 7, 34, 2, 2, 302, 304, 5, 60, 31, 2, 303, 301, 3, 2, 2, 2, 304, 307, 3, 2, 2, 2, 305, 303, 3, 2, 2, 2, 305, 306, 3, 2, 2, 2, 306, 309, 3, 2, 2, 2, 307, 305, 3, 2, 2, 2, 308, 300, 3, 2, 2, 2, 308, 309, 3, 2, 2, 2, 309, 310, 3, 2, 2, 2, 310, 311, 7, 47, 2, 2, 311, 31, 3, 2, 2, 2, 312, 313, 8, 17, 1, 2, 313, 319, 5, 34, 18, 2, 314, 319, 5, 28, 15, 2, 315, 319, 5, 30, 16, 2, 316, 317, 9, 2, 2, 2, 317, 319, 5, 32, 17, 5, 318, 312, 3, 2, 2, 2, 318, 314, 3, 2, 2, 2, 318, 315, 3, 2, 2, 2, 318, 316, 3, 2, 2, 2, 319, 328, 3, 2, 2, 2, 320, 321, 12, 4, 2, 2, 321, 322, 9, 3, 2, 2, 322, 327, 5, 32, 17, 5, 323, 324, 12, 3, 2, 2, 324, 325, 9, 2, 2, 2, 325, 327, 5, 32, 17, 4, 326, 320, 3, 2, 2, 2, 326, 323, 3, 2, 2, 2, 327, 330, 3, 2, 2, 2, 328, 326, 3, 2, 2, 2, 328, 329, 3, 2, 2, 2, 329, 33, 3, 2, 2, 2, 330, 328, 3, 2, 2, 2, 331, 352, 5, 72, 37, 2, 332, 352, 5, 62, 32, 2, 333, 334, 7, 36, 2, 2, 334, 335, 5, 20, 11, 2, 335, 336, 7, 47, 2, 2, 336, 352, 3, 2, 2, 2, 337, 338, 5, 66, 34, 2, 338, 347, 7, 36, 2, 2, 339, 344, 5, 20, 11, 2, 340, 341, 7, 34, 2, 2, 341, 343, 5, 20, 11, 2, 342, 340, 3, 2, 2, 2, 343, 346, 3, 2, 2, 2, 344, 342, 3, 2, 2, 2, 344, 345, 3, 2, 2, 2, 345, 348, 3, 2, 2, 2, 346, 344, 3, 2, 2, 2, 347, 339, 3, 2, 2, 2, 347, 348, 3, 2, 2, 2, 348, 349, 3, 2, 2, 2, 349, 350, 7, 47, 2, 2, 350, 352, 3, 2, 2, 2, 351, 331, 3, 2, 2, 2, 351, 332, 3, 2, 2, 2, 351, 333, 3, 2, 2, 2, 351, 337, 3, 2, 2, 2, 352, 35, 3, 2, 2, 2, 353, 354, 7, 8, 2, 2, 354, 355, 5, 38, 20, 2, 355, 37, 3, 2, 2, 2, 356, 361, 5, 40, 21, 2, 357, 358, 7, 34, 2, 2, 358, 360, 5, 40, 21, 2, 359, 357, 3, 2, 2, 2, 360, 363, 3, 2, 2, 2, 361, 359, 3, 2, 2, 2, 361, 362, 3, 2, 2, 2, 362, 39, 3, 2, 2, 2, 363, 361, 3, 2, 2, 2, 364, 370, 5, 20, 11, 2, 365, 366, 5, 44, 23, 2, 366, 367, 7, 33, 2, 2, 367, 368, 5, 20, 11, 2, 368, 370, 3, 2, 2, 2, 369, 364, 3, 2, 2, 2, 369, 365, 3, 2, 2, 2, 370, 41, 3, 2, 2, 2, 371, 372, 9, 4, 2, 2, 372, 43, 3, 2, 2, 2, 373, 374, 5, 66, 34, 2, 374, 45, 3, 2, 2, 2, 375, 376, 7, 7, 2, 2, 376, 381, 5, 54, 28, 2, 377, 378, 7, 34, 2, 2, 378, 380, 5, 54, 28, 2, 379, 377, 3, 2, 2, 2, 380, 383, 3, 2, 2, 2, 381, 379, 3, 2, 2, 2, 381, 382, 3, 2, 2, 2, 382, 385, 3, 2, 2, 2, 383, 381, 3, 2, 2, 2, 384, 386, 5, 48, 25, 2, 385, 384, 3, 2, 2, 2, 385, 386, 3, 2, 2, 2, 386, 47, 3, 2, 2, 2, 387, 388, 7, 37, 2, 2, 388, 389, 7, 70, 2, 2, 389, 394, 5, 54, 28, 2, 390, 391, 7, 34, 2, 2, 391, 393, 5, 54, 28, 2, 392, 390, 3, 2, 2, 2, 393, 396, 3, 2, 2, 2, 394, 392, 3, 2, 2, 2, 394, 395, 3, 2, 2, 2, 395, 397, 3, 2, 2, 2, 396, 394, 3, 2, 2, 2, 397, 398, 7, 38, 2, 2, 398, 49, 3, 2, 2, 2, 399, 400, 7, 5, 2, 2, 400, 401, 5, 38, 20, 2, 401, 51, 3, 2, 2, 2, 402, 404, 7, 9, 2, 2, 403, 405, 5, 38, 20, 2, 404, 403, 3, 2, 2, 2, 404, 405, 3, 2, 2, 2, 405, 408, 3, 2, 2, 2, 406, 407, 7, 30, 2, 2, 407, 409, 5, 64, 33, 2, 408, 406, 3, 2, 2, 2, 408, 409, 3, 2, 2, 2, 409, 53, 3, 2, 2, 2, 410, 411, 9, 5, 2, 2, 411, 55, 3, 2, 2, 2, 412, 413, 9, 4, 2, 2, 413, 57, 3, 2, 2, 2, 414, 418, 5, 62, 32, 2, 415, 418, 5, 110, 56, 2, 416, 418, 5, 104, 53, 2, 417, 414, 3, 2, 2, 2, 417, 415, 3, 2, 2, 2, 417, 416, 3, 2, 2, 2, 418, 59, 3, 2, 2, 2, 419, 428, 5, 62, 32, 2, 420, 428, 5, 110, 56, 2, 421, 428, 5, 104, 53, 2, 422, 428, 5, 32, 17, 2, 423, 424, 5, 104, 53, 2, 424, 425, 7, 31, 2, 2, 425, 428, 3, 2, 2, 2, 426, 428, 5, 26, 14, 2, 427, 419, 3, 2, 2, 2, 427, 420, 3, 2, 2, 2, 427, 421, 3, 2, 2, 2, 427, 422, 3, 2, 2, 2, 427, 423, 3, 2, 2, 2, 427, 426, 3, 2, 2, 2, 428, 61, 3, 2, 2, 2, 429, 434, 5, 66, 34, 2, 430, 431, 7, 35, 2, 2, 431, 433, 5, 66, 34, 2, 432, 430, 3, 2, 2, 2, 433, 436, 3, 2, 2, 2, 434, 432, 3, 2, 2, 2, 434, 435, 3, 2, 2, 2, 435, 63, 3, 2, 2, 2, 436, 434, 3, 2, 2, 2, 437, 442, 5, 62, 32, 2, 438, 439, 7, 34, 2, 2, 439, 441, 5, 62, 32, 2, 440, 438, 3, 2, 2, 2, 441, 444, 3, 2, 2, 2, 442, 440, 3, 2, 2, 2, 442, 443, 3, 2, 2, 2, 443, 65, 3, 2, 2, 2, 444, 442, 3, 2, 2, 2, 445, 446, 9, 6, 2, 2, 446, 67, 3, 2, 2, 2, 447, 448, 7, 62, 2, 2, 448, 69, 3, 2, 2, 2, 449, 450, 7, 63, 2, 2, 450, 71, 3, 2, 2, 2, 451, 489, 7, 45, 2, 2, 452, 489, 5, 74, 38, 2, 453, 489, 5, 102, 52, 2, 454, 489, 5, 110, 56, 2, 455, 456, 7, 37, 2, 2, 456, 461, 5, 74, 38, 2, 457, 458, 7, 34, 2, 2, 458, 460, 5, 74, 38, 2, 459, 457, 3, 2, 2, 2, 460, 463, 3, 2, 2, 2, 461, 459, 3, 2, 2, 2, 461, 462, 3, 2, 2, 2, 462, 464, 3, 2, 2, 2, 463, 461, 3, 2, 2, 2, 464, 465, 7, 38, 2, 2, 465, 489, 3, 2, 2, 2, 466, 467, 7, 37, 2, 2, 467, 472, 5, 102, 52, 2, 468, 469, 7, 34, 2, 2, 469, 471, 5, 102, 52, 2, 470, 468, 3, 2, 2, 2, 471, 474, 3, 2, 2, 2, 472, 470, 3, 2, 2, 2, 472, 473, 3, 2, 2, 2, 473, 475, 3, 2, 2, 2, 474, 472, 3, 2, 2, 2, 475, 476, 7, 38, 2, 2, 476, 489, 3, 2, 2, 2, 477, 478, 7, 37, 2, 2, 478, 483, 5, 110, 56, 2, 479, 480, 7, 34, 2, 2, 480, 482, 5, 110, 56, 2, 481, 479, 3, 2, 2, 2, 482, 485, 3, 2, 2, 2, 483, 481, 3, 2, 2, 2, 483, 484, 3, 2, 2, 2, 484, 486, 3, 2, 2, 2, 485, 483, 3, 2, 2, 2, 486, 487, 7, 38, 2, 2, 487, 489, 3, 2, 2, 2, 488, 451, 3, 2, 2, 2, 488, 452, 3, 2, 2, 2, 488, 453, 3, 2, 2, 2, 488, 454, 3, 2, 2, 2, 488, 455, 3, 2, 2, 2, 488, 466, 3, 2, 2, 2, 488, 477, 3, 2, 2, 2, 489, 73, 3, 2, 2, 2, 490, 493, 5, 106, 54, 2, 491, 493, 5, 108, 55, 2, 492, 490, 3, 2, 2, 2, 492, 491, 3, 2, 2, 2, 493, 75, 3, 2, 2, 2, 494, 495, 7, 13, 2, 2, 495, 496, 7, 28, 2, 2, 496, 77, 3, 2, 2, 2, 497, 498, 7, 11, 2, 2, 498, 503, 5, 80, 41, 2, 499, 500, 7, 34, 2, 2, 500, 502, 5, 80, 41, 2, 501, 499, 3, 2, 2, 2, 502, 505, 3, 2, 2, 2, 503, 501, 3, 2, 2, 2, 503, 504, 3, 2, 2, 2, 504, 79, 3, 2, 2, 2, 505, 503, 3, 2, 2, 2, 506, 508, 5, 20, 11, 2, 507, 509, 7, 59, 2, 2, 508, 507, 3, 2, 2, 2, 508, 509, 3, 2, 2, 2, 509, 512, 3, 2, 2, 2, 510, 511, 7, 60, 2, 2, 511, 513, 7, 61, 2, 2, 512, 510, 3, 2, 2, 2, 512, 513, 3, 2, 2, 2, 513, 81, 3, 2, 2, 2, 514, 515, 7, 14, 2, 2, 515, 516, 5, 64, 33, 2, 516, 83, 3, 2, 2, 2, 517, 518, 7, 19, 2, 2, 518, 519, 5, 64, 33, 2, 519, 85, 3, 2, 2, 2, 520, 521, 7, 15, 2, 2, 521, 522, 5, 64, 33, 2, 522, 87, 3, 2, 2, 2, 523, 528, 5, 66, 34, 2, 524, 525, 7, 35, 2, 2, 525, 527, 5, 66, 34, 2, 526, 524, 3, 2, 2, 2, 527, 530, 3, 2, 2, 2, 528, 526, 3, 2, 2, 2, 528, 529, 3, 2, 2, 2, 529, 89, 3, 2, 2, 2, 530, 528, 3, 2, 2, 2, 531, 532, 7, 16, 2, 2, 532, 537, 5, 92, 47, 2, 533, 534, 7, 34, 2, 2, 534, 536, 5, 92, 47, 2, 535, 533, 3, 2, 2, 2, 536, 539, 3, 2, 2, 2, 537, 535, 3, 2, 2, 2, 537, 538, 3, 2, 2, 2, 538, 91, 3, 2, 2, 2, 539, 537, 3, 2, 2, 2, 540, 541, 5, 62, 32, 2, 541, 542, 7, 44, 2, 2, 542, 543, 5, 88, 45, 2, 543, 93, 3, 2, 2, 2, 544, 545, 7, 3, 2, 2, 545, 546, 5, 64, 33, 2, 546, 548, 5, 110, 56, 2, 547, 549, 5, 98, 50, 2, 548, 547, 3, 2, 2, 2, 548, 549, 3, 2, 2, 2, 549, 95, 3, 2, 2, 2, 550, 551, 7, 4, 2, 2, 551, 552, 5, 64, 33, 2, 552, 553, 5, 110, 56, 2, 553, 97, 3, 2, 2, 2, 554, 559, 5, 100, 51, 2, 555, 556, 7, 34, 2, 2, 556, 558, 5, 100, 51, 2, 557, 555, 3, 2, 2, 2, 558, 561, 3, 2, 2, 2, 559, 557, 3, 2, 2, 2, 559, 560, 3, 2, 2, 2, 560, 99, 3, 2, 2, 2, 561, 559, 3, 2, 2, 2, 562, 563, 5, 66, 34, 2, 563, 564, 7, 33, 2, 2, 564, 565, 5, 72, 37, 2, 565, 101, 3, 2, 2, 2, 566, 567, 7, 51, 2, 2, 567, 103, 3, 2, 2, 2, 568, 571, 7, 29, 2, 2, 569, 571, 7, 28, 2, 2, 570, 568, 3, 2, 2, 2, 570, 569, 3, 2, 2, 2, 571, 105, 3, 2, 2, 2, 572, 573, 7, 29, 2, 2, 573, 107, 3, 2, 2, 2, 574, 575, 7, 28, 2, 2, 575, 109, 3, 2, 2, 2, 576, 577, 7, 27, 2, 2, 577, 111, 3, 2, 2, 2, 578, 579, 7, 52, 2, 2, 579, 113, 3, 2, 2, 2, 580, 581, 7, 6, 2, 2, 581, 582, 5, 116, 59, 2, 582, 115, 3, 2, 2, 2, 583, 584, 7, 37, 2, 2, 584, 585, 5, 4, 3, 2, 585, 586, 7, 38, 2, 2, 586, 117, 3, 2, 2, 2, 587, 588, 7, 17, 2, 2, 588, 592, 7, 49, 2, 2, 589, 590, 7, 17, 2, 2, 590, 592, 7, 50, 2, 2, 591, 587, 3, 2, 2, 2, 591, 589, 3, 2, 2, 2, 592, 119, 3, 2, 2, 2, 60, 131, 138, 153, 159, 167, 170, 175, 192, 201, 207, 216, 219, 226, 230, 238, 240, 247, 255, 257, 262, 269, 274, 278, 291, 294, 305, 308, 318, 326, 328, 344, 347, 351, 361, 369, 381, 385, 394, 404, 408, 417, 427, 434, 442, 461, 472, 483, 488, 492, 503, 508, 512, 528, 537, 548, 559, 570, 591] \ No newline at end of file diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser.tokens b/packages/kbn-monaco/src/esql/antlr/esql_parser.tokens index c2dafff2f222c..b72e97b9a2961 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser.tokens +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser.tokens @@ -1,74 +1,98 @@ -EVAL=1 -EXPLAIN=2 -FROM=3 -ROW=4 -STATS=5 -WHERE=6 -SORT=7 -LIMIT=8 -PROJECT=9 -LINE_COMMENT=10 -MULTILINE_COMMENT=11 -WS=12 -PIPE=13 -STRING=14 -INTEGER_LITERAL=15 -DECIMAL_LITERAL=16 -BY=17 -AND=18 -ASSIGN=19 -COMMA=20 -DOT=21 -LP=22 -OPENING_BRACKET=23 -CLOSING_BRACKET=24 -NOT=25 -NULL=26 -OR=27 -RP=28 -BOOLEAN_VALUE=29 -COMPARISON_OPERATOR=30 -PLUS=31 -MINUS=32 -ASTERISK=33 -SLASH=34 -PERCENT=35 -ORDERING=36 -NULLS_ORDERING=37 -NULLS_ORDERING_DIRECTION=38 -UNARY_FUNCTION=39 -UNQUOTED_IDENTIFIER=40 -QUOTED_IDENTIFIER=41 -EXPR_LINE_COMMENT=42 -EXPR_MULTILINE_COMMENT=43 -EXPR_WS=44 -SRC_UNQUOTED_IDENTIFIER=45 -SRC_QUOTED_IDENTIFIER=46 -SRC_LINE_COMMENT=47 -SRC_MULTILINE_COMMENT=48 -SRC_WS=49 -'eval'=1 -'explain'=2 -'from'=3 -'row'=4 -'stats'=5 -'where'=6 -'sort'=7 -'limit'=8 -'project'=9 -'by'=17 -'and'=18 -'.'=21 -'('=22 -'['=23 -']'=24 -'not'=25 -'null'=26 -'or'=27 -')'=28 -'+'=31 -'-'=32 -'*'=33 -'/'=34 -'%'=35 -'nulls'=37 +DISSECT=1 +GROK=2 +EVAL=3 +EXPLAIN=4 +FROM=5 +ROW=6 +STATS=7 +WHERE=8 +SORT=9 +MV_EXPAND=10 +LIMIT=11 +PROJECT=12 +DROP=13 +RENAME=14 +SHOW=15 +ENRICH=16 +KEEP=17 +LINE_COMMENT=18 +MULTILINE_COMMENT=19 +WS=20 +EXPLAIN_WS=21 +EXPLAIN_LINE_COMMENT=22 +EXPLAIN_MULTILINE_COMMENT=23 +PIPE=24 +STRING=25 +INTEGER_LITERAL=26 +DECIMAL_LITERAL=27 +BY=28 +DATE_LITERAL=29 +AND=30 +ASSIGN=31 +COMMA=32 +DOT=33 +LP=34 +OPENING_BRACKET=35 +CLOSING_BRACKET=36 +NOT=37 +LIKE=38 +RLIKE=39 +IN=40 +IS=41 +AS=42 +NULL=43 +OR=44 +RP=45 +UNDERSCORE=46 +INFO=47 +FUNCTIONS=48 +BOOLEAN_VALUE=49 +COMPARISON_OPERATOR=50 +PLUS=51 +MINUS=52 +ASTERISK=53 +SLASH=54 +PERCENT=55 +TEN=56 +ORDERING=57 +NULLS_ORDERING=58 +NULLS_ORDERING_DIRECTION=59 +MATH_FUNCTION=60 +UNARY_FUNCTION=61 +WHERE_FUNCTIONS=62 +UNQUOTED_IDENTIFIER=63 +QUOTED_IDENTIFIER=64 +EXPR_LINE_COMMENT=65 +EXPR_MULTILINE_COMMENT=66 +EXPR_WS=67 +METADATA=68 +SRC_UNQUOTED_IDENTIFIER=69 +SRC_QUOTED_IDENTIFIER=70 +SRC_LINE_COMMENT=71 +SRC_MULTILINE_COMMENT=72 +SRC_WS=73 +ON=74 +WITH=75 +ENR_UNQUOTED_IDENTIFIER=76 +ENR_QUOTED_IDENTIFIER=77 +ENR_LINE_COMMENT=78 +ENR_MULTILINE_COMMENT=79 +ENR_WS=80 +EXPLAIN_PIPE=81 +'by'=28 +'and'=30 +'.'=33 +'('=34 +']'=36 +'or'=44 +')'=45 +'_'=46 +'info'=47 +'functions'=48 +'+'=51 +'-'=52 +'*'=53 +'/'=54 +'%'=55 +'10'=56 +'nulls'=58 diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser.ts b/packages/kbn-monaco/src/esql/antlr/esql_parser.ts index de825a0b3698e..e70ad71c58ba1 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser.ts +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser.ts @@ -24,123 +24,192 @@ import { VocabularyImpl } from "antlr4ts/VocabularyImpl"; import * as Utils from "antlr4ts/misc/Utils"; -import { esql_parserListener } from "./esql_parser_listener"; +import { esql_parserListener } from "./esql_parserListener"; export class esql_parser extends Parser { - public static readonly EVAL = 1; - public static readonly EXPLAIN = 2; - public static readonly FROM = 3; - public static readonly ROW = 4; - public static readonly STATS = 5; - public static readonly WHERE = 6; - public static readonly SORT = 7; - public static readonly LIMIT = 8; - public static readonly PROJECT = 9; - public static readonly LINE_COMMENT = 10; - public static readonly MULTILINE_COMMENT = 11; - public static readonly WS = 12; - public static readonly PIPE = 13; - public static readonly STRING = 14; - public static readonly INTEGER_LITERAL = 15; - public static readonly DECIMAL_LITERAL = 16; - public static readonly BY = 17; - public static readonly AND = 18; - public static readonly ASSIGN = 19; - public static readonly COMMA = 20; - public static readonly DOT = 21; - public static readonly LP = 22; - public static readonly OPENING_BRACKET = 23; - public static readonly CLOSING_BRACKET = 24; - public static readonly NOT = 25; - public static readonly NULL = 26; - public static readonly OR = 27; - public static readonly RP = 28; - public static readonly BOOLEAN_VALUE = 29; - public static readonly COMPARISON_OPERATOR = 30; - public static readonly PLUS = 31; - public static readonly MINUS = 32; - public static readonly ASTERISK = 33; - public static readonly SLASH = 34; - public static readonly PERCENT = 35; - public static readonly ORDERING = 36; - public static readonly NULLS_ORDERING = 37; - public static readonly NULLS_ORDERING_DIRECTION = 38; - public static readonly UNARY_FUNCTION = 39; - public static readonly UNQUOTED_IDENTIFIER = 40; - public static readonly QUOTED_IDENTIFIER = 41; - public static readonly EXPR_LINE_COMMENT = 42; - public static readonly EXPR_MULTILINE_COMMENT = 43; - public static readonly EXPR_WS = 44; - public static readonly SRC_UNQUOTED_IDENTIFIER = 45; - public static readonly SRC_QUOTED_IDENTIFIER = 46; - public static readonly SRC_LINE_COMMENT = 47; - public static readonly SRC_MULTILINE_COMMENT = 48; - public static readonly SRC_WS = 49; + public static readonly DISSECT = 1; + public static readonly GROK = 2; + public static readonly EVAL = 3; + public static readonly EXPLAIN = 4; + public static readonly FROM = 5; + public static readonly ROW = 6; + public static readonly STATS = 7; + public static readonly WHERE = 8; + public static readonly SORT = 9; + public static readonly MV_EXPAND = 10; + public static readonly LIMIT = 11; + public static readonly PROJECT = 12; + public static readonly DROP = 13; + public static readonly RENAME = 14; + public static readonly SHOW = 15; + public static readonly ENRICH = 16; + public static readonly KEEP = 17; + public static readonly LINE_COMMENT = 18; + public static readonly MULTILINE_COMMENT = 19; + public static readonly WS = 20; + public static readonly EXPLAIN_WS = 21; + public static readonly EXPLAIN_LINE_COMMENT = 22; + public static readonly EXPLAIN_MULTILINE_COMMENT = 23; + public static readonly PIPE = 24; + public static readonly STRING = 25; + public static readonly INTEGER_LITERAL = 26; + public static readonly DECIMAL_LITERAL = 27; + public static readonly BY = 28; + public static readonly DATE_LITERAL = 29; + public static readonly AND = 30; + public static readonly ASSIGN = 31; + public static readonly COMMA = 32; + public static readonly DOT = 33; + public static readonly LP = 34; + public static readonly OPENING_BRACKET = 35; + public static readonly CLOSING_BRACKET = 36; + public static readonly NOT = 37; + public static readonly LIKE = 38; + public static readonly RLIKE = 39; + public static readonly IN = 40; + public static readonly IS = 41; + public static readonly AS = 42; + public static readonly NULL = 43; + public static readonly OR = 44; + public static readonly RP = 45; + public static readonly UNDERSCORE = 46; + public static readonly INFO = 47; + public static readonly FUNCTIONS = 48; + public static readonly BOOLEAN_VALUE = 49; + public static readonly COMPARISON_OPERATOR = 50; + public static readonly PLUS = 51; + public static readonly MINUS = 52; + public static readonly ASTERISK = 53; + public static readonly SLASH = 54; + public static readonly PERCENT = 55; + public static readonly TEN = 56; + public static readonly ORDERING = 57; + public static readonly NULLS_ORDERING = 58; + public static readonly NULLS_ORDERING_DIRECTION = 59; + public static readonly MATH_FUNCTION = 60; + public static readonly UNARY_FUNCTION = 61; + public static readonly WHERE_FUNCTIONS = 62; + public static readonly UNQUOTED_IDENTIFIER = 63; + public static readonly QUOTED_IDENTIFIER = 64; + public static readonly EXPR_LINE_COMMENT = 65; + public static readonly EXPR_MULTILINE_COMMENT = 66; + public static readonly EXPR_WS = 67; + public static readonly METADATA = 68; + public static readonly SRC_UNQUOTED_IDENTIFIER = 69; + public static readonly SRC_QUOTED_IDENTIFIER = 70; + public static readonly SRC_LINE_COMMENT = 71; + public static readonly SRC_MULTILINE_COMMENT = 72; + public static readonly SRC_WS = 73; + public static readonly ON = 74; + public static readonly WITH = 75; + public static readonly ENR_UNQUOTED_IDENTIFIER = 76; + public static readonly ENR_QUOTED_IDENTIFIER = 77; + public static readonly ENR_LINE_COMMENT = 78; + public static readonly ENR_MULTILINE_COMMENT = 79; + public static readonly ENR_WS = 80; + public static readonly EXPLAIN_PIPE = 81; public static readonly RULE_singleStatement = 0; public static readonly RULE_query = 1; public static readonly RULE_sourceCommand = 2; public static readonly RULE_processingCommand = 3; - public static readonly RULE_whereCommand = 4; - public static readonly RULE_booleanExpression = 5; - public static readonly RULE_valueExpression = 6; - public static readonly RULE_comparison = 7; - public static readonly RULE_mathFn = 8; - public static readonly RULE_operatorExpression = 9; - public static readonly RULE_primaryExpression = 10; - public static readonly RULE_rowCommand = 11; - public static readonly RULE_fields = 12; - public static readonly RULE_field = 13; - public static readonly RULE_userVariable = 14; - public static readonly RULE_fromCommand = 15; - public static readonly RULE_evalCommand = 16; - public static readonly RULE_statsCommand = 17; - public static readonly RULE_sourceIdentifier = 18; - public static readonly RULE_functionExpressionArgument = 19; - public static readonly RULE_qualifiedName = 20; - public static readonly RULE_qualifiedNames = 21; - public static readonly RULE_identifier = 22; - public static readonly RULE_functionIdentifier = 23; - public static readonly RULE_constant = 24; - public static readonly RULE_limitCommand = 25; - public static readonly RULE_sortCommand = 26; - public static readonly RULE_orderExpression = 27; - public static readonly RULE_projectCommand = 28; - public static readonly RULE_projectClause = 29; - public static readonly RULE_booleanValue = 30; - public static readonly RULE_number = 31; - public static readonly RULE_string = 32; - public static readonly RULE_comparisonOperator = 33; - public static readonly RULE_explainCommand = 34; - public static readonly RULE_subqueryExpression = 35; + public static readonly RULE_enrichCommand = 4; + public static readonly RULE_enrichWithClause = 5; + public static readonly RULE_mvExpandCommand = 6; + public static readonly RULE_whereCommand = 7; + public static readonly RULE_whereBooleanExpression = 8; + public static readonly RULE_booleanExpression = 9; + public static readonly RULE_regexBooleanExpression = 10; + public static readonly RULE_valueExpression = 11; + public static readonly RULE_comparison = 12; + public static readonly RULE_mathFn = 13; + public static readonly RULE_mathEvalFn = 14; + public static readonly RULE_operatorExpression = 15; + public static readonly RULE_primaryExpression = 16; + public static readonly RULE_rowCommand = 17; + public static readonly RULE_fields = 18; + public static readonly RULE_field = 19; + public static readonly RULE_enrichFieldIdentifier = 20; + public static readonly RULE_userVariable = 21; + public static readonly RULE_fromCommand = 22; + public static readonly RULE_metadata = 23; + public static readonly RULE_evalCommand = 24; + public static readonly RULE_statsCommand = 25; + public static readonly RULE_sourceIdentifier = 26; + public static readonly RULE_enrichIdentifier = 27; + public static readonly RULE_functionExpressionArgument = 28; + public static readonly RULE_mathFunctionExpressionArgument = 29; + public static readonly RULE_qualifiedName = 30; + public static readonly RULE_qualifiedNames = 31; + public static readonly RULE_identifier = 32; + public static readonly RULE_mathFunctionIdentifier = 33; + public static readonly RULE_functionIdentifier = 34; + public static readonly RULE_constant = 35; + public static readonly RULE_numericValue = 36; + public static readonly RULE_limitCommand = 37; + public static readonly RULE_sortCommand = 38; + public static readonly RULE_orderExpression = 39; + public static readonly RULE_projectCommand = 40; + public static readonly RULE_keepCommand = 41; + public static readonly RULE_dropCommand = 42; + public static readonly RULE_renameVariable = 43; + public static readonly RULE_renameCommand = 44; + public static readonly RULE_renameClause = 45; + public static readonly RULE_dissectCommand = 46; + public static readonly RULE_grokCommand = 47; + public static readonly RULE_commandOptions = 48; + public static readonly RULE_commandOption = 49; + public static readonly RULE_booleanValue = 50; + public static readonly RULE_number = 51; + public static readonly RULE_decimalValue = 52; + public static readonly RULE_integerValue = 53; + public static readonly RULE_string = 54; + public static readonly RULE_comparisonOperator = 55; + public static readonly RULE_explainCommand = 56; + public static readonly RULE_subqueryExpression = 57; + public static readonly RULE_showCommand = 58; // tslint:disable:no-trailing-whitespace public static readonly ruleNames: string[] = [ - "singleStatement", "query", "sourceCommand", "processingCommand", "whereCommand", - "booleanExpression", "valueExpression", "comparison", "mathFn", "operatorExpression", - "primaryExpression", "rowCommand", "fields", "field", "userVariable", - "fromCommand", "evalCommand", "statsCommand", "sourceIdentifier", "functionExpressionArgument", - "qualifiedName", "qualifiedNames", "identifier", "functionIdentifier", - "constant", "limitCommand", "sortCommand", "orderExpression", "projectCommand", - "projectClause", "booleanValue", "number", "string", "comparisonOperator", - "explainCommand", "subqueryExpression", + "singleStatement", "query", "sourceCommand", "processingCommand", "enrichCommand", + "enrichWithClause", "mvExpandCommand", "whereCommand", "whereBooleanExpression", + "booleanExpression", "regexBooleanExpression", "valueExpression", "comparison", + "mathFn", "mathEvalFn", "operatorExpression", "primaryExpression", "rowCommand", + "fields", "field", "enrichFieldIdentifier", "userVariable", "fromCommand", + "metadata", "evalCommand", "statsCommand", "sourceIdentifier", "enrichIdentifier", + "functionExpressionArgument", "mathFunctionExpressionArgument", "qualifiedName", + "qualifiedNames", "identifier", "mathFunctionIdentifier", "functionIdentifier", + "constant", "numericValue", "limitCommand", "sortCommand", "orderExpression", + "projectCommand", "keepCommand", "dropCommand", "renameVariable", "renameCommand", + "renameClause", "dissectCommand", "grokCommand", "commandOptions", "commandOption", + "booleanValue", "number", "decimalValue", "integerValue", "string", "comparisonOperator", + "explainCommand", "subqueryExpression", "showCommand", ]; private static readonly _LITERAL_NAMES: Array = [ - undefined, "'eval'", "'explain'", "'from'", "'row'", "'stats'", "'where'", - "'sort'", "'limit'", "'project'", undefined, undefined, undefined, undefined, - undefined, undefined, undefined, "'by'", "'and'", undefined, undefined, - "'.'", "'('", "'['", "']'", "'not'", "'null'", "'or'", "')'", undefined, - undefined, "'+'", "'-'", "'*'", "'/'", "'%'", undefined, "'nulls'", + undefined, undefined, undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined, undefined, undefined, + "'by'", undefined, "'and'", undefined, undefined, "'.'", "'('", undefined, + "']'", undefined, undefined, undefined, undefined, undefined, undefined, + undefined, "'or'", "')'", "'_'", "'info'", "'functions'", undefined, undefined, + "'+'", "'-'", "'*'", "'/'", "'%'", "'10'", undefined, "'nulls'", ]; private static readonly _SYMBOLIC_NAMES: Array = [ - undefined, "EVAL", "EXPLAIN", "FROM", "ROW", "STATS", "WHERE", "SORT", - "LIMIT", "PROJECT", "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "PIPE", - "STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "AND", "ASSIGN", - "COMMA", "DOT", "LP", "OPENING_BRACKET", "CLOSING_BRACKET", "NOT", "NULL", - "OR", "RP", "BOOLEAN_VALUE", "COMPARISON_OPERATOR", "PLUS", "MINUS", "ASTERISK", - "SLASH", "PERCENT", "ORDERING", "NULLS_ORDERING", "NULLS_ORDERING_DIRECTION", - "UNARY_FUNCTION", "UNQUOTED_IDENTIFIER", "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", - "EXPR_MULTILINE_COMMENT", "EXPR_WS", "SRC_UNQUOTED_IDENTIFIER", "SRC_QUOTED_IDENTIFIER", - "SRC_LINE_COMMENT", "SRC_MULTILINE_COMMENT", "SRC_WS", + undefined, "DISSECT", "GROK", "EVAL", "EXPLAIN", "FROM", "ROW", "STATS", + "WHERE", "SORT", "MV_EXPAND", "LIMIT", "PROJECT", "DROP", "RENAME", "SHOW", + "ENRICH", "KEEP", "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "EXPLAIN_WS", + "EXPLAIN_LINE_COMMENT", "EXPLAIN_MULTILINE_COMMENT", "PIPE", "STRING", + "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "DATE_LITERAL", "AND", "ASSIGN", + "COMMA", "DOT", "LP", "OPENING_BRACKET", "CLOSING_BRACKET", "NOT", "LIKE", + "RLIKE", "IN", "IS", "AS", "NULL", "OR", "RP", "UNDERSCORE", "INFO", "FUNCTIONS", + "BOOLEAN_VALUE", "COMPARISON_OPERATOR", "PLUS", "MINUS", "ASTERISK", "SLASH", + "PERCENT", "TEN", "ORDERING", "NULLS_ORDERING", "NULLS_ORDERING_DIRECTION", + "MATH_FUNCTION", "UNARY_FUNCTION", "WHERE_FUNCTIONS", "UNQUOTED_IDENTIFIER", + "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", "EXPR_MULTILINE_COMMENT", "EXPR_WS", + "METADATA", "SRC_UNQUOTED_IDENTIFIER", "SRC_QUOTED_IDENTIFIER", "SRC_LINE_COMMENT", + "SRC_MULTILINE_COMMENT", "SRC_WS", "ON", "WITH", "ENR_UNQUOTED_IDENTIFIER", + "ENR_QUOTED_IDENTIFIER", "ENR_LINE_COMMENT", "ENR_MULTILINE_COMMENT", + "ENR_WS", "EXPLAIN_PIPE", ]; public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(esql_parser._LITERAL_NAMES, esql_parser._SYMBOLIC_NAMES, []); @@ -171,9 +240,9 @@ export class esql_parser extends Parser { try { this.enterOuterAlt(_localctx, 1); { - this.state = 72; + this.state = 118; this.query(0); - this.state = 73; + this.state = 119; this.match(esql_parser.EOF); } } @@ -215,11 +284,11 @@ export class esql_parser extends Parser { this._ctx = _localctx; _prevctx = _localctx; - this.state = 76; + this.state = 122; this.sourceCommand(); } this._ctx._stop = this._input.tryLT(-1); - this.state = 83; + this.state = 129; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 0, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -232,18 +301,18 @@ export class esql_parser extends Parser { { _localctx = new CompositeQueryContext(new QueryContext(_parentctx, _parentState)); this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_query); - this.state = 78; + this.state = 124; if (!(this.precpred(this._ctx, 1))) { throw new FailedPredicateException(this, "this.precpred(this._ctx, 1)"); } - this.state = 79; + this.state = 125; this.match(esql_parser.PIPE); - this.state = 80; + this.state = 126; this.processingCommand(); } } } - this.state = 85; + this.state = 131; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 0, this._ctx); } @@ -268,30 +337,37 @@ export class esql_parser extends Parser { let _localctx: SourceCommandContext = new SourceCommandContext(this._ctx, this.state); this.enterRule(_localctx, 4, esql_parser.RULE_sourceCommand); try { - this.state = 89; + this.state = 136; this._errHandler.sync(this); switch (this._input.LA(1)) { case esql_parser.EXPLAIN: this.enterOuterAlt(_localctx, 1); { - this.state = 86; + this.state = 132; this.explainCommand(); } break; case esql_parser.FROM: this.enterOuterAlt(_localctx, 2); { - this.state = 87; + this.state = 133; this.fromCommand(); } break; case esql_parser.ROW: this.enterOuterAlt(_localctx, 3); { - this.state = 88; + this.state = 134; this.rowCommand(); } break; + case esql_parser.SHOW: + this.enterOuterAlt(_localctx, 4); + { + this.state = 135; + this.showCommand(); + } + break; default: throw new NoViableAltException(this); } @@ -315,51 +391,100 @@ export class esql_parser extends Parser { let _localctx: ProcessingCommandContext = new ProcessingCommandContext(this._ctx, this.state); this.enterRule(_localctx, 6, esql_parser.RULE_processingCommand); try { - this.state = 97; + this.state = 151; this._errHandler.sync(this); switch (this._input.LA(1)) { case esql_parser.EVAL: this.enterOuterAlt(_localctx, 1); { - this.state = 91; + this.state = 138; this.evalCommand(); } break; case esql_parser.LIMIT: this.enterOuterAlt(_localctx, 2); { - this.state = 92; + this.state = 139; this.limitCommand(); } break; case esql_parser.PROJECT: this.enterOuterAlt(_localctx, 3); { - this.state = 93; + this.state = 140; this.projectCommand(); } break; - case esql_parser.SORT: + case esql_parser.KEEP: this.enterOuterAlt(_localctx, 4); { - this.state = 94; + this.state = 141; + this.keepCommand(); + } + break; + case esql_parser.RENAME: + this.enterOuterAlt(_localctx, 5); + { + this.state = 142; + this.renameCommand(); + } + break; + case esql_parser.DROP: + this.enterOuterAlt(_localctx, 6); + { + this.state = 143; + this.dropCommand(); + } + break; + case esql_parser.DISSECT: + this.enterOuterAlt(_localctx, 7); + { + this.state = 144; + this.dissectCommand(); + } + break; + case esql_parser.GROK: + this.enterOuterAlt(_localctx, 8); + { + this.state = 145; + this.grokCommand(); + } + break; + case esql_parser.SORT: + this.enterOuterAlt(_localctx, 9); + { + this.state = 146; this.sortCommand(); } break; case esql_parser.STATS: - this.enterOuterAlt(_localctx, 5); + this.enterOuterAlt(_localctx, 10); { - this.state = 95; + this.state = 147; this.statsCommand(); } break; case esql_parser.WHERE: - this.enterOuterAlt(_localctx, 6); + this.enterOuterAlt(_localctx, 11); { - this.state = 96; + this.state = 148; this.whereCommand(); } break; + case esql_parser.MV_EXPAND: + this.enterOuterAlt(_localctx, 12); + { + this.state = 149; + this.mvExpandCommand(); + } + break; + case esql_parser.ENRICH: + this.enterOuterAlt(_localctx, 13); + { + this.state = 150; + this.enrichCommand(); + } + break; default: throw new NoViableAltException(this); } @@ -379,132 +504,58 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public whereCommand(): WhereCommandContext { - let _localctx: WhereCommandContext = new WhereCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 8, esql_parser.RULE_whereCommand); - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 99; - this.match(esql_parser.WHERE); - this.state = 100; - this.booleanExpression(0); - } - } - catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } - finally { - this.exitRule(); - } - return _localctx; - } - - public booleanExpression(): BooleanExpressionContext; - public booleanExpression(_p: number): BooleanExpressionContext; - // @RuleVersion(0) - public booleanExpression(_p?: number): BooleanExpressionContext { - if (_p === undefined) { - _p = 0; - } - - let _parentctx: ParserRuleContext = this._ctx; - let _parentState: number = this.state; - let _localctx: BooleanExpressionContext = new BooleanExpressionContext(this._ctx, _parentState); - let _prevctx: BooleanExpressionContext = _localctx; - let _startState: number = 10; - this.enterRecursionRule(_localctx, 10, esql_parser.RULE_booleanExpression, _p); + public enrichCommand(): EnrichCommandContext { + let _localctx: EnrichCommandContext = new EnrichCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 8, esql_parser.RULE_enrichCommand); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 106; + this.state = 153; + this.match(esql_parser.ENRICH); + this.state = 154; + _localctx._policyName = this.enrichIdentifier(); + this.state = 157; this._errHandler.sync(this); - switch (this._input.LA(1)) { - case esql_parser.NOT: - { - this.state = 103; - this.match(esql_parser.NOT); - this.state = 104; - this.booleanExpression(4); - } - break; - case esql_parser.STRING: - case esql_parser.INTEGER_LITERAL: - case esql_parser.DECIMAL_LITERAL: - case esql_parser.LP: - case esql_parser.NULL: - case esql_parser.BOOLEAN_VALUE: - case esql_parser.PLUS: - case esql_parser.MINUS: - case esql_parser.UNARY_FUNCTION: - case esql_parser.UNQUOTED_IDENTIFIER: - case esql_parser.QUOTED_IDENTIFIER: + switch ( this.interpreter.adaptivePredict(this._input, 3, this._ctx) ) { + case 1: { - this.state = 105; - this.valueExpression(); + this.state = 155; + this.match(esql_parser.ON); + this.state = 156; + _localctx._matchField = this.enrichFieldIdentifier(); } break; - default: - throw new NoViableAltException(this); } - this._ctx._stop = this._input.tryLT(-1); - this.state = 116; + this.state = 168; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 5, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - if (this._parseListeners != null) { - this.triggerExitRuleEvent(); - } - _prevctx = _localctx; - { - this.state = 114; - this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 4, this._ctx) ) { - case 1: + switch ( this.interpreter.adaptivePredict(this._input, 5, this._ctx) ) { + case 1: + { + this.state = 159; + this.match(esql_parser.WITH); + this.state = 160; + this.enrichWithClause(); + this.state = 165; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 4, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { { - _localctx = new BooleanExpressionContext(_parentctx, _parentState); - _localctx._left = _prevctx; - this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_booleanExpression); - this.state = 108; - if (!(this.precpred(this._ctx, 2))) { - throw new FailedPredicateException(this, "this.precpred(this._ctx, 2)"); - } - this.state = 109; - _localctx._operator = this.match(esql_parser.AND); - this.state = 110; - _localctx._right = this.booleanExpression(3); - } - break; - - case 2: { - _localctx = new BooleanExpressionContext(_parentctx, _parentState); - _localctx._left = _prevctx; - this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_booleanExpression); - this.state = 111; - if (!(this.precpred(this._ctx, 1))) { - throw new FailedPredicateException(this, "this.precpred(this._ctx, 1)"); + this.state = 161; + this.match(esql_parser.COMMA); + this.state = 162; + this.enrichWithClause(); } - this.state = 112; - _localctx._operator = this.match(esql_parser.OR); - this.state = 113; - _localctx._right = this.booleanExpression(2); } - break; - } } + this.state = 167; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 4, this._ctx); } - this.state = 118; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 5, this._ctx); + } + break; } } } @@ -518,34 +569,32 @@ export class esql_parser extends Parser { } } finally { - this.unrollRecursionContexts(_parentctx); + this.exitRule(); } return _localctx; } // @RuleVersion(0) - public valueExpression(): ValueExpressionContext { - let _localctx: ValueExpressionContext = new ValueExpressionContext(this._ctx, this.state); - this.enterRule(_localctx, 12, esql_parser.RULE_valueExpression); + public enrichWithClause(): EnrichWithClauseContext { + let _localctx: EnrichWithClauseContext = new EnrichWithClauseContext(this._ctx, this.state); + this.enterRule(_localctx, 10, esql_parser.RULE_enrichWithClause); try { - this.state = 121; + this.enterOuterAlt(_localctx, 1); + { + this.state = 173; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 6, this._ctx) ) { case 1: - this.enterOuterAlt(_localctx, 1); { - this.state = 119; - this.operatorExpression(0); - } - break; - - case 2: - this.enterOuterAlt(_localctx, 2); - { - this.state = 120; - this.comparison(); + this.state = 170; + _localctx._newName = this.enrichFieldIdentifier(); + this.state = 171; + this.match(esql_parser.ASSIGN); } break; } + this.state = 175; + _localctx._enrichField = this.enrichFieldIdentifier(); + } } catch (re) { if (re instanceof RecognitionException) { @@ -562,18 +611,16 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public comparison(): ComparisonContext { - let _localctx: ComparisonContext = new ComparisonContext(this._ctx, this.state); - this.enterRule(_localctx, 14, esql_parser.RULE_comparison); + public mvExpandCommand(): MvExpandCommandContext { + let _localctx: MvExpandCommandContext = new MvExpandCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 12, esql_parser.RULE_mvExpandCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 123; - _localctx._left = this.operatorExpression(0); - this.state = 124; - this.comparisonOperator(); - this.state = 125; - _localctx._right = this.operatorExpression(0); + this.state = 177; + this.match(esql_parser.MV_EXPAND); + this.state = 178; + this.qualifiedNames(); } } catch (re) { @@ -591,45 +638,16 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public mathFn(): MathFnContext { - let _localctx: MathFnContext = new MathFnContext(this._ctx, this.state); - this.enterRule(_localctx, 16, esql_parser.RULE_mathFn); - let _la: number; + public whereCommand(): WhereCommandContext { + let _localctx: WhereCommandContext = new WhereCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 14, esql_parser.RULE_whereCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 127; - this.functionIdentifier(); - this.state = 128; - this.match(esql_parser.LP); - this.state = 137; - this._errHandler.sync(this); - _la = this._input.LA(1); - if (((((_la - 14)) & ~0x1F) === 0 && ((1 << (_la - 14)) & ((1 << (esql_parser.STRING - 14)) | (1 << (esql_parser.UNQUOTED_IDENTIFIER - 14)) | (1 << (esql_parser.QUOTED_IDENTIFIER - 14)))) !== 0)) { - { - this.state = 129; - this.functionExpressionArgument(); - this.state = 134; - this._errHandler.sync(this); - _la = this._input.LA(1); - while (_la === esql_parser.COMMA) { - { - { - this.state = 130; - this.match(esql_parser.COMMA); - this.state = 131; - this.functionExpressionArgument(); - } - } - this.state = 136; - this._errHandler.sync(this); - _la = this._input.LA(1); - } - } - } - - this.state = 139; - this.match(esql_parser.RP); + this.state = 180; + this.match(esql_parser.WHERE); + this.state = 181; + this.whereBooleanExpression(0); } } catch (re) { @@ -647,74 +665,164 @@ export class esql_parser extends Parser { return _localctx; } - public operatorExpression(): OperatorExpressionContext; - public operatorExpression(_p: number): OperatorExpressionContext; + public whereBooleanExpression(): WhereBooleanExpressionContext; + public whereBooleanExpression(_p: number): WhereBooleanExpressionContext; // @RuleVersion(0) - public operatorExpression(_p?: number): OperatorExpressionContext { + public whereBooleanExpression(_p?: number): WhereBooleanExpressionContext { if (_p === undefined) { _p = 0; } let _parentctx: ParserRuleContext = this._ctx; let _parentState: number = this.state; - let _localctx: OperatorExpressionContext = new OperatorExpressionContext(this._ctx, _parentState); - let _prevctx: OperatorExpressionContext = _localctx; - let _startState: number = 18; - this.enterRecursionRule(_localctx, 18, esql_parser.RULE_operatorExpression, _p); + let _localctx: WhereBooleanExpressionContext = new WhereBooleanExpressionContext(this._ctx, _parentState); + let _prevctx: WhereBooleanExpressionContext = _localctx; + let _startState: number = 16; + this.enterRecursionRule(_localctx, 16, esql_parser.RULE_whereBooleanExpression, _p); let _la: number; try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 146; + this.state = 228; this._errHandler.sync(this); - switch (this._input.LA(1)) { - case esql_parser.STRING: - case esql_parser.INTEGER_LITERAL: - case esql_parser.DECIMAL_LITERAL: - case esql_parser.LP: - case esql_parser.NULL: - case esql_parser.BOOLEAN_VALUE: - case esql_parser.UNQUOTED_IDENTIFIER: - case esql_parser.QUOTED_IDENTIFIER: + switch ( this.interpreter.adaptivePredict(this._input, 13, this._ctx) ) { + case 1: { - this.state = 142; - this.primaryExpression(); + this.state = 184; + this.match(esql_parser.NOT); + this.state = 185; + this.whereBooleanExpression(8); } break; - case esql_parser.UNARY_FUNCTION: + + case 2: { - this.state = 143; - this.mathFn(); + this.state = 186; + this.valueExpression(); } break; - case esql_parser.PLUS: - case esql_parser.MINUS: + + case 3: { - this.state = 144; - _localctx._operator = this._input.LT(1); + this.state = 187; + this.regexBooleanExpression(); + } + break; + + case 4: + { + this.state = 188; + this.valueExpression(); + this.state = 190; + this._errHandler.sync(this); _la = this._input.LA(1); - if (!(_la === esql_parser.PLUS || _la === esql_parser.MINUS)) { - _localctx._operator = this._errHandler.recoverInline(this); - } else { - if (this._input.LA(1) === Token.EOF) { - this.matchedEOF = true; + if (_la === esql_parser.NOT) { + { + this.state = 189; + this.match(esql_parser.NOT); } + } - this._errHandler.reportMatch(this); - this.consume(); + this.state = 192; + this.match(esql_parser.IN); + this.state = 193; + this.match(esql_parser.LP); + this.state = 194; + this.valueExpression(); + this.state = 199; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { + { + { + this.state = 195; + this.match(esql_parser.COMMA); + this.state = 196; + this.valueExpression(); + } + } + this.state = 201; + this._errHandler.sync(this); + _la = this._input.LA(1); } - this.state = 145; - this.operatorExpression(3); + this.state = 202; + this.match(esql_parser.RP); + } + break; + + case 5: + { + this.state = 205; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la === esql_parser.NOT) { + { + this.state = 204; + this.match(esql_parser.NOT); + } + } + + this.state = 207; + this.match(esql_parser.WHERE_FUNCTIONS); + this.state = 208; + this.match(esql_parser.LP); + this.state = 209; + this.qualifiedName(); + this.state = 217; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 11, this._ctx) ) { + case 1: + { + this.state = 214; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { + { + { + this.state = 210; + this.match(esql_parser.COMMA); + this.state = 211; + this.functionExpressionArgument(); + } + } + this.state = 216; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + break; + } + this.state = 219; + this.match(esql_parser.RP); + } + break; + + case 6: + { + this.state = 221; + this.valueExpression(); + this.state = 222; + this.match(esql_parser.IS); + this.state = 224; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la === esql_parser.NOT) { + { + this.state = 223; + this.match(esql_parser.NOT); + } + } + + this.state = 226; + this.match(esql_parser.NULL); } break; - default: - throw new NoViableAltException(this); } this._ctx._stop = this._input.tryLT(-1); - this.state = 156; + this.state = 238; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 11, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 15, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { if (this._parseListeners != null) { @@ -722,68 +830,166 @@ export class esql_parser extends Parser { } _prevctx = _localctx; { - this.state = 154; + this.state = 236; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 10, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 14, this._ctx) ) { case 1: { - _localctx = new OperatorExpressionContext(_parentctx, _parentState); + _localctx = new WhereBooleanExpressionContext(_parentctx, _parentState); _localctx._left = _prevctx; - this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_operatorExpression); - this.state = 148; - if (!(this.precpred(this._ctx, 2))) { - throw new FailedPredicateException(this, "this.precpred(this._ctx, 2)"); + this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_whereBooleanExpression); + this.state = 230; + if (!(this.precpred(this._ctx, 5))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 5)"); } - this.state = 149; - _localctx._operator = this._input.LT(1); - _la = this._input.LA(1); - if (!(((((_la - 33)) & ~0x1F) === 0 && ((1 << (_la - 33)) & ((1 << (esql_parser.ASTERISK - 33)) | (1 << (esql_parser.SLASH - 33)) | (1 << (esql_parser.PERCENT - 33)))) !== 0))) { - _localctx._operator = this._errHandler.recoverInline(this); - } else { - if (this._input.LA(1) === Token.EOF) { - this.matchedEOF = true; - } + this.state = 231; + _localctx._operator = this.match(esql_parser.AND); + this.state = 232; + _localctx._right = this.whereBooleanExpression(6); + } + break; - this._errHandler.reportMatch(this); - this.consume(); + case 2: + { + _localctx = new WhereBooleanExpressionContext(_parentctx, _parentState); + _localctx._left = _prevctx; + this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_whereBooleanExpression); + this.state = 233; + if (!(this.precpred(this._ctx, 4))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 4)"); } - this.state = 150; - _localctx._right = this.operatorExpression(3); + this.state = 234; + _localctx._operator = this.match(esql_parser.OR); + this.state = 235; + _localctx._right = this.whereBooleanExpression(5); + } + break; + } + } + } + this.state = 240; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 15, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.unrollRecursionContexts(_parentctx); + } + return _localctx; + } + + public booleanExpression(): BooleanExpressionContext; + public booleanExpression(_p: number): BooleanExpressionContext; + // @RuleVersion(0) + public booleanExpression(_p?: number): BooleanExpressionContext { + if (_p === undefined) { + _p = 0; + } + + let _parentctx: ParserRuleContext = this._ctx; + let _parentState: number = this.state; + let _localctx: BooleanExpressionContext = new BooleanExpressionContext(this._ctx, _parentState); + let _prevctx: BooleanExpressionContext = _localctx; + let _startState: number = 18; + this.enterRecursionRule(_localctx, 18, esql_parser.RULE_booleanExpression, _p); + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 245; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case esql_parser.NOT: + { + this.state = 242; + this.match(esql_parser.NOT); + this.state = 243; + this.booleanExpression(4); + } + break; + case esql_parser.STRING: + case esql_parser.INTEGER_LITERAL: + case esql_parser.DECIMAL_LITERAL: + case esql_parser.LP: + case esql_parser.OPENING_BRACKET: + case esql_parser.NULL: + case esql_parser.BOOLEAN_VALUE: + case esql_parser.PLUS: + case esql_parser.MINUS: + case esql_parser.ASTERISK: + case esql_parser.MATH_FUNCTION: + case esql_parser.UNARY_FUNCTION: + case esql_parser.UNQUOTED_IDENTIFIER: + case esql_parser.QUOTED_IDENTIFIER: + { + this.state = 244; + this.valueExpression(); + } + break; + default: + throw new NoViableAltException(this); + } + this._ctx._stop = this._input.tryLT(-1); + this.state = 255; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 18, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + if (this._parseListeners != null) { + this.triggerExitRuleEvent(); + } + _prevctx = _localctx; + { + this.state = 253; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 17, this._ctx) ) { + case 1: + { + _localctx = new BooleanExpressionContext(_parentctx, _parentState); + _localctx._left = _prevctx; + this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_booleanExpression); + this.state = 247; + if (!(this.precpred(this._ctx, 2))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 2)"); + } + this.state = 248; + _localctx._operator = this.match(esql_parser.AND); + this.state = 249; + _localctx._right = this.booleanExpression(3); } break; case 2: { - _localctx = new OperatorExpressionContext(_parentctx, _parentState); + _localctx = new BooleanExpressionContext(_parentctx, _parentState); _localctx._left = _prevctx; - this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_operatorExpression); - this.state = 151; + this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_booleanExpression); + this.state = 250; if (!(this.precpred(this._ctx, 1))) { throw new FailedPredicateException(this, "this.precpred(this._ctx, 1)"); } - this.state = 152; - _localctx._operator = this._input.LT(1); - _la = this._input.LA(1); - if (!(_la === esql_parser.PLUS || _la === esql_parser.MINUS)) { - _localctx._operator = this._errHandler.recoverInline(this); - } else { - if (this._input.LA(1) === Token.EOF) { - this.matchedEOF = true; - } - - this._errHandler.reportMatch(this); - this.consume(); - } - this.state = 153; - _localctx._right = this.operatorExpression(2); + this.state = 251; + _localctx._operator = this.match(esql_parser.OR); + this.state = 252; + _localctx._right = this.booleanExpression(2); } break; } } } - this.state = 158; + this.state = 257; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 11, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 18, this._ctx); } } } @@ -802,77 +1008,55 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public primaryExpression(): PrimaryExpressionContext { - let _localctx: PrimaryExpressionContext = new PrimaryExpressionContext(this._ctx, this.state); - this.enterRule(_localctx, 20, esql_parser.RULE_primaryExpression); + public regexBooleanExpression(): RegexBooleanExpressionContext { + let _localctx: RegexBooleanExpressionContext = new RegexBooleanExpressionContext(this._ctx, this.state); + this.enterRule(_localctx, 20, esql_parser.RULE_regexBooleanExpression); let _la: number; try { - this.state = 179; + this.state = 272; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 14, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 21, this._ctx) ) { case 1: this.enterOuterAlt(_localctx, 1); { - this.state = 159; - this.constant(); - } - break; - - case 2: - this.enterOuterAlt(_localctx, 2); - { - this.state = 160; - this.qualifiedName(); + this.state = 258; + this.valueExpression(); + this.state = 260; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la === esql_parser.NOT) { + { + this.state = 259; + this.match(esql_parser.NOT); + } } - break; - case 3: - this.enterOuterAlt(_localctx, 3); - { - this.state = 161; - this.match(esql_parser.LP); - this.state = 162; - this.booleanExpression(0); - this.state = 163; - this.match(esql_parser.RP); + this.state = 262; + _localctx._kind = this.match(esql_parser.LIKE); + this.state = 263; + _localctx._pattern = this.string(); } break; - case 4: - this.enterOuterAlt(_localctx, 4); + case 2: + this.enterOuterAlt(_localctx, 2); { - this.state = 165; - this.identifier(); - this.state = 166; - this.match(esql_parser.LP); - this.state = 175; + this.state = 265; + this.valueExpression(); + this.state = 267; this._errHandler.sync(this); _la = this._input.LA(1); - if (((((_la - 14)) & ~0x1F) === 0 && ((1 << (_la - 14)) & ((1 << (esql_parser.STRING - 14)) | (1 << (esql_parser.INTEGER_LITERAL - 14)) | (1 << (esql_parser.DECIMAL_LITERAL - 14)) | (1 << (esql_parser.LP - 14)) | (1 << (esql_parser.NOT - 14)) | (1 << (esql_parser.NULL - 14)) | (1 << (esql_parser.BOOLEAN_VALUE - 14)) | (1 << (esql_parser.PLUS - 14)) | (1 << (esql_parser.MINUS - 14)) | (1 << (esql_parser.UNARY_FUNCTION - 14)) | (1 << (esql_parser.UNQUOTED_IDENTIFIER - 14)) | (1 << (esql_parser.QUOTED_IDENTIFIER - 14)))) !== 0)) { + if (_la === esql_parser.NOT) { { - this.state = 167; - this.booleanExpression(0); - this.state = 172; - this._errHandler.sync(this); - _la = this._input.LA(1); - while (_la === esql_parser.COMMA) { - { - { - this.state = 168; - this.match(esql_parser.COMMA); - this.state = 169; - this.booleanExpression(0); - } - } - this.state = 174; - this._errHandler.sync(this); - _la = this._input.LA(1); - } + this.state = 266; + this.match(esql_parser.NOT); } } - this.state = 177; - this.match(esql_parser.RP); + this.state = 269; + _localctx._kind = this.match(esql_parser.RLIKE); + this.state = 270; + _localctx._pattern = this.string(); } break; } @@ -892,16 +1076,28 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public rowCommand(): RowCommandContext { - let _localctx: RowCommandContext = new RowCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 22, esql_parser.RULE_rowCommand); + public valueExpression(): ValueExpressionContext { + let _localctx: ValueExpressionContext = new ValueExpressionContext(this._ctx, this.state); + this.enterRule(_localctx, 22, esql_parser.RULE_valueExpression); try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 181; - this.match(esql_parser.ROW); - this.state = 182; - this.fields(); + this.state = 276; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 22, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 274; + this.operatorExpression(0); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 275; + this.comparison(); + } + break; } } catch (re) { @@ -919,33 +1115,18 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public fields(): FieldsContext { - let _localctx: FieldsContext = new FieldsContext(this._ctx, this.state); - this.enterRule(_localctx, 24, esql_parser.RULE_fields); + public comparison(): ComparisonContext { + let _localctx: ComparisonContext = new ComparisonContext(this._ctx, this.state); + this.enterRule(_localctx, 24, esql_parser.RULE_comparison); try { - let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 184; - this.field(); - this.state = 189; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 15, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - { - { - this.state = 185; - this.match(esql_parser.COMMA); - this.state = 186; - this.field(); - } - } - } - this.state = 191; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 15, this._ctx); - } + this.state = 278; + _localctx._left = this.operatorExpression(0); + this.state = 279; + this.comparisonOperator(); + this.state = 280; + _localctx._right = this.operatorExpression(0); } } catch (re) { @@ -963,57 +1144,45 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public field(): FieldContext { - let _localctx: FieldContext = new FieldContext(this._ctx, this.state); - this.enterRule(_localctx, 26, esql_parser.RULE_field); + public mathFn(): MathFnContext { + let _localctx: MathFnContext = new MathFnContext(this._ctx, this.state); + this.enterRule(_localctx, 26, esql_parser.RULE_mathFn); + let _la: number; try { - this.state = 197; + this.enterOuterAlt(_localctx, 1); + { + this.state = 282; + this.functionIdentifier(); + this.state = 283; + this.match(esql_parser.LP); + this.state = 292; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 16, this._ctx) ) { - case 1: - this.enterOuterAlt(_localctx, 1); + _la = this._input.LA(1); + if ((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << esql_parser.STRING) | (1 << esql_parser.INTEGER_LITERAL) | (1 << esql_parser.DECIMAL_LITERAL))) !== 0) || ((((_la - 53)) & ~0x1F) === 0 && ((1 << (_la - 53)) & ((1 << (esql_parser.ASTERISK - 53)) | (1 << (esql_parser.UNQUOTED_IDENTIFIER - 53)) | (1 << (esql_parser.QUOTED_IDENTIFIER - 53)))) !== 0)) { { - this.state = 192; - this.booleanExpression(0); + this.state = 284; + this.functionExpressionArgument(); + this.state = 289; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { + { + { + this.state = 285; + this.match(esql_parser.COMMA); + this.state = 286; + this.functionExpressionArgument(); + } + } + this.state = 291; + this._errHandler.sync(this); + _la = this._input.LA(1); } - break; - - case 2: - this.enterOuterAlt(_localctx, 2); - { - this.state = 193; - this.userVariable(); - this.state = 194; - this.match(esql_parser.ASSIGN); - this.state = 195; - this.booleanExpression(0); } - break; - } - } - catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; } - } - finally { - this.exitRule(); - } - return _localctx; - } - // @RuleVersion(0) - public userVariable(): UserVariableContext { - let _localctx: UserVariableContext = new UserVariableContext(this._ctx, this.state); - this.enterRule(_localctx, 28, esql_parser.RULE_userVariable); - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 199; - this.identifier(); + + this.state = 294; + this.match(esql_parser.RP); } } catch (re) { @@ -1031,35 +1200,45 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public fromCommand(): FromCommandContext { - let _localctx: FromCommandContext = new FromCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 30, esql_parser.RULE_fromCommand); + public mathEvalFn(): MathEvalFnContext { + let _localctx: MathEvalFnContext = new MathEvalFnContext(this._ctx, this.state); + this.enterRule(_localctx, 28, esql_parser.RULE_mathEvalFn); + let _la: number; try { - let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 201; - this.match(esql_parser.FROM); - this.state = 202; - this.sourceIdentifier(); - this.state = 207; + this.state = 296; + this.mathFunctionIdentifier(); + this.state = 297; + this.match(esql_parser.LP); + this.state = 306; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 17, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { + _la = this._input.LA(1); + if ((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << esql_parser.STRING) | (1 << esql_parser.INTEGER_LITERAL) | (1 << esql_parser.DECIMAL_LITERAL))) !== 0) || ((((_la - 34)) & ~0x1F) === 0 && ((1 << (_la - 34)) & ((1 << (esql_parser.LP - 34)) | (1 << (esql_parser.OPENING_BRACKET - 34)) | (1 << (esql_parser.NULL - 34)) | (1 << (esql_parser.BOOLEAN_VALUE - 34)) | (1 << (esql_parser.PLUS - 34)) | (1 << (esql_parser.MINUS - 34)) | (1 << (esql_parser.ASTERISK - 34)) | (1 << (esql_parser.MATH_FUNCTION - 34)) | (1 << (esql_parser.UNARY_FUNCTION - 34)) | (1 << (esql_parser.UNQUOTED_IDENTIFIER - 34)) | (1 << (esql_parser.QUOTED_IDENTIFIER - 34)))) !== 0)) { + { + this.state = 298; + this.mathFunctionExpressionArgument(); + this.state = 303; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { { { - this.state = 203; + this.state = 299; this.match(esql_parser.COMMA); - this.state = 204; - this.sourceIdentifier(); + this.state = 300; + this.mathFunctionExpressionArgument(); } } + this.state = 305; + this._errHandler.sync(this); + _la = this._input.LA(1); + } } - this.state = 209; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 17, this._ctx); } + + this.state = 308; + this.match(esql_parser.RP); } } catch (re) { @@ -1076,55 +1255,152 @@ export class esql_parser extends Parser { } return _localctx; } + + public operatorExpression(): OperatorExpressionContext; + public operatorExpression(_p: number): OperatorExpressionContext; // @RuleVersion(0) - public evalCommand(): EvalCommandContext { - let _localctx: EvalCommandContext = new EvalCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 32, esql_parser.RULE_evalCommand); - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 210; - this.match(esql_parser.EVAL); - this.state = 211; - this.fields(); - } - } - catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } - finally { - this.exitRule(); + public operatorExpression(_p?: number): OperatorExpressionContext { + if (_p === undefined) { + _p = 0; } - return _localctx; - } - // @RuleVersion(0) - public statsCommand(): StatsCommandContext { - let _localctx: StatsCommandContext = new StatsCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 34, esql_parser.RULE_statsCommand); + + let _parentctx: ParserRuleContext = this._ctx; + let _parentState: number = this.state; + let _localctx: OperatorExpressionContext = new OperatorExpressionContext(this._ctx, _parentState); + let _prevctx: OperatorExpressionContext = _localctx; + let _startState: number = 30; + this.enterRecursionRule(_localctx, 30, esql_parser.RULE_operatorExpression, _p); + let _la: number; try { + let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 213; - this.match(esql_parser.STATS); - this.state = 214; - this.fields(); - this.state = 217; + this.state = 316; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 18, this._ctx) ) { - case 1: + switch (this._input.LA(1)) { + case esql_parser.STRING: + case esql_parser.INTEGER_LITERAL: + case esql_parser.DECIMAL_LITERAL: + case esql_parser.LP: + case esql_parser.OPENING_BRACKET: + case esql_parser.NULL: + case esql_parser.BOOLEAN_VALUE: + case esql_parser.ASTERISK: + case esql_parser.UNQUOTED_IDENTIFIER: + case esql_parser.QUOTED_IDENTIFIER: { - this.state = 215; - this.match(esql_parser.BY); - this.state = 216; - this.qualifiedNames(); + this.state = 311; + this.primaryExpression(); + } + break; + case esql_parser.UNARY_FUNCTION: + { + this.state = 312; + this.mathFn(); } break; + case esql_parser.MATH_FUNCTION: + { + this.state = 313; + this.mathEvalFn(); + } + break; + case esql_parser.PLUS: + case esql_parser.MINUS: + { + this.state = 314; + _localctx._operator = this._input.LT(1); + _la = this._input.LA(1); + if (!(_la === esql_parser.PLUS || _la === esql_parser.MINUS)) { + _localctx._operator = this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 315; + this.operatorExpression(3); + } + break; + default: + throw new NoViableAltException(this); + } + this._ctx._stop = this._input.tryLT(-1); + this.state = 326; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 29, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + if (this._parseListeners != null) { + this.triggerExitRuleEvent(); + } + _prevctx = _localctx; + { + this.state = 324; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 28, this._ctx) ) { + case 1: + { + _localctx = new OperatorExpressionContext(_parentctx, _parentState); + _localctx._left = _prevctx; + this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_operatorExpression); + this.state = 318; + if (!(this.precpred(this._ctx, 2))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 2)"); + } + this.state = 319; + _localctx._operator = this._input.LT(1); + _la = this._input.LA(1); + if (!(((((_la - 53)) & ~0x1F) === 0 && ((1 << (_la - 53)) & ((1 << (esql_parser.ASTERISK - 53)) | (1 << (esql_parser.SLASH - 53)) | (1 << (esql_parser.PERCENT - 53)))) !== 0))) { + _localctx._operator = this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 320; + _localctx._right = this.operatorExpression(3); + } + break; + + case 2: + { + _localctx = new OperatorExpressionContext(_parentctx, _parentState); + _localctx._left = _prevctx; + this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_operatorExpression); + this.state = 321; + if (!(this.precpred(this._ctx, 1))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 1)"); + } + this.state = 322; + _localctx._operator = this._input.LT(1); + _la = this._input.LA(1); + if (!(_la === esql_parser.PLUS || _la === esql_parser.MINUS)) { + _localctx._operator = this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 323; + _localctx._right = this.operatorExpression(2); + } + break; + } + } + } + this.state = 328; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 29, this._ctx); } } } @@ -1138,30 +1414,84 @@ export class esql_parser extends Parser { } } finally { - this.exitRule(); + this.unrollRecursionContexts(_parentctx); } return _localctx; } // @RuleVersion(0) - public sourceIdentifier(): SourceIdentifierContext { - let _localctx: SourceIdentifierContext = new SourceIdentifierContext(this._ctx, this.state); - this.enterRule(_localctx, 36, esql_parser.RULE_sourceIdentifier); + public primaryExpression(): PrimaryExpressionContext { + let _localctx: PrimaryExpressionContext = new PrimaryExpressionContext(this._ctx, this.state); + this.enterRule(_localctx, 32, esql_parser.RULE_primaryExpression); let _la: number; try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 219; - _la = this._input.LA(1); - if (!(_la === esql_parser.SRC_UNQUOTED_IDENTIFIER || _la === esql_parser.SRC_QUOTED_IDENTIFIER)) { - this._errHandler.recoverInline(this); - } else { - if (this._input.LA(1) === Token.EOF) { - this.matchedEOF = true; + this.state = 349; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 32, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 329; + this.constant(); } + break; - this._errHandler.reportMatch(this); - this.consume(); - } + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 330; + this.qualifiedName(); + } + break; + + case 3: + this.enterOuterAlt(_localctx, 3); + { + this.state = 331; + this.match(esql_parser.LP); + this.state = 332; + this.booleanExpression(0); + this.state = 333; + this.match(esql_parser.RP); + } + break; + + case 4: + this.enterOuterAlt(_localctx, 4); + { + this.state = 335; + this.identifier(); + this.state = 336; + this.match(esql_parser.LP); + this.state = 345; + this._errHandler.sync(this); + _la = this._input.LA(1); + if ((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << esql_parser.STRING) | (1 << esql_parser.INTEGER_LITERAL) | (1 << esql_parser.DECIMAL_LITERAL))) !== 0) || ((((_la - 34)) & ~0x1F) === 0 && ((1 << (_la - 34)) & ((1 << (esql_parser.LP - 34)) | (1 << (esql_parser.OPENING_BRACKET - 34)) | (1 << (esql_parser.NOT - 34)) | (1 << (esql_parser.NULL - 34)) | (1 << (esql_parser.BOOLEAN_VALUE - 34)) | (1 << (esql_parser.PLUS - 34)) | (1 << (esql_parser.MINUS - 34)) | (1 << (esql_parser.ASTERISK - 34)) | (1 << (esql_parser.MATH_FUNCTION - 34)) | (1 << (esql_parser.UNARY_FUNCTION - 34)) | (1 << (esql_parser.UNQUOTED_IDENTIFIER - 34)) | (1 << (esql_parser.QUOTED_IDENTIFIER - 34)))) !== 0)) { + { + this.state = 337; + this.booleanExpression(0); + this.state = 342; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { + { + { + this.state = 338; + this.match(esql_parser.COMMA); + this.state = 339; + this.booleanExpression(0); + } + } + this.state = 344; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + + this.state = 347; + this.match(esql_parser.RP); + } + break; } } catch (re) { @@ -1179,30 +1509,16 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public functionExpressionArgument(): FunctionExpressionArgumentContext { - let _localctx: FunctionExpressionArgumentContext = new FunctionExpressionArgumentContext(this._ctx, this.state); - this.enterRule(_localctx, 38, esql_parser.RULE_functionExpressionArgument); + public rowCommand(): RowCommandContext { + let _localctx: RowCommandContext = new RowCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 34, esql_parser.RULE_rowCommand); try { - this.state = 223; - this._errHandler.sync(this); - switch (this._input.LA(1)) { - case esql_parser.UNQUOTED_IDENTIFIER: - case esql_parser.QUOTED_IDENTIFIER: - this.enterOuterAlt(_localctx, 1); - { - this.state = 221; - this.qualifiedName(); - } - break; - case esql_parser.STRING: - this.enterOuterAlt(_localctx, 2); - { - this.state = 222; - this.string(); - } - break; - default: - throw new NoViableAltException(this); + this.enterOuterAlt(_localctx, 1); + { + this.state = 351; + this.match(esql_parser.ROW); + this.state = 352; + this.fields(); } } catch (re) { @@ -1220,32 +1536,32 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public qualifiedName(): QualifiedNameContext { - let _localctx: QualifiedNameContext = new QualifiedNameContext(this._ctx, this.state); - this.enterRule(_localctx, 40, esql_parser.RULE_qualifiedName); + public fields(): FieldsContext { + let _localctx: FieldsContext = new FieldsContext(this._ctx, this.state); + this.enterRule(_localctx, 36, esql_parser.RULE_fields); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 225; - this.identifier(); - this.state = 230; + this.state = 354; + this.field(); + this.state = 359; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 20, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 33, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 226; - this.match(esql_parser.DOT); - this.state = 227; - this.identifier(); + this.state = 355; + this.match(esql_parser.COMMA); + this.state = 356; + this.field(); } } } - this.state = 232; + this.state = 361; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 20, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 33, this._ctx); } } } @@ -1264,33 +1580,32 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public qualifiedNames(): QualifiedNamesContext { - let _localctx: QualifiedNamesContext = new QualifiedNamesContext(this._ctx, this.state); - this.enterRule(_localctx, 42, esql_parser.RULE_qualifiedNames); + public field(): FieldContext { + let _localctx: FieldContext = new FieldContext(this._ctx, this.state); + this.enterRule(_localctx, 38, esql_parser.RULE_field); try { - let _alt: number; - this.enterOuterAlt(_localctx, 1); - { - this.state = 233; - this.qualifiedName(); - this.state = 238; + this.state = 367; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 21, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - { - { - this.state = 234; - this.match(esql_parser.COMMA); - this.state = 235; - this.qualifiedName(); - } - } + switch ( this.interpreter.adaptivePredict(this._input, 34, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 362; + this.booleanExpression(0); } - this.state = 240; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 21, this._ctx); - } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 363; + this.userVariable(); + this.state = 364; + this.match(esql_parser.ASSIGN); + this.state = 365; + this.booleanExpression(0); + } + break; } } catch (re) { @@ -1308,16 +1623,16 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public identifier(): IdentifierContext { - let _localctx: IdentifierContext = new IdentifierContext(this._ctx, this.state); - this.enterRule(_localctx, 44, esql_parser.RULE_identifier); + public enrichFieldIdentifier(): EnrichFieldIdentifierContext { + let _localctx: EnrichFieldIdentifierContext = new EnrichFieldIdentifierContext(this._ctx, this.state); + this.enterRule(_localctx, 40, esql_parser.RULE_enrichFieldIdentifier); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 241; + this.state = 369; _la = this._input.LA(1); - if (!(_la === esql_parser.UNQUOTED_IDENTIFIER || _la === esql_parser.QUOTED_IDENTIFIER)) { + if (!(_la === esql_parser.ENR_UNQUOTED_IDENTIFIER || _la === esql_parser.ENR_QUOTED_IDENTIFIER)) { this._errHandler.recoverInline(this); } else { if (this._input.LA(1) === Token.EOF) { @@ -1344,14 +1659,14 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public functionIdentifier(): FunctionIdentifierContext { - let _localctx: FunctionIdentifierContext = new FunctionIdentifierContext(this._ctx, this.state); - this.enterRule(_localctx, 46, esql_parser.RULE_functionIdentifier); + public userVariable(): UserVariableContext { + let _localctx: UserVariableContext = new UserVariableContext(this._ctx, this.state); + this.enterRule(_localctx, 42, esql_parser.RULE_userVariable); try { this.enterOuterAlt(_localctx, 1); { - this.state = 243; - this.match(esql_parser.UNARY_FUNCTION); + this.state = 371; + this.identifier(); } } catch (re) { @@ -1369,48 +1684,45 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public constant(): ConstantContext { - let _localctx: ConstantContext = new ConstantContext(this._ctx, this.state); - this.enterRule(_localctx, 48, esql_parser.RULE_constant); + public fromCommand(): FromCommandContext { + let _localctx: FromCommandContext = new FromCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 44, esql_parser.RULE_fromCommand); try { - this.state = 249; + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 373; + this.match(esql_parser.FROM); + this.state = 374; + this.sourceIdentifier(); + this.state = 379; this._errHandler.sync(this); - switch (this._input.LA(1)) { - case esql_parser.NULL: - _localctx = new NullLiteralContext(_localctx); - this.enterOuterAlt(_localctx, 1); - { - this.state = 245; - this.match(esql_parser.NULL); - } - break; - case esql_parser.INTEGER_LITERAL: - case esql_parser.DECIMAL_LITERAL: - _localctx = new NumericLiteralContext(_localctx); - this.enterOuterAlt(_localctx, 2); - { - this.state = 246; - this.number(); - } - break; - case esql_parser.BOOLEAN_VALUE: - _localctx = new BooleanLiteralContext(_localctx); - this.enterOuterAlt(_localctx, 3); - { - this.state = 247; - this.booleanValue(); + _alt = this.interpreter.adaptivePredict(this._input, 35, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 375; + this.match(esql_parser.COMMA); + this.state = 376; + this.sourceIdentifier(); + } + } } - break; - case esql_parser.STRING: - _localctx = new StringLiteralContext(_localctx); - this.enterOuterAlt(_localctx, 4); + this.state = 381; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 35, this._ctx); + } + this.state = 383; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 36, this._ctx) ) { + case 1: { - this.state = 248; - this.string(); + this.state = 382; + this.metadata(); } break; - default: - throw new NoViableAltException(this); + } } } catch (re) { @@ -1428,16 +1740,37 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public limitCommand(): LimitCommandContext { - let _localctx: LimitCommandContext = new LimitCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 50, esql_parser.RULE_limitCommand); + public metadata(): MetadataContext { + let _localctx: MetadataContext = new MetadataContext(this._ctx, this.state); + this.enterRule(_localctx, 46, esql_parser.RULE_metadata); + let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 251; - this.match(esql_parser.LIMIT); - this.state = 252; - this.match(esql_parser.INTEGER_LITERAL); + this.state = 385; + this.match(esql_parser.OPENING_BRACKET); + this.state = 386; + this.match(esql_parser.METADATA); + this.state = 387; + this.sourceIdentifier(); + this.state = 392; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { + { + { + this.state = 388; + this.match(esql_parser.COMMA); + this.state = 389; + this.sourceIdentifier(); + } + } + this.state = 394; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 395; + this.match(esql_parser.CLOSING_BRACKET); } } catch (re) { @@ -1455,35 +1788,16 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public sortCommand(): SortCommandContext { - let _localctx: SortCommandContext = new SortCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 52, esql_parser.RULE_sortCommand); + public evalCommand(): EvalCommandContext { + let _localctx: EvalCommandContext = new EvalCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 48, esql_parser.RULE_evalCommand); try { - let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 254; - this.match(esql_parser.SORT); - this.state = 255; - this.orderExpression(); - this.state = 260; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 23, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - { - { - this.state = 256; - this.match(esql_parser.COMMA); - this.state = 257; - this.orderExpression(); - } - } - } - this.state = 262; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 23, this._ctx); - } + this.state = 397; + this.match(esql_parser.EVAL); + this.state = 398; + this.fields(); } } catch (re) { @@ -1501,35 +1815,33 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public orderExpression(): OrderExpressionContext { - let _localctx: OrderExpressionContext = new OrderExpressionContext(this._ctx, this.state); - this.enterRule(_localctx, 54, esql_parser.RULE_orderExpression); + public statsCommand(): StatsCommandContext { + let _localctx: StatsCommandContext = new StatsCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 50, esql_parser.RULE_statsCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 263; - this.booleanExpression(0); - this.state = 265; + this.state = 400; + this.match(esql_parser.STATS); + this.state = 402; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 24, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 38, this._ctx) ) { case 1: { - this.state = 264; - this.match(esql_parser.ORDERING); + this.state = 401; + this.fields(); } break; } - this.state = 269; + this.state = 406; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 25, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 39, this._ctx) ) { case 1: { - this.state = 267; - this.match(esql_parser.NULLS_ORDERING); - { - this.state = 268; - this.match(esql_parser.NULLS_ORDERING_DIRECTION); - } + this.state = 404; + this.match(esql_parser.BY); + this.state = 405; + this.qualifiedNames(); } break; } @@ -1550,34 +1862,24 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public projectCommand(): ProjectCommandContext { - let _localctx: ProjectCommandContext = new ProjectCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 56, esql_parser.RULE_projectCommand); + public sourceIdentifier(): SourceIdentifierContext { + let _localctx: SourceIdentifierContext = new SourceIdentifierContext(this._ctx, this.state); + this.enterRule(_localctx, 52, esql_parser.RULE_sourceIdentifier); + let _la: number; try { - let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 271; - this.match(esql_parser.PROJECT); - this.state = 272; - this.projectClause(); - this.state = 277; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 26, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - { - { - this.state = 273; - this.match(esql_parser.COMMA); - this.state = 274; - this.projectClause(); - } - } + this.state = 408; + _la = this._input.LA(1); + if (!(_la === esql_parser.SRC_UNQUOTED_IDENTIFIER || _la === esql_parser.SRC_QUOTED_IDENTIFIER)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; } - this.state = 279; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 26, this._ctx); + + this._errHandler.reportMatch(this); + this.consume(); } } } @@ -1596,32 +1898,25 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public projectClause(): ProjectClauseContext { - let _localctx: ProjectClauseContext = new ProjectClauseContext(this._ctx, this.state); - this.enterRule(_localctx, 58, esql_parser.RULE_projectClause); + public enrichIdentifier(): EnrichIdentifierContext { + let _localctx: EnrichIdentifierContext = new EnrichIdentifierContext(this._ctx, this.state); + this.enterRule(_localctx, 54, esql_parser.RULE_enrichIdentifier); + let _la: number; try { - this.state = 285; - this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 27, this._ctx) ) { - case 1: - this.enterOuterAlt(_localctx, 1); - { - this.state = 280; - this.sourceIdentifier(); + this.enterOuterAlt(_localctx, 1); + { + this.state = 410; + _la = this._input.LA(1); + if (!(_la === esql_parser.ENR_UNQUOTED_IDENTIFIER || _la === esql_parser.ENR_QUOTED_IDENTIFIER)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; } - break; - case 2: - this.enterOuterAlt(_localctx, 2); - { - this.state = 281; - _localctx._newName = this.sourceIdentifier(); - this.state = 282; - this.match(esql_parser.ASSIGN); - this.state = 283; - _localctx._oldName = this.sourceIdentifier(); - } - break; + this._errHandler.reportMatch(this); + this.consume(); + } } } catch (re) { @@ -1639,14 +1934,39 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public booleanValue(): BooleanValueContext { - let _localctx: BooleanValueContext = new BooleanValueContext(this._ctx, this.state); - this.enterRule(_localctx, 60, esql_parser.RULE_booleanValue); + public functionExpressionArgument(): FunctionExpressionArgumentContext { + let _localctx: FunctionExpressionArgumentContext = new FunctionExpressionArgumentContext(this._ctx, this.state); + this.enterRule(_localctx, 56, esql_parser.RULE_functionExpressionArgument); try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 287; - this.match(esql_parser.BOOLEAN_VALUE); + this.state = 415; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case esql_parser.ASTERISK: + case esql_parser.UNQUOTED_IDENTIFIER: + case esql_parser.QUOTED_IDENTIFIER: + this.enterOuterAlt(_localctx, 1); + { + this.state = 412; + this.qualifiedName(); + } + break; + case esql_parser.STRING: + this.enterOuterAlt(_localctx, 2); + { + this.state = 413; + this.string(); + } + break; + case esql_parser.INTEGER_LITERAL: + case esql_parser.DECIMAL_LITERAL: + this.enterOuterAlt(_localctx, 3); + { + this.state = 414; + this.number(); + } + break; + default: + throw new NoViableAltException(this); } } catch (re) { @@ -1664,31 +1984,64 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public number(): NumberContext { - let _localctx: NumberContext = new NumberContext(this._ctx, this.state); - this.enterRule(_localctx, 62, esql_parser.RULE_number); + public mathFunctionExpressionArgument(): MathFunctionExpressionArgumentContext { + let _localctx: MathFunctionExpressionArgumentContext = new MathFunctionExpressionArgumentContext(this._ctx, this.state); + this.enterRule(_localctx, 58, esql_parser.RULE_mathFunctionExpressionArgument); try { - this.state = 291; + this.state = 425; this._errHandler.sync(this); - switch (this._input.LA(1)) { - case esql_parser.DECIMAL_LITERAL: - _localctx = new DecimalLiteralContext(_localctx); + switch ( this.interpreter.adaptivePredict(this._input, 41, this._ctx) ) { + case 1: this.enterOuterAlt(_localctx, 1); { - this.state = 289; - this.match(esql_parser.DECIMAL_LITERAL); + this.state = 417; + this.qualifiedName(); } break; - case esql_parser.INTEGER_LITERAL: - _localctx = new IntegerLiteralContext(_localctx); + + case 2: this.enterOuterAlt(_localctx, 2); { - this.state = 290; - this.match(esql_parser.INTEGER_LITERAL); + this.state = 418; + this.string(); + } + break; + + case 3: + this.enterOuterAlt(_localctx, 3); + { + this.state = 419; + this.number(); + } + break; + + case 4: + this.enterOuterAlt(_localctx, 4); + { + this.state = 420; + this.operatorExpression(0); + } + break; + + case 5: + this.enterOuterAlt(_localctx, 5); + { + this.state = 421; + this.number(); + { + this.state = 422; + this.match(esql_parser.DATE_LITERAL); + } + } + break; + + case 6: + this.enterOuterAlt(_localctx, 6); + { + this.state = 424; + this.comparison(); } break; - default: - throw new NoViableAltException(this); } } catch (re) { @@ -1706,14 +2059,33 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public string(): StringContext { - let _localctx: StringContext = new StringContext(this._ctx, this.state); - this.enterRule(_localctx, 64, esql_parser.RULE_string); + public qualifiedName(): QualifiedNameContext { + let _localctx: QualifiedNameContext = new QualifiedNameContext(this._ctx, this.state); + this.enterRule(_localctx, 60, esql_parser.RULE_qualifiedName); try { + let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 293; - this.match(esql_parser.STRING); + this.state = 427; + this.identifier(); + this.state = 432; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 42, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 428; + this.match(esql_parser.DOT); + this.state = 429; + this.identifier(); + } + } + } + this.state = 434; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 42, this._ctx); + } } } catch (re) { @@ -1731,14 +2103,33 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public comparisonOperator(): ComparisonOperatorContext { - let _localctx: ComparisonOperatorContext = new ComparisonOperatorContext(this._ctx, this.state); - this.enterRule(_localctx, 66, esql_parser.RULE_comparisonOperator); + public qualifiedNames(): QualifiedNamesContext { + let _localctx: QualifiedNamesContext = new QualifiedNamesContext(this._ctx, this.state); + this.enterRule(_localctx, 62, esql_parser.RULE_qualifiedNames); try { + let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 295; - this.match(esql_parser.COMPARISON_OPERATOR); + this.state = 435; + this.qualifiedName(); + this.state = 440; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 43, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 436; + this.match(esql_parser.COMMA); + this.state = 437; + this.qualifiedName(); + } + } + } + this.state = 442; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 43, this._ctx); + } } } catch (re) { @@ -1756,16 +2147,25 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public explainCommand(): ExplainCommandContext { - let _localctx: ExplainCommandContext = new ExplainCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 68, esql_parser.RULE_explainCommand); + public identifier(): IdentifierContext { + let _localctx: IdentifierContext = new IdentifierContext(this._ctx, this.state); + this.enterRule(_localctx, 64, esql_parser.RULE_identifier); + let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 297; - this.match(esql_parser.EXPLAIN); - this.state = 298; - this.subqueryExpression(); + this.state = 443; + _la = this._input.LA(1); + if (!(((((_la - 53)) & ~0x1F) === 0 && ((1 << (_la - 53)) & ((1 << (esql_parser.ASTERISK - 53)) | (1 << (esql_parser.UNQUOTED_IDENTIFIER - 53)) | (1 << (esql_parser.QUOTED_IDENTIFIER - 53)))) !== 0))) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } } } catch (re) { @@ -1783,18 +2183,14 @@ export class esql_parser extends Parser { return _localctx; } // @RuleVersion(0) - public subqueryExpression(): SubqueryExpressionContext { - let _localctx: SubqueryExpressionContext = new SubqueryExpressionContext(this._ctx, this.state); - this.enterRule(_localctx, 70, esql_parser.RULE_subqueryExpression); + public mathFunctionIdentifier(): MathFunctionIdentifierContext { + let _localctx: MathFunctionIdentifierContext = new MathFunctionIdentifierContext(this._ctx, this.state); + this.enterRule(_localctx, 66, esql_parser.RULE_mathFunctionIdentifier); try { this.enterOuterAlt(_localctx, 1); { - this.state = 300; - this.match(esql_parser.OPENING_BRACKET); - this.state = 301; - this.query(0); - this.state = 302; - this.match(esql_parser.CLOSING_BRACKET); + this.state = 445; + this.match(esql_parser.MATH_FUNCTION); } } catch (re) { @@ -1811,488 +2207,2199 @@ export class esql_parser extends Parser { } return _localctx; } - - public sempred(_localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { - switch (ruleIndex) { - case 1: - return this.query_sempred(_localctx as QueryContext, predIndex); - - case 5: - return this.booleanExpression_sempred(_localctx as BooleanExpressionContext, predIndex); - - case 9: - return this.operatorExpression_sempred(_localctx as OperatorExpressionContext, predIndex); - } - return true; - } - private query_sempred(_localctx: QueryContext, predIndex: number): boolean { - switch (predIndex) { - case 0: - return this.precpred(this._ctx, 1); + // @RuleVersion(0) + public functionIdentifier(): FunctionIdentifierContext { + let _localctx: FunctionIdentifierContext = new FunctionIdentifierContext(this._ctx, this.state); + this.enterRule(_localctx, 68, esql_parser.RULE_functionIdentifier); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 447; + this.match(esql_parser.UNARY_FUNCTION); + } } - return true; - } - private booleanExpression_sempred(_localctx: BooleanExpressionContext, predIndex: number): boolean { - switch (predIndex) { - case 1: - return this.precpred(this._ctx, 2); - - case 2: - return this.precpred(this._ctx, 1); + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } } - return true; - } - private operatorExpression_sempred(_localctx: OperatorExpressionContext, predIndex: number): boolean { - switch (predIndex) { - case 3: - return this.precpred(this._ctx, 2); - - case 4: - return this.precpred(this._ctx, 1); + finally { + this.exitRule(); } - return true; + return _localctx; } + // @RuleVersion(0) + public constant(): ConstantContext { + let _localctx: ConstantContext = new ConstantContext(this._ctx, this.state); + this.enterRule(_localctx, 70, esql_parser.RULE_constant); + let _la: number; + try { + this.state = 486; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 47, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 449; + this.match(esql_parser.NULL); + } + break; - public static readonly _serializedATN: string = - "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x033\u0133\x04\x02" + - "\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x04\x07" + - "\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r\t\r\x04" + - "\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04\x12\t\x12\x04" + - "\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04\x16\t\x16\x04\x17\t\x17\x04" + - "\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B\x04\x1C\t\x1C\x04" + - "\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04 \t \x04!\t!\x04\"\t\"\x04#" + - "\t#\x04$\t$\x04%\t%\x03\x02\x03\x02\x03\x02\x03\x03\x03\x03\x03\x03\x03" + - "\x03\x03\x03\x03\x03\x07\x03T\n\x03\f\x03\x0E\x03W\v\x03\x03\x04\x03\x04" + - "\x03\x04\x05\x04\\\n\x04\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05" + - "\x05\x05d\n\x05\x03\x06\x03\x06\x03\x06\x03\x07\x03\x07\x03\x07\x03\x07" + - "\x05\x07m\n\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x07\x07" + - "u\n\x07\f\x07\x0E\x07x\v\x07\x03\b\x03\b\x05\b|\n\b\x03\t\x03\t\x03\t" + - "\x03\t\x03\n\x03\n\x03\n\x03\n\x03\n\x07\n\x87\n\n\f\n\x0E\n\x8A\v\n\x05" + - "\n\x8C\n\n\x03\n\x03\n\x03\v\x03\v\x03\v\x03\v\x03\v\x05\v\x95\n\v\x03" + - "\v\x03\v\x03\v\x03\v\x03\v\x03\v\x07\v\x9D\n\v\f\v\x0E\v\xA0\v\v\x03\f" + - "\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x07\f\xAD" + - "\n\f\f\f\x0E\f\xB0\v\f\x05\f\xB2\n\f\x03\f\x03\f\x05\f\xB6\n\f\x03\r\x03" + - "\r\x03\r\x03\x0E\x03\x0E\x03\x0E\x07\x0E\xBE\n\x0E\f\x0E\x0E\x0E\xC1\v" + - "\x0E\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x05\x0F\xC8\n\x0F\x03\x10" + - "\x03\x10\x03\x11\x03\x11\x03\x11\x03\x11\x07\x11\xD0\n\x11\f\x11\x0E\x11" + - "\xD3\v\x11\x03\x12\x03\x12\x03\x12\x03\x13\x03\x13\x03\x13\x03\x13\x05" + - "\x13\xDC\n\x13\x03\x14\x03\x14\x03\x15\x03\x15\x05\x15\xE2\n\x15\x03\x16" + - "\x03\x16\x03\x16\x07\x16\xE7\n\x16\f\x16\x0E\x16\xEA\v\x16\x03\x17\x03" + - "\x17\x03\x17\x07\x17\xEF\n\x17\f\x17\x0E\x17\xF2\v\x17\x03\x18\x03\x18" + - "\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x05\x1A\xFC\n\x1A\x03" + - "\x1B\x03\x1B\x03\x1B\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x07\x1C\u0105\n\x1C" + - "\f\x1C\x0E\x1C\u0108\v\x1C\x03\x1D\x03\x1D\x05\x1D\u010C\n\x1D\x03\x1D" + - "\x03\x1D\x05\x1D\u0110\n\x1D\x03\x1E\x03\x1E\x03\x1E\x03\x1E\x07\x1E\u0116" + - "\n\x1E\f\x1E\x0E\x1E\u0119\v\x1E\x03\x1F\x03\x1F\x03\x1F\x03\x1F\x03\x1F" + - "\x05\x1F\u0120\n\x1F\x03 \x03 \x03!\x03!\x05!\u0126\n!\x03\"\x03\"\x03" + - "#\x03#\x03$\x03$\x03$\x03%\x03%\x03%\x03%\x03%\x02\x02\x05\x04\f\x14&" + - "\x02\x02\x04\x02\x06\x02\b\x02\n\x02\f\x02\x0E\x02\x10\x02\x12\x02\x14" + - "\x02\x16\x02\x18\x02\x1A\x02\x1C\x02\x1E\x02 \x02\"\x02$\x02&\x02(\x02" + - "*\x02,\x02.\x020\x022\x024\x026\x028\x02:\x02<\x02>\x02@\x02B\x02D\x02" + - "F\x02H\x02\x02\x06\x03\x02!\"\x03\x02#%\x03\x02/0\x03\x02*+\x02\u0135" + - "\x02J\x03\x02\x02\x02\x04M\x03\x02\x02\x02\x06[\x03\x02\x02\x02\bc\x03" + - "\x02\x02\x02\ne\x03\x02\x02\x02\fl\x03\x02\x02\x02\x0E{\x03\x02\x02\x02" + - "\x10}\x03\x02\x02\x02\x12\x81\x03\x02\x02\x02\x14\x94\x03\x02\x02\x02" + - "\x16\xB5\x03\x02\x02\x02\x18\xB7\x03\x02\x02\x02\x1A\xBA\x03\x02\x02\x02" + - "\x1C\xC7\x03\x02\x02\x02\x1E\xC9\x03\x02\x02\x02 \xCB\x03\x02\x02\x02" + - "\"\xD4\x03\x02\x02\x02$\xD7\x03\x02\x02\x02&\xDD\x03\x02\x02\x02(\xE1" + - "\x03\x02\x02\x02*\xE3\x03\x02\x02\x02,\xEB\x03\x02\x02\x02.\xF3\x03\x02" + - "\x02\x020\xF5\x03\x02\x02\x022\xFB\x03\x02\x02\x024\xFD\x03\x02\x02\x02" + - "6\u0100\x03\x02\x02\x028\u0109\x03\x02\x02\x02:\u0111\x03\x02\x02\x02" + - "<\u011F\x03\x02\x02\x02>\u0121\x03\x02\x02\x02@\u0125\x03\x02\x02\x02" + - "B\u0127\x03\x02\x02\x02D\u0129\x03\x02\x02\x02F\u012B\x03\x02\x02\x02" + - "H\u012E\x03\x02\x02\x02JK\x05\x04\x03\x02KL\x07\x02\x02\x03L\x03\x03\x02" + - "\x02\x02MN\b\x03\x01\x02NO\x05\x06\x04\x02OU\x03\x02\x02\x02PQ\f\x03\x02" + - "\x02QR\x07\x0F\x02\x02RT\x05\b\x05\x02SP\x03\x02\x02\x02TW\x03\x02\x02" + - "\x02US\x03\x02\x02\x02UV\x03\x02\x02\x02V\x05\x03\x02\x02\x02WU\x03\x02" + - "\x02\x02X\\\x05F$\x02Y\\\x05 \x11\x02Z\\\x05\x18\r\x02[X\x03\x02\x02\x02" + - "[Y\x03\x02\x02\x02[Z\x03\x02\x02\x02\\\x07\x03\x02\x02\x02]d\x05\"\x12" + - "\x02^d\x054\x1B\x02_d\x05:\x1E\x02`d\x056\x1C\x02ad\x05$\x13\x02bd\x05" + - "\n\x06\x02c]\x03\x02\x02\x02c^\x03\x02\x02\x02c_\x03\x02\x02\x02c`\x03" + - "\x02\x02\x02ca\x03\x02\x02\x02cb\x03\x02\x02\x02d\t\x03\x02\x02\x02ef" + - "\x07\b\x02\x02fg\x05\f\x07\x02g\v\x03\x02\x02\x02hi\b\x07\x01\x02ij\x07" + - "\x1B\x02\x02jm\x05\f\x07\x06km\x05\x0E\b\x02lh\x03\x02\x02\x02lk\x03\x02" + - "\x02\x02mv\x03\x02\x02\x02no\f\x04\x02\x02op\x07\x14\x02\x02pu\x05\f\x07" + - "\x05qr\f\x03\x02\x02rs\x07\x1D\x02\x02su\x05\f\x07\x04tn\x03\x02\x02\x02" + - "tq\x03\x02\x02\x02ux\x03\x02\x02\x02vt\x03\x02\x02\x02vw\x03\x02\x02\x02" + - "w\r\x03\x02\x02\x02xv\x03\x02\x02\x02y|\x05\x14\v\x02z|\x05\x10\t\x02" + - "{y\x03\x02\x02\x02{z\x03\x02\x02\x02|\x0F\x03\x02\x02\x02}~\x05\x14\v" + - "\x02~\x7F\x05D#\x02\x7F\x80\x05\x14\v\x02\x80\x11\x03\x02\x02\x02\x81" + - "\x82\x050\x19\x02\x82\x8B\x07\x18\x02\x02\x83\x88\x05(\x15\x02\x84\x85" + - "\x07\x16\x02\x02\x85\x87\x05(\x15\x02\x86\x84\x03\x02\x02\x02\x87\x8A" + - "\x03\x02\x02\x02\x88\x86\x03\x02\x02\x02\x88\x89\x03\x02\x02\x02\x89\x8C" + - "\x03\x02\x02\x02\x8A\x88\x03\x02\x02\x02\x8B\x83\x03\x02\x02\x02\x8B\x8C" + - "\x03\x02\x02\x02\x8C\x8D\x03\x02\x02\x02\x8D\x8E\x07\x1E\x02\x02\x8E\x13" + - "\x03\x02\x02\x02\x8F\x90\b\v\x01\x02\x90\x95\x05\x16\f\x02\x91\x95\x05" + - "\x12\n\x02\x92\x93\t\x02\x02\x02\x93\x95\x05\x14\v\x05\x94\x8F\x03\x02" + - "\x02\x02\x94\x91\x03\x02\x02\x02\x94\x92\x03\x02\x02\x02\x95\x9E\x03\x02" + - "\x02\x02\x96\x97\f\x04\x02\x02\x97\x98\t\x03\x02\x02\x98\x9D\x05\x14\v" + - "\x05\x99\x9A\f\x03\x02\x02\x9A\x9B\t\x02\x02\x02\x9B\x9D\x05\x14\v\x04" + - "\x9C\x96\x03\x02\x02\x02\x9C\x99\x03\x02\x02\x02\x9D\xA0\x03\x02\x02\x02" + - "\x9E\x9C\x03\x02\x02\x02\x9E\x9F\x03\x02\x02\x02\x9F\x15\x03\x02\x02\x02" + - "\xA0\x9E\x03\x02\x02\x02\xA1\xB6\x052\x1A\x02\xA2\xB6\x05*\x16\x02\xA3" + - "\xA4\x07\x18\x02\x02\xA4\xA5\x05\f\x07\x02\xA5\xA6\x07\x1E\x02\x02\xA6" + - "\xB6\x03\x02\x02\x02\xA7\xA8\x05.\x18\x02\xA8\xB1\x07\x18\x02\x02\xA9" + - "\xAE\x05\f\x07\x02\xAA\xAB\x07\x16\x02\x02\xAB\xAD\x05\f\x07\x02\xAC\xAA" + - "\x03\x02\x02\x02\xAD\xB0\x03\x02\x02\x02\xAE\xAC\x03\x02\x02\x02\xAE\xAF" + - "\x03\x02\x02\x02\xAF\xB2\x03\x02\x02\x02\xB0\xAE\x03\x02\x02\x02\xB1\xA9" + - "\x03\x02\x02\x02\xB1\xB2\x03\x02\x02\x02\xB2\xB3\x03\x02\x02\x02\xB3\xB4" + - "\x07\x1E\x02\x02\xB4\xB6\x03\x02\x02\x02\xB5\xA1\x03\x02\x02\x02\xB5\xA2" + - "\x03\x02\x02\x02\xB5\xA3\x03\x02\x02\x02\xB5\xA7\x03\x02\x02\x02\xB6\x17" + - "\x03\x02\x02\x02\xB7\xB8\x07\x06\x02\x02\xB8\xB9\x05\x1A\x0E\x02\xB9\x19" + - "\x03\x02\x02\x02\xBA\xBF\x05\x1C\x0F\x02\xBB\xBC\x07\x16\x02\x02\xBC\xBE" + - "\x05\x1C\x0F\x02\xBD\xBB\x03\x02\x02\x02\xBE\xC1\x03\x02\x02\x02\xBF\xBD" + - "\x03\x02\x02\x02\xBF\xC0\x03\x02\x02\x02\xC0\x1B\x03\x02\x02\x02\xC1\xBF" + - "\x03\x02\x02\x02\xC2\xC8\x05\f\x07\x02\xC3\xC4\x05\x1E\x10\x02\xC4\xC5" + - "\x07\x15\x02\x02\xC5\xC6\x05\f\x07\x02\xC6\xC8\x03\x02\x02\x02\xC7\xC2" + - "\x03\x02\x02\x02\xC7\xC3\x03\x02\x02\x02\xC8\x1D\x03\x02\x02\x02\xC9\xCA" + - "\x05.\x18\x02\xCA\x1F\x03\x02\x02\x02\xCB\xCC\x07\x05\x02\x02\xCC\xD1" + - "\x05&\x14\x02\xCD\xCE\x07\x16\x02\x02\xCE\xD0\x05&\x14\x02\xCF\xCD\x03" + - "\x02\x02\x02\xD0\xD3\x03\x02\x02\x02\xD1\xCF\x03\x02\x02\x02\xD1\xD2\x03" + - "\x02\x02\x02\xD2!\x03\x02\x02\x02\xD3\xD1\x03\x02\x02\x02\xD4\xD5\x07" + - "\x03\x02\x02\xD5\xD6\x05\x1A\x0E\x02\xD6#\x03\x02\x02\x02\xD7\xD8\x07" + - "\x07\x02\x02\xD8\xDB\x05\x1A\x0E\x02\xD9\xDA\x07\x13\x02\x02\xDA\xDC\x05" + - ",\x17\x02\xDB\xD9\x03\x02\x02\x02\xDB\xDC\x03\x02\x02\x02\xDC%\x03\x02" + - "\x02\x02\xDD\xDE\t\x04\x02\x02\xDE\'\x03\x02\x02\x02\xDF\xE2\x05*\x16" + - "\x02\xE0\xE2\x05B\"\x02\xE1\xDF\x03\x02\x02\x02\xE1\xE0\x03\x02\x02\x02" + - "\xE2)\x03\x02\x02\x02\xE3\xE8\x05.\x18\x02\xE4\xE5\x07\x17\x02\x02\xE5" + - "\xE7\x05.\x18\x02\xE6\xE4\x03\x02\x02\x02\xE7\xEA\x03\x02\x02\x02\xE8" + - "\xE6\x03\x02\x02\x02\xE8\xE9\x03\x02\x02\x02\xE9+\x03\x02\x02\x02\xEA" + - "\xE8\x03\x02\x02\x02\xEB\xF0\x05*\x16\x02\xEC\xED\x07\x16\x02\x02\xED" + - "\xEF\x05*\x16\x02\xEE\xEC\x03\x02\x02\x02\xEF\xF2\x03\x02\x02\x02\xF0" + - "\xEE\x03\x02\x02\x02\xF0\xF1\x03\x02\x02\x02\xF1-\x03\x02\x02\x02\xF2" + - "\xF0\x03\x02\x02\x02\xF3\xF4\t\x05\x02\x02\xF4/\x03\x02\x02\x02\xF5\xF6" + - "\x07)\x02\x02\xF61\x03\x02\x02\x02\xF7\xFC\x07\x1C\x02\x02\xF8\xFC\x05" + - "@!\x02\xF9\xFC\x05> \x02\xFA\xFC\x05B\"\x02\xFB\xF7\x03\x02\x02\x02\xFB" + - "\xF8\x03\x02\x02\x02\xFB\xF9\x03\x02\x02\x02\xFB\xFA\x03\x02\x02\x02\xFC" + - "3\x03\x02\x02\x02\xFD\xFE\x07\n\x02\x02\xFE\xFF\x07\x11\x02\x02\xFF5\x03" + - "\x02\x02\x02\u0100\u0101\x07\t\x02\x02\u0101\u0106\x058\x1D\x02\u0102" + - "\u0103\x07\x16\x02\x02\u0103\u0105\x058\x1D\x02\u0104\u0102\x03\x02\x02" + - "\x02\u0105\u0108\x03\x02\x02\x02\u0106\u0104\x03\x02\x02\x02\u0106\u0107" + - "\x03\x02\x02\x02\u01077\x03\x02\x02\x02\u0108\u0106\x03\x02\x02\x02\u0109" + - "\u010B\x05\f\x07\x02\u010A\u010C\x07&\x02\x02\u010B\u010A\x03\x02\x02" + - "\x02\u010B\u010C\x03\x02\x02\x02\u010C\u010F\x03\x02\x02\x02\u010D\u010E" + - "\x07\'\x02\x02\u010E\u0110\x07(\x02\x02\u010F\u010D\x03\x02\x02\x02\u010F" + - "\u0110\x03\x02\x02\x02\u01109\x03\x02\x02\x02\u0111\u0112\x07\v\x02\x02" + - "\u0112\u0117\x05<\x1F\x02\u0113\u0114\x07\x16\x02\x02\u0114\u0116\x05" + - "<\x1F\x02\u0115\u0113\x03\x02\x02\x02\u0116\u0119\x03\x02\x02\x02\u0117" + - "\u0115\x03\x02\x02\x02\u0117\u0118\x03\x02\x02\x02\u0118;\x03\x02\x02" + - "\x02\u0119\u0117\x03\x02\x02\x02\u011A\u0120\x05&\x14\x02\u011B\u011C" + - "\x05&\x14\x02\u011C\u011D\x07\x15\x02\x02\u011D\u011E\x05&\x14\x02\u011E" + - "\u0120\x03\x02\x02\x02\u011F\u011A\x03\x02\x02\x02\u011F\u011B\x03\x02" + - "\x02\x02\u0120=\x03\x02\x02\x02\u0121\u0122\x07\x1F\x02\x02\u0122?\x03" + - "\x02\x02\x02\u0123\u0126\x07\x12\x02\x02\u0124\u0126\x07\x11\x02\x02\u0125" + - "\u0123\x03\x02\x02\x02\u0125\u0124\x03\x02\x02\x02\u0126A\x03\x02\x02" + - "\x02\u0127\u0128\x07\x10\x02\x02\u0128C\x03\x02\x02\x02\u0129\u012A\x07" + - " \x02\x02\u012AE\x03\x02\x02\x02\u012B\u012C\x07\x04\x02\x02\u012C\u012D" + - "\x05H%\x02\u012DG\x03\x02\x02\x02\u012E\u012F\x07\x19\x02\x02\u012F\u0130" + - "\x05\x04\x03\x02\u0130\u0131\x07\x1A\x02\x02\u0131I\x03\x02\x02\x02\x1F" + - "U[cltv{\x88\x8B\x94\x9C\x9E\xAE\xB1\xB5\xBF\xC7\xD1\xDB\xE1\xE8\xF0\xFB" + - "\u0106\u010B\u010F\u0117\u011F\u0125"; - public static __ATN: ATN; - public static get _ATN(): ATN { - if (!esql_parser.__ATN) { - esql_parser.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(esql_parser._serializedATN)); - } + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 450; + this.numericValue(); + } + break; - return esql_parser.__ATN; - } + case 3: + this.enterOuterAlt(_localctx, 3); + { + this.state = 451; + this.booleanValue(); + } + break; + case 4: + this.enterOuterAlt(_localctx, 4); + { + this.state = 452; + this.string(); + } + break; + + case 5: + this.enterOuterAlt(_localctx, 5); + { + this.state = 453; + this.match(esql_parser.OPENING_BRACKET); + this.state = 454; + this.numericValue(); + this.state = 459; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { + { + { + this.state = 455; + this.match(esql_parser.COMMA); + this.state = 456; + this.numericValue(); + } + } + this.state = 461; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 462; + this.match(esql_parser.CLOSING_BRACKET); + } + break; + + case 6: + this.enterOuterAlt(_localctx, 6); + { + this.state = 464; + this.match(esql_parser.OPENING_BRACKET); + this.state = 465; + this.booleanValue(); + this.state = 470; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { + { + { + this.state = 466; + this.match(esql_parser.COMMA); + this.state = 467; + this.booleanValue(); + } + } + this.state = 472; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 473; + this.match(esql_parser.CLOSING_BRACKET); + } + break; + + case 7: + this.enterOuterAlt(_localctx, 7); + { + this.state = 475; + this.match(esql_parser.OPENING_BRACKET); + this.state = 476; + this.string(); + this.state = 481; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === esql_parser.COMMA) { + { + { + this.state = 477; + this.match(esql_parser.COMMA); + this.state = 478; + this.string(); + } + } + this.state = 483; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 484; + this.match(esql_parser.CLOSING_BRACKET); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public numericValue(): NumericValueContext { + let _localctx: NumericValueContext = new NumericValueContext(this._ctx, this.state); + this.enterRule(_localctx, 72, esql_parser.RULE_numericValue); + try { + this.state = 490; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case esql_parser.DECIMAL_LITERAL: + this.enterOuterAlt(_localctx, 1); + { + this.state = 488; + this.decimalValue(); + } + break; + case esql_parser.INTEGER_LITERAL: + this.enterOuterAlt(_localctx, 2); + { + this.state = 489; + this.integerValue(); + } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public limitCommand(): LimitCommandContext { + let _localctx: LimitCommandContext = new LimitCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 74, esql_parser.RULE_limitCommand); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 492; + this.match(esql_parser.LIMIT); + this.state = 493; + this.match(esql_parser.INTEGER_LITERAL); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public sortCommand(): SortCommandContext { + let _localctx: SortCommandContext = new SortCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 76, esql_parser.RULE_sortCommand); + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 495; + this.match(esql_parser.SORT); + this.state = 496; + this.orderExpression(); + this.state = 501; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 49, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 497; + this.match(esql_parser.COMMA); + this.state = 498; + this.orderExpression(); + } + } + } + this.state = 503; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 49, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public orderExpression(): OrderExpressionContext { + let _localctx: OrderExpressionContext = new OrderExpressionContext(this._ctx, this.state); + this.enterRule(_localctx, 78, esql_parser.RULE_orderExpression); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 504; + this.booleanExpression(0); + this.state = 506; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 50, this._ctx) ) { + case 1: + { + this.state = 505; + this.match(esql_parser.ORDERING); + } + break; + } + this.state = 510; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 51, this._ctx) ) { + case 1: + { + this.state = 508; + this.match(esql_parser.NULLS_ORDERING); + { + this.state = 509; + this.match(esql_parser.NULLS_ORDERING_DIRECTION); + } + } + break; + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public projectCommand(): ProjectCommandContext { + let _localctx: ProjectCommandContext = new ProjectCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 80, esql_parser.RULE_projectCommand); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 512; + this.match(esql_parser.PROJECT); + this.state = 513; + this.qualifiedNames(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public keepCommand(): KeepCommandContext { + let _localctx: KeepCommandContext = new KeepCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 82, esql_parser.RULE_keepCommand); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 515; + this.match(esql_parser.KEEP); + this.state = 516; + this.qualifiedNames(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public dropCommand(): DropCommandContext { + let _localctx: DropCommandContext = new DropCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 84, esql_parser.RULE_dropCommand); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 518; + this.match(esql_parser.DROP); + this.state = 519; + this.qualifiedNames(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public renameVariable(): RenameVariableContext { + let _localctx: RenameVariableContext = new RenameVariableContext(this._ctx, this.state); + this.enterRule(_localctx, 86, esql_parser.RULE_renameVariable); + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 521; + this.identifier(); + this.state = 526; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 52, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 522; + this.match(esql_parser.DOT); + this.state = 523; + this.identifier(); + } + } + } + this.state = 528; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 52, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public renameCommand(): RenameCommandContext { + let _localctx: RenameCommandContext = new RenameCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 88, esql_parser.RULE_renameCommand); + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 529; + this.match(esql_parser.RENAME); + this.state = 530; + this.renameClause(); + this.state = 535; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 53, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 531; + this.match(esql_parser.COMMA); + this.state = 532; + this.renameClause(); + } + } + } + this.state = 537; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 53, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public renameClause(): RenameClauseContext { + let _localctx: RenameClauseContext = new RenameClauseContext(this._ctx, this.state); + this.enterRule(_localctx, 90, esql_parser.RULE_renameClause); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 538; + this.qualifiedName(); + this.state = 539; + this.match(esql_parser.AS); + this.state = 540; + this.renameVariable(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public dissectCommand(): DissectCommandContext { + let _localctx: DissectCommandContext = new DissectCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 92, esql_parser.RULE_dissectCommand); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 542; + this.match(esql_parser.DISSECT); + this.state = 543; + this.qualifiedNames(); + this.state = 544; + this.string(); + this.state = 546; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 54, this._ctx) ) { + case 1: + { + this.state = 545; + this.commandOptions(); + } + break; + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public grokCommand(): GrokCommandContext { + let _localctx: GrokCommandContext = new GrokCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 94, esql_parser.RULE_grokCommand); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 548; + this.match(esql_parser.GROK); + this.state = 549; + this.qualifiedNames(); + this.state = 550; + this.string(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public commandOptions(): CommandOptionsContext { + let _localctx: CommandOptionsContext = new CommandOptionsContext(this._ctx, this.state); + this.enterRule(_localctx, 96, esql_parser.RULE_commandOptions); + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 552; + this.commandOption(); + this.state = 557; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 55, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 553; + this.match(esql_parser.COMMA); + this.state = 554; + this.commandOption(); + } + } + } + this.state = 559; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 55, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public commandOption(): CommandOptionContext { + let _localctx: CommandOptionContext = new CommandOptionContext(this._ctx, this.state); + this.enterRule(_localctx, 98, esql_parser.RULE_commandOption); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 560; + this.identifier(); + this.state = 561; + this.match(esql_parser.ASSIGN); + this.state = 562; + this.constant(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public booleanValue(): BooleanValueContext { + let _localctx: BooleanValueContext = new BooleanValueContext(this._ctx, this.state); + this.enterRule(_localctx, 100, esql_parser.RULE_booleanValue); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 564; + this.match(esql_parser.BOOLEAN_VALUE); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public number(): NumberContext { + let _localctx: NumberContext = new NumberContext(this._ctx, this.state); + this.enterRule(_localctx, 102, esql_parser.RULE_number); + try { + this.state = 568; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case esql_parser.DECIMAL_LITERAL: + _localctx = new DecimalLiteralContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 566; + this.match(esql_parser.DECIMAL_LITERAL); + } + break; + case esql_parser.INTEGER_LITERAL: + _localctx = new IntegerLiteralContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 567; + this.match(esql_parser.INTEGER_LITERAL); + } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public decimalValue(): DecimalValueContext { + let _localctx: DecimalValueContext = new DecimalValueContext(this._ctx, this.state); + this.enterRule(_localctx, 104, esql_parser.RULE_decimalValue); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 570; + this.match(esql_parser.DECIMAL_LITERAL); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public integerValue(): IntegerValueContext { + let _localctx: IntegerValueContext = new IntegerValueContext(this._ctx, this.state); + this.enterRule(_localctx, 106, esql_parser.RULE_integerValue); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 572; + this.match(esql_parser.INTEGER_LITERAL); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public string(): StringContext { + let _localctx: StringContext = new StringContext(this._ctx, this.state); + this.enterRule(_localctx, 108, esql_parser.RULE_string); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 574; + this.match(esql_parser.STRING); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public comparisonOperator(): ComparisonOperatorContext { + let _localctx: ComparisonOperatorContext = new ComparisonOperatorContext(this._ctx, this.state); + this.enterRule(_localctx, 110, esql_parser.RULE_comparisonOperator); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 576; + this.match(esql_parser.COMPARISON_OPERATOR); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public explainCommand(): ExplainCommandContext { + let _localctx: ExplainCommandContext = new ExplainCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 112, esql_parser.RULE_explainCommand); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 578; + this.match(esql_parser.EXPLAIN); + this.state = 579; + this.subqueryExpression(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public subqueryExpression(): SubqueryExpressionContext { + let _localctx: SubqueryExpressionContext = new SubqueryExpressionContext(this._ctx, this.state); + this.enterRule(_localctx, 114, esql_parser.RULE_subqueryExpression); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 581; + this.match(esql_parser.OPENING_BRACKET); + this.state = 582; + this.query(0); + this.state = 583; + this.match(esql_parser.CLOSING_BRACKET); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public showCommand(): ShowCommandContext { + let _localctx: ShowCommandContext = new ShowCommandContext(this._ctx, this.state); + this.enterRule(_localctx, 116, esql_parser.RULE_showCommand); + try { + this.state = 589; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 57, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 585; + this.match(esql_parser.SHOW); + this.state = 586; + this.match(esql_parser.INFO); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 587; + this.match(esql_parser.SHOW); + this.state = 588; + this.match(esql_parser.FUNCTIONS); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + + public sempred(_localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { + switch (ruleIndex) { + case 1: + return this.query_sempred(_localctx as QueryContext, predIndex); + + case 8: + return this.whereBooleanExpression_sempred(_localctx as WhereBooleanExpressionContext, predIndex); + + case 9: + return this.booleanExpression_sempred(_localctx as BooleanExpressionContext, predIndex); + + case 15: + return this.operatorExpression_sempred(_localctx as OperatorExpressionContext, predIndex); + } + return true; + } + private query_sempred(_localctx: QueryContext, predIndex: number): boolean { + switch (predIndex) { + case 0: + return this.precpred(this._ctx, 1); + } + return true; + } + private whereBooleanExpression_sempred(_localctx: WhereBooleanExpressionContext, predIndex: number): boolean { + switch (predIndex) { + case 1: + return this.precpred(this._ctx, 5); + + case 2: + return this.precpred(this._ctx, 4); + } + return true; + } + private booleanExpression_sempred(_localctx: BooleanExpressionContext, predIndex: number): boolean { + switch (predIndex) { + case 3: + return this.precpred(this._ctx, 2); + + case 4: + return this.precpred(this._ctx, 1); + } + return true; + } + private operatorExpression_sempred(_localctx: OperatorExpressionContext, predIndex: number): boolean { + switch (predIndex) { + case 5: + return this.precpred(this._ctx, 2); + + case 6: + return this.precpred(this._ctx, 1); + } + return true; + } + + private static readonly _serializedATNSegments: number = 2; + private static readonly _serializedATNSegment0: string = + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x03S\u0252\x04\x02" + + "\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x04\x07" + + "\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r\t\r\x04" + + "\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04\x12\t\x12\x04" + + "\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04\x16\t\x16\x04\x17\t\x17\x04" + + "\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B\x04\x1C\t\x1C\x04" + + "\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04 \t \x04!\t!\x04\"\t\"\x04#" + + "\t#\x04$\t$\x04%\t%\x04&\t&\x04\'\t\'\x04(\t(\x04)\t)\x04*\t*\x04+\t+" + + "\x04,\t,\x04-\t-\x04.\t.\x04/\t/\x040\t0\x041\t1\x042\t2\x043\t3\x044" + + "\t4\x045\t5\x046\t6\x047\t7\x048\t8\x049\t9\x04:\t:\x04;\t;\x04<\t<\x03" + + "\x02\x03\x02\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x07" + + "\x03\x82\n\x03\f\x03\x0E\x03\x85\v\x03\x03\x04\x03\x04\x03\x04\x03\x04" + + "\x05\x04\x8B\n\x04\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03" + + "\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x05\x05\x9A\n\x05" + + "\x03\x06\x03\x06\x03\x06\x03\x06\x05\x06\xA0\n\x06\x03\x06\x03\x06\x03" + + "\x06\x03\x06\x07\x06\xA6\n\x06\f\x06\x0E\x06\xA9\v\x06\x05\x06\xAB\n\x06" + + "\x03\x07\x03\x07\x03\x07\x05\x07\xB0\n\x07\x03\x07\x03\x07\x03\b\x03\b" + + "\x03\b\x03\t\x03\t\x03\t\x03\n\x03\n\x03\n\x03\n\x03\n\x03\n\x03\n\x05" + + "\n\xC1\n\n\x03\n\x03\n\x03\n\x03\n\x03\n\x07\n\xC8\n\n\f\n\x0E\n\xCB\v" + + "\n\x03\n\x03\n\x03\n\x05\n\xD0\n\n\x03\n\x03\n\x03\n\x03\n\x03\n\x07\n" + + "\xD7\n\n\f\n\x0E\n\xDA\v\n\x05\n\xDC\n\n\x03\n\x03\n\x03\n\x03\n\x03\n" + + "\x05\n\xE3\n\n\x03\n\x03\n\x05\n\xE7\n\n\x03\n\x03\n\x03\n\x03\n\x03\n" + + "\x03\n\x07\n\xEF\n\n\f\n\x0E\n\xF2\v\n\x03\v\x03\v\x03\v\x03\v\x05\v\xF8" + + "\n\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x07\v\u0100\n\v\f\v\x0E\v\u0103" + + "\v\v\x03\f\x03\f\x05\f\u0107\n\f\x03\f\x03\f\x03\f\x03\f\x03\f\x05\f\u010E" + + "\n\f\x03\f\x03\f\x03\f\x05\f\u0113\n\f\x03\r\x03\r\x05\r\u0117\n\r\x03" + + "\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x07" + + "\x0F\u0122\n\x0F\f\x0F\x0E\x0F\u0125\v\x0F\x05\x0F\u0127\n\x0F\x03\x0F" + + "\x03\x0F\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x07\x10\u0130\n\x10\f" + + "\x10\x0E\x10\u0133\v\x10\x05\x10\u0135\n\x10\x03\x10\x03\x10\x03\x11\x03" + + "\x11\x03\x11\x03\x11\x03\x11\x03\x11\x05\x11\u013F\n\x11\x03\x11\x03\x11" + + "\x03\x11\x03\x11\x03\x11\x03\x11\x07\x11\u0147\n\x11\f\x11\x0E\x11\u014A" + + "\v\x11\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12" + + "\x03\x12\x03\x12\x03\x12\x07\x12\u0157\n\x12\f\x12\x0E\x12\u015A\v\x12" + + "\x05\x12\u015C\n\x12\x03\x12\x03\x12\x05\x12\u0160\n\x12\x03\x13\x03\x13" + + "\x03\x13\x03\x14\x03\x14\x03\x14\x07\x14\u0168\n\x14\f\x14\x0E\x14\u016B" + + "\v\x14\x03\x15\x03\x15\x03\x15\x03\x15\x03\x15\x05\x15\u0172\n\x15\x03" + + "\x16\x03\x16\x03\x17\x03\x17\x03\x18\x03\x18\x03\x18\x03\x18\x07\x18\u017C" + + "\n\x18\f\x18\x0E\x18\u017F\v\x18\x03\x18\x05\x18\u0182\n\x18\x03\x19\x03" + + "\x19\x03\x19\x03\x19\x03\x19\x07\x19\u0189\n\x19\f\x19\x0E\x19\u018C\v" + + "\x19\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1A\x03\x1B\x03\x1B\x05\x1B\u0195" + + "\n\x1B\x03\x1B\x03\x1B\x05\x1B\u0199\n\x1B\x03\x1C\x03\x1C\x03\x1D\x03" + + "\x1D\x03\x1E\x03\x1E\x03\x1E\x05\x1E\u01A2\n\x1E\x03\x1F\x03\x1F\x03\x1F" + + "\x03\x1F\x03\x1F\x03\x1F\x03\x1F\x03\x1F\x05\x1F\u01AC\n\x1F\x03 \x03" + + " \x03 \x07 \u01B1\n \f \x0E \u01B4\v \x03!\x03!\x03!\x07!\u01B9\n!\f!" + + "\x0E!\u01BC\v!\x03\"\x03\"\x03#\x03#\x03$\x03$\x03%\x03%\x03%\x03%\x03" + + "%\x03%\x03%\x03%\x07%\u01CC\n%\f%\x0E%\u01CF\v%\x03%\x03%\x03%\x03%\x03" + + "%\x03%\x07%\u01D7\n%\f%\x0E%\u01DA\v%\x03%\x03%\x03%\x03%\x03%\x03%\x07" + + "%\u01E2\n%\f%\x0E%\u01E5\v%\x03%\x03%\x05%\u01E9\n%\x03&\x03&\x05&\u01ED" + + "\n&\x03\'\x03\'\x03\'\x03(\x03(\x03(\x03(\x07(\u01F6\n(\f(\x0E(\u01F9" + + "\v(\x03)\x03)\x05)\u01FD\n)\x03)\x03)\x05)\u0201\n)\x03*\x03*\x03*\x03" + + "+\x03+\x03+\x03,\x03,\x03,\x03-\x03-\x03-\x07-\u020F\n-\f-\x0E-\u0212" + + "\v-\x03.\x03.\x03.\x03.\x07.\u0218\n.\f.\x0E.\u021B\v.\x03/\x03/\x03/" + + "\x03/\x030\x030\x030\x030\x050\u0225\n0\x031\x031\x031\x031\x032\x032" + + "\x032\x072\u022E\n2\f2\x0E2\u0231\v2\x033\x033\x033\x033\x034\x034\x03" + + "5\x035\x055\u023B\n5\x036\x036\x037\x037\x038\x038\x039\x039\x03:\x03" + + ":\x03:\x03;\x03;\x03;\x03;\x03<\x03<\x03<\x03<\x05<\u0250\n<\x03<\x02" + + "\x02\x06\x04\x12\x14 =\x02\x02\x04\x02\x06\x02\b\x02\n\x02\f\x02\x0E\x02" + + "\x10\x02\x12\x02\x14\x02\x16\x02\x18\x02\x1A\x02\x1C\x02\x1E\x02 \x02" + + "\"\x02$\x02&\x02(\x02*\x02,\x02.\x020\x022\x024\x026\x028\x02:\x02<\x02" + + ">\x02@\x02B\x02D\x02F\x02H\x02J\x02L\x02N\x02P\x02R\x02T\x02V\x02X\x02" + + "Z\x02\\\x02^\x02`\x02b\x02d\x02f\x02h\x02j\x02l\x02n\x02p\x02r\x02t\x02" + + "v\x02\x02\x07\x03\x0256\x03\x0279\x03\x02NO\x03\x02GH\x04\x0277AB\x02" + + "\u026F\x02x\x03\x02\x02\x02\x04{\x03\x02\x02\x02\x06\x8A\x03\x02\x02\x02" + + "\b\x99\x03\x02\x02\x02\n\x9B\x03\x02\x02\x02\f\xAF\x03\x02\x02\x02\x0E" + + "\xB3\x03\x02\x02\x02\x10\xB6\x03\x02\x02\x02\x12\xE6\x03\x02\x02\x02\x14" + + "\xF7\x03\x02\x02\x02\x16\u0112\x03\x02\x02\x02\x18\u0116\x03\x02\x02\x02" + + "\x1A\u0118\x03\x02\x02\x02\x1C\u011C\x03\x02\x02\x02\x1E\u012A\x03\x02" + + "\x02\x02 \u013E\x03\x02\x02\x02\"\u015F\x03\x02\x02\x02$\u0161\x03\x02" + + "\x02\x02&\u0164\x03\x02\x02\x02(\u0171\x03\x02\x02\x02*\u0173\x03\x02" + + "\x02\x02,\u0175\x03\x02\x02\x02.\u0177\x03\x02\x02\x020\u0183\x03\x02" + + "\x02\x022\u018F\x03\x02\x02\x024\u0192\x03\x02\x02\x026\u019A\x03\x02" + + "\x02\x028\u019C\x03\x02\x02\x02:\u01A1\x03\x02\x02\x02<\u01AB\x03\x02" + + "\x02\x02>\u01AD\x03\x02\x02\x02@\u01B5\x03\x02\x02\x02B\u01BD\x03\x02" + + "\x02\x02D\u01BF\x03\x02\x02\x02F\u01C1\x03\x02\x02\x02H\u01E8\x03\x02" + + "\x02\x02J\u01EC\x03\x02\x02\x02L\u01EE\x03\x02\x02\x02N\u01F1\x03\x02" + + "\x02\x02P\u01FA\x03\x02\x02\x02R\u0202\x03\x02\x02\x02T\u0205\x03\x02" + + "\x02\x02V\u0208\x03\x02\x02\x02X\u020B\x03\x02\x02\x02Z\u0213\x03\x02" + + "\x02\x02\\\u021C\x03\x02\x02\x02^\u0220\x03\x02\x02\x02`\u0226\x03\x02" + + "\x02\x02b\u022A\x03\x02\x02\x02d\u0232\x03\x02\x02\x02f\u0236\x03\x02" + + "\x02\x02h\u023A\x03\x02\x02\x02j\u023C\x03\x02\x02\x02l\u023E\x03\x02" + + "\x02\x02n\u0240\x03\x02\x02\x02p\u0242\x03\x02\x02\x02r\u0244\x03\x02" + + "\x02\x02t\u0247\x03\x02\x02\x02v\u024F\x03\x02\x02\x02xy\x05\x04\x03\x02" + + "yz\x07\x02\x02\x03z\x03\x03\x02\x02\x02{|\b\x03\x01\x02|}\x05\x06\x04" + + "\x02}\x83\x03\x02\x02\x02~\x7F\f\x03\x02\x02\x7F\x80\x07\x1A\x02\x02\x80" + + "\x82\x05\b\x05\x02\x81~\x03\x02\x02\x02\x82\x85\x03\x02\x02\x02\x83\x81" + + "\x03\x02\x02\x02\x83\x84\x03\x02\x02\x02\x84\x05\x03\x02\x02\x02\x85\x83" + + "\x03\x02\x02\x02\x86\x8B\x05r:\x02\x87\x8B\x05.\x18\x02\x88\x8B\x05$\x13" + + "\x02\x89\x8B\x05v<\x02\x8A\x86\x03\x02\x02\x02\x8A\x87\x03\x02\x02\x02" + + "\x8A\x88\x03\x02\x02\x02\x8A\x89\x03\x02\x02\x02\x8B\x07\x03\x02\x02\x02" + + "\x8C\x9A\x052\x1A\x02\x8D\x9A\x05L\'\x02\x8E\x9A\x05R*\x02\x8F\x9A\x05" + + "T+\x02\x90\x9A\x05Z.\x02\x91\x9A\x05V,\x02\x92\x9A\x05^0\x02\x93\x9A\x05" + + "`1\x02\x94\x9A\x05N(\x02\x95\x9A\x054\x1B\x02\x96\x9A\x05\x10\t\x02\x97" + + "\x9A\x05\x0E\b\x02\x98\x9A\x05\n\x06\x02\x99\x8C\x03\x02\x02\x02\x99\x8D" + + "\x03\x02\x02\x02\x99\x8E\x03\x02\x02\x02\x99\x8F\x03\x02\x02\x02\x99\x90" + + "\x03\x02\x02\x02\x99\x91\x03\x02\x02\x02\x99\x92\x03\x02\x02\x02\x99\x93" + + "\x03\x02\x02\x02\x99\x94\x03\x02\x02\x02\x99\x95\x03\x02\x02\x02\x99\x96" + + "\x03\x02\x02\x02\x99\x97\x03\x02\x02\x02\x99\x98\x03\x02\x02\x02\x9A\t" + + "\x03\x02\x02\x02\x9B\x9C\x07\x12\x02\x02\x9C\x9F\x058\x1D\x02\x9D\x9E" + + "\x07L\x02\x02\x9E\xA0\x05*\x16\x02\x9F\x9D\x03\x02\x02\x02\x9F\xA0\x03" + + "\x02\x02\x02\xA0\xAA\x03\x02\x02\x02\xA1\xA2\x07M\x02\x02\xA2\xA7\x05" + + "\f\x07\x02\xA3\xA4\x07\"\x02\x02\xA4\xA6\x05\f\x07\x02\xA5\xA3\x03\x02" + + "\x02\x02\xA6\xA9\x03\x02\x02\x02\xA7\xA5\x03\x02\x02\x02\xA7\xA8\x03\x02" + + "\x02\x02\xA8\xAB\x03\x02\x02\x02\xA9\xA7\x03\x02\x02\x02\xAA\xA1\x03\x02" + + "\x02\x02\xAA\xAB\x03\x02\x02\x02\xAB\v\x03\x02\x02\x02\xAC\xAD\x05*\x16" + + "\x02\xAD\xAE\x07!\x02\x02\xAE\xB0\x03\x02\x02\x02\xAF\xAC\x03\x02\x02" + + "\x02\xAF\xB0\x03\x02\x02\x02\xB0\xB1\x03\x02\x02\x02\xB1\xB2\x05*\x16" + + "\x02\xB2\r\x03\x02\x02\x02\xB3\xB4\x07\f\x02\x02\xB4\xB5\x05@!\x02\xB5" + + "\x0F\x03\x02\x02\x02\xB6\xB7\x07\n\x02\x02\xB7\xB8\x05\x12\n\x02\xB8\x11" + + "\x03\x02\x02\x02\xB9\xBA\b\n\x01\x02\xBA\xBB\x07\'\x02\x02\xBB\xE7\x05" + + "\x12\n\n\xBC\xE7\x05\x18\r\x02\xBD\xE7\x05\x16\f\x02\xBE\xC0\x05\x18\r" + + "\x02\xBF\xC1\x07\'\x02\x02\xC0\xBF\x03\x02\x02\x02\xC0\xC1\x03\x02\x02" + + "\x02\xC1\xC2\x03\x02\x02\x02\xC2\xC3\x07*\x02\x02\xC3\xC4\x07$\x02\x02" + + "\xC4\xC9\x05\x18\r\x02\xC5\xC6\x07\"\x02\x02\xC6\xC8\x05\x18\r\x02\xC7" + + "\xC5\x03\x02\x02\x02\xC8\xCB\x03\x02\x02\x02\xC9\xC7\x03\x02\x02\x02\xC9" + + "\xCA\x03\x02\x02\x02\xCA\xCC\x03\x02\x02\x02\xCB\xC9\x03\x02\x02\x02\xCC" + + "\xCD\x07/\x02\x02\xCD\xE7\x03\x02\x02\x02\xCE\xD0\x07\'\x02\x02\xCF\xCE" + + "\x03\x02\x02\x02\xCF\xD0\x03\x02\x02\x02\xD0\xD1\x03\x02\x02\x02\xD1\xD2" + + "\x07@\x02\x02\xD2\xD3\x07$\x02\x02\xD3\xDB\x05> \x02\xD4\xD5\x07\"\x02" + + "\x02\xD5\xD7\x05:\x1E\x02\xD6\xD4\x03\x02\x02\x02\xD7\xDA\x03\x02\x02" + + "\x02\xD8\xD6\x03\x02\x02\x02\xD8\xD9\x03\x02\x02\x02\xD9\xDC\x03\x02\x02" + + "\x02\xDA\xD8\x03\x02\x02\x02\xDB\xD8\x03\x02\x02\x02\xDB\xDC\x03\x02\x02" + + "\x02\xDC\xDD\x03\x02\x02\x02\xDD\xDE\x07/\x02\x02\xDE\xE7\x03\x02\x02" + + "\x02\xDF\xE0\x05\x18\r\x02\xE0\xE2\x07+\x02\x02\xE1\xE3\x07\'\x02\x02" + + "\xE2\xE1\x03\x02\x02\x02\xE2\xE3\x03\x02\x02\x02\xE3\xE4\x03\x02\x02\x02" + + "\xE4\xE5\x07-\x02\x02\xE5\xE7\x03\x02\x02\x02\xE6\xB9\x03\x02\x02\x02" + + "\xE6\xBC\x03\x02\x02\x02\xE6\xBD\x03\x02\x02\x02\xE6\xBE\x03\x02\x02\x02" + + "\xE6\xCF\x03\x02\x02\x02\xE6\xDF\x03\x02\x02\x02\xE7\xF0\x03\x02\x02\x02" + + "\xE8\xE9\f\x07\x02\x02\xE9\xEA\x07 \x02\x02\xEA\xEF\x05\x12\n\b\xEB\xEC" + + "\f\x06\x02\x02\xEC\xED\x07.\x02\x02\xED\xEF\x05\x12\n\x07\xEE\xE8\x03" + + "\x02\x02\x02\xEE\xEB\x03\x02\x02\x02\xEF\xF2\x03\x02\x02\x02\xF0\xEE\x03" + + "\x02\x02\x02\xF0\xF1\x03\x02\x02\x02\xF1\x13\x03\x02\x02\x02\xF2\xF0\x03" + + "\x02\x02\x02\xF3\xF4\b\v\x01\x02\xF4\xF5\x07\'\x02\x02\xF5\xF8\x05\x14" + + "\v\x06\xF6\xF8\x05\x18\r\x02\xF7\xF3\x03\x02\x02\x02\xF7\xF6\x03\x02\x02" + + "\x02\xF8\u0101\x03\x02\x02\x02\xF9\xFA\f\x04\x02\x02\xFA\xFB\x07 \x02" + + "\x02\xFB\u0100\x05\x14\v\x05\xFC\xFD\f\x03\x02\x02\xFD\xFE\x07.\x02\x02" + + "\xFE\u0100\x05\x14\v\x04\xFF\xF9\x03\x02\x02\x02\xFF\xFC\x03\x02\x02\x02" + + "\u0100\u0103\x03\x02\x02\x02\u0101\xFF\x03\x02\x02\x02\u0101\u0102\x03" + + "\x02\x02\x02\u0102\x15\x03\x02\x02\x02\u0103\u0101\x03\x02\x02\x02\u0104" + + "\u0106\x05\x18\r\x02\u0105\u0107\x07\'\x02\x02\u0106\u0105\x03\x02\x02" + + "\x02\u0106\u0107\x03\x02\x02\x02\u0107\u0108\x03\x02\x02\x02\u0108\u0109" + + "\x07(\x02\x02\u0109\u010A\x05n8\x02\u010A\u0113\x03\x02\x02\x02\u010B" + + "\u010D\x05\x18\r\x02\u010C\u010E\x07\'\x02\x02\u010D\u010C\x03\x02\x02" + + "\x02\u010D\u010E\x03\x02\x02\x02\u010E\u010F\x03\x02\x02\x02\u010F\u0110" + + "\x07)\x02\x02\u0110\u0111\x05n8\x02\u0111\u0113\x03\x02\x02\x02\u0112" + + "\u0104\x03\x02\x02\x02\u0112\u010B\x03\x02\x02\x02\u0113\x17\x03\x02\x02" + + "\x02\u0114\u0117\x05 \x11\x02\u0115\u0117\x05\x1A\x0E\x02\u0116\u0114" + + "\x03\x02\x02\x02\u0116\u0115\x03\x02\x02\x02\u0117\x19\x03\x02\x02\x02" + + "\u0118\u0119\x05 \x11\x02\u0119\u011A\x05p9\x02\u011A\u011B\x05 \x11\x02" + + "\u011B\x1B\x03\x02\x02\x02\u011C\u011D\x05F$\x02\u011D\u0126\x07$\x02" + + "\x02\u011E\u0123\x05:\x1E\x02\u011F\u0120\x07\"\x02\x02\u0120\u0122\x05" + + ":\x1E\x02\u0121\u011F\x03\x02\x02\x02\u0122\u0125\x03\x02\x02\x02\u0123" + + "\u0121\x03\x02\x02\x02\u0123\u0124\x03\x02\x02\x02\u0124\u0127\x03\x02" + + "\x02\x02\u0125\u0123\x03\x02\x02\x02\u0126\u011E\x03\x02\x02\x02\u0126" + + "\u0127\x03\x02\x02\x02\u0127\u0128\x03\x02\x02\x02\u0128\u0129\x07/\x02" + + "\x02\u0129\x1D\x03\x02\x02\x02\u012A\u012B\x05D#\x02\u012B\u0134\x07$" + + "\x02\x02\u012C\u0131\x05<\x1F\x02\u012D\u012E\x07\"\x02\x02\u012E\u0130" + + "\x05<\x1F\x02\u012F\u012D\x03\x02\x02\x02\u0130\u0133\x03\x02\x02\x02" + + "\u0131\u012F\x03\x02\x02\x02\u0131\u0132\x03\x02\x02\x02\u0132\u0135\x03" + + "\x02\x02\x02\u0133\u0131\x03\x02\x02\x02\u0134\u012C\x03\x02\x02\x02\u0134" + + "\u0135\x03\x02\x02\x02\u0135\u0136\x03\x02\x02\x02\u0136\u0137\x07/\x02" + + "\x02\u0137\x1F\x03\x02\x02\x02\u0138\u0139\b\x11\x01\x02\u0139\u013F\x05" + + "\"\x12\x02\u013A\u013F\x05\x1C\x0F\x02\u013B\u013F\x05\x1E\x10\x02\u013C" + + "\u013D\t\x02\x02\x02\u013D\u013F\x05 \x11\x05\u013E\u0138\x03\x02\x02" + + "\x02\u013E\u013A\x03\x02\x02\x02\u013E\u013B\x03\x02\x02\x02\u013E\u013C" + + "\x03\x02\x02\x02\u013F\u0148\x03\x02\x02\x02\u0140\u0141\f\x04\x02\x02" + + "\u0141\u0142\t\x03\x02\x02\u0142\u0147\x05 \x11\x05\u0143\u0144\f\x03" + + "\x02\x02\u0144\u0145\t\x02\x02\x02\u0145\u0147\x05 \x11\x04\u0146\u0140" + + "\x03\x02\x02\x02\u0146\u0143\x03\x02\x02\x02\u0147\u014A\x03\x02\x02\x02" + + "\u0148\u0146\x03\x02\x02\x02\u0148\u0149\x03\x02\x02\x02\u0149!\x03\x02" + + "\x02\x02\u014A\u0148\x03\x02\x02\x02\u014B\u0160\x05H%\x02\u014C\u0160" + + "\x05> \x02\u014D\u014E\x07$\x02\x02\u014E\u014F\x05\x14\v\x02\u014F\u0150" + + "\x07/\x02\x02\u0150\u0160\x03\x02\x02\x02\u0151\u0152\x05B\"\x02\u0152" + + "\u015B\x07$\x02\x02\u0153\u0158\x05\x14\v\x02\u0154\u0155\x07\"\x02\x02" + + "\u0155\u0157\x05\x14\v\x02\u0156\u0154\x03\x02\x02\x02\u0157\u015A\x03" + + "\x02\x02\x02\u0158\u0156\x03\x02\x02\x02\u0158\u0159\x03\x02\x02\x02\u0159" + + "\u015C\x03\x02\x02\x02\u015A\u0158\x03\x02\x02\x02\u015B\u0153\x03\x02" + + "\x02\x02\u015B\u015C\x03\x02\x02\x02\u015C\u015D\x03\x02\x02\x02\u015D" + + "\u015E\x07/\x02\x02\u015E\u0160\x03\x02\x02\x02\u015F\u014B\x03\x02\x02" + + "\x02\u015F\u014C\x03\x02\x02\x02\u015F\u014D\x03\x02\x02\x02\u015F\u0151" + + "\x03\x02\x02\x02\u0160#\x03\x02\x02\x02\u0161\u0162\x07\b\x02\x02\u0162" + + "\u0163\x05&\x14\x02\u0163%\x03\x02\x02\x02\u0164\u0169\x05(\x15\x02\u0165" + + "\u0166\x07\"\x02\x02\u0166\u0168\x05(\x15\x02\u0167\u0165\x03\x02\x02" + + "\x02\u0168\u016B\x03\x02\x02\x02\u0169\u0167\x03\x02\x02\x02\u0169\u016A" + + "\x03\x02\x02\x02\u016A\'\x03\x02\x02\x02\u016B\u0169\x03\x02\x02\x02\u016C" + + "\u0172\x05\x14\v\x02\u016D\u016E\x05,\x17\x02\u016E\u016F\x07!\x02\x02" + + "\u016F\u0170\x05\x14\v\x02\u0170\u0172\x03\x02\x02\x02\u0171\u016C\x03" + + "\x02\x02\x02\u0171\u016D\x03\x02\x02\x02\u0172)\x03\x02\x02\x02\u0173" + + "\u0174\t\x04\x02\x02\u0174+\x03\x02\x02\x02\u0175\u0176\x05B\"\x02\u0176" + + "-\x03\x02\x02\x02\u0177\u0178\x07\x07\x02\x02\u0178\u017D\x056\x1C\x02" + + "\u0179\u017A\x07\"\x02\x02\u017A\u017C\x056\x1C\x02\u017B\u0179\x03\x02" + + "\x02\x02\u017C\u017F\x03\x02\x02\x02\u017D\u017B\x03\x02\x02\x02\u017D" + + "\u017E\x03\x02\x02\x02\u017E\u0181\x03\x02\x02\x02\u017F\u017D\x03\x02" + + "\x02\x02\u0180\u0182\x050\x19\x02\u0181\u0180\x03\x02\x02\x02\u0181\u0182" + + "\x03\x02\x02\x02\u0182/\x03\x02\x02\x02\u0183\u0184\x07%\x02\x02\u0184" + + "\u0185\x07F\x02\x02\u0185\u018A\x056\x1C\x02\u0186\u0187\x07\"\x02\x02" + + "\u0187\u0189\x056\x1C\x02\u0188\u0186\x03\x02\x02\x02\u0189\u018C\x03" + + "\x02\x02\x02\u018A\u0188\x03\x02\x02\x02\u018A\u018B\x03\x02\x02\x02\u018B" + + "\u018D\x03\x02\x02\x02\u018C\u018A\x03\x02\x02\x02\u018D\u018E\x07&\x02" + + "\x02\u018E1\x03\x02\x02\x02\u018F\u0190\x07\x05\x02\x02\u0190\u0191\x05" + + "&\x14\x02\u01913\x03\x02\x02\x02\u0192\u0194\x07\t\x02\x02\u0193\u0195" + + "\x05&\x14\x02\u0194\u0193\x03\x02\x02\x02\u0194\u0195\x03\x02\x02\x02" + + "\u0195\u0198\x03\x02\x02\x02\u0196\u0197\x07\x1E\x02\x02\u0197\u0199\x05" + + "@!\x02\u0198\u0196\x03\x02\x02\x02\u0198\u0199\x03\x02\x02\x02\u01995" + + "\x03\x02\x02\x02\u019A\u019B\t\x05\x02\x02\u019B7\x03\x02\x02\x02\u019C" + + "\u019D\t\x04\x02\x02\u019D9\x03\x02\x02\x02\u019E\u01A2\x05> \x02\u019F" + + "\u01A2\x05n8\x02\u01A0\u01A2\x05h5\x02\u01A1\u019E\x03\x02\x02\x02\u01A1" + + "\u019F\x03\x02\x02\x02\u01A1\u01A0\x03\x02\x02\x02\u01A2;\x03\x02\x02" + + "\x02\u01A3\u01AC\x05> \x02\u01A4\u01AC\x05n8\x02\u01A5\u01AC\x05h5\x02" + + "\u01A6\u01AC\x05 \x11\x02\u01A7\u01A8\x05h5\x02\u01A8\u01A9\x07\x1F\x02" + + "\x02\u01A9\u01AC\x03\x02\x02\x02\u01AA\u01AC\x05\x1A\x0E\x02\u01AB\u01A3" + + "\x03\x02\x02\x02\u01AB\u01A4\x03\x02\x02\x02\u01AB\u01A5\x03\x02\x02\x02" + + "\u01AB\u01A6\x03\x02\x02\x02\u01AB\u01A7\x03\x02\x02\x02\u01AB\u01AA\x03" + + "\x02\x02\x02\u01AC=\x03\x02\x02\x02\u01AD\u01B2\x05B\"\x02\u01AE\u01AF" + + "\x07#\x02\x02\u01AF\u01B1\x05B\"\x02\u01B0\u01AE\x03\x02\x02\x02\u01B1" + + "\u01B4\x03\x02\x02\x02\u01B2\u01B0\x03\x02\x02\x02\u01B2\u01B3\x03\x02" + + "\x02\x02\u01B3?\x03\x02\x02\x02\u01B4\u01B2\x03\x02\x02\x02\u01B5\u01BA" + + "\x05> \x02\u01B6\u01B7\x07\"\x02\x02\u01B7\u01B9\x05> \x02\u01B8\u01B6" + + "\x03\x02\x02\x02\u01B9\u01BC\x03\x02\x02\x02\u01BA\u01B8\x03\x02\x02\x02" + + "\u01BA\u01BB\x03\x02\x02\x02\u01BBA\x03\x02\x02\x02\u01BC\u01BA\x03\x02" + + "\x02\x02\u01BD\u01BE\t\x06\x02\x02\u01BEC\x03\x02\x02\x02\u01BF\u01C0" + + "\x07>\x02\x02\u01C0E\x03\x02\x02\x02\u01C1\u01C2\x07?\x02\x02\u01C2G\x03" + + "\x02\x02\x02\u01C3\u01E9\x07-\x02\x02\u01C4\u01E9\x05J&\x02\u01C5\u01E9" + + "\x05f4\x02\u01C6\u01E9\x05n8\x02\u01C7\u01C8\x07%\x02\x02\u01C8\u01CD" + + "\x05J&\x02\u01C9\u01CA\x07\"\x02\x02\u01CA\u01CC\x05J&\x02\u01CB\u01C9" + + "\x03\x02\x02\x02\u01CC\u01CF\x03\x02\x02\x02\u01CD\u01CB\x03\x02\x02\x02" + + "\u01CD\u01CE\x03\x02\x02\x02\u01CE\u01D0\x03\x02\x02\x02\u01CF\u01CD\x03" + + "\x02\x02\x02\u01D0\u01D1\x07&\x02\x02\u01D1\u01E9\x03\x02\x02\x02\u01D2" + + "\u01D3\x07%\x02\x02\u01D3\u01D8\x05f4\x02\u01D4\u01D5\x07\"\x02\x02\u01D5" + + "\u01D7\x05f4\x02\u01D6\u01D4\x03\x02\x02\x02\u01D7\u01DA\x03\x02\x02\x02" + + "\u01D8\u01D6\x03\x02\x02\x02\u01D8\u01D9\x03\x02\x02\x02\u01D9\u01DB\x03" + + "\x02\x02\x02\u01DA\u01D8\x03\x02\x02\x02\u01DB\u01DC\x07&\x02\x02\u01DC" + + "\u01E9\x03\x02\x02\x02\u01DD\u01DE\x07%\x02\x02\u01DE\u01E3\x05n8\x02" + + "\u01DF\u01E0\x07\"\x02\x02\u01E0\u01E2\x05n8\x02\u01E1\u01DF\x03\x02\x02" + + "\x02\u01E2\u01E5\x03\x02\x02\x02\u01E3\u01E1\x03\x02\x02\x02\u01E3\u01E4" + + "\x03\x02\x02\x02\u01E4\u01E6\x03\x02\x02\x02\u01E5\u01E3\x03\x02\x02\x02" + + "\u01E6\u01E7\x07&\x02\x02\u01E7\u01E9\x03\x02\x02\x02\u01E8\u01C3\x03" + + "\x02\x02\x02\u01E8\u01C4\x03\x02\x02\x02\u01E8\u01C5\x03\x02\x02\x02\u01E8" + + "\u01C6\x03\x02\x02\x02\u01E8\u01C7\x03\x02\x02\x02\u01E8\u01D2\x03\x02" + + "\x02\x02\u01E8\u01DD\x03\x02\x02\x02\u01E9I\x03\x02\x02\x02\u01EA\u01ED" + + "\x05j6\x02\u01EB\u01ED\x05l7\x02\u01EC\u01EA\x03\x02\x02\x02\u01EC\u01EB" + + "\x03\x02\x02\x02\u01EDK\x03\x02\x02\x02\u01EE\u01EF\x07\r\x02\x02\u01EF" + + "\u01F0\x07\x1C\x02\x02\u01F0M\x03\x02\x02\x02\u01F1\u01F2\x07\v\x02\x02" + + "\u01F2\u01F7\x05P)\x02\u01F3\u01F4\x07\"\x02\x02\u01F4\u01F6\x05P)\x02" + + "\u01F5\u01F3\x03\x02\x02\x02\u01F6\u01F9\x03\x02\x02\x02\u01F7\u01F5\x03" + + "\x02\x02\x02\u01F7\u01F8\x03\x02\x02\x02\u01F8O\x03\x02\x02\x02\u01F9" + + "\u01F7\x03\x02\x02\x02\u01FA\u01FC\x05\x14\v\x02\u01FB\u01FD\x07;\x02" + + "\x02\u01FC\u01FB\x03\x02\x02\x02\u01FC\u01FD\x03\x02\x02\x02\u01FD\u0200" + + "\x03\x02\x02\x02\u01FE\u01FF\x07<\x02\x02\u01FF\u0201\x07=\x02\x02\u0200" + + "\u01FE\x03\x02\x02\x02\u0200\u0201\x03\x02\x02\x02\u0201Q\x03\x02\x02" + + "\x02\u0202\u0203\x07\x0E\x02\x02\u0203\u0204\x05@!\x02\u0204S\x03\x02" + + "\x02\x02\u0205\u0206\x07\x13\x02\x02\u0206\u0207\x05@!\x02\u0207U\x03" + + "\x02\x02\x02\u0208\u0209\x07\x0F\x02\x02\u0209\u020A\x05@!\x02\u020AW" + + "\x03\x02\x02\x02\u020B\u0210\x05B\"\x02\u020C\u020D\x07#\x02\x02\u020D" + + "\u020F\x05B\"\x02\u020E\u020C\x03\x02\x02\x02\u020F\u0212\x03\x02\x02" + + "\x02\u0210\u020E\x03\x02\x02\x02\u0210\u0211\x03\x02\x02\x02\u0211Y\x03" + + "\x02\x02\x02\u0212\u0210\x03\x02\x02\x02\u0213\u0214\x07\x10\x02\x02\u0214" + + "\u0219\x05\\/\x02\u0215\u0216\x07\"\x02\x02\u0216\u0218\x05\\/\x02\u0217" + + "\u0215\x03\x02\x02\x02\u0218\u021B\x03\x02\x02\x02\u0219\u0217\x03\x02" + + "\x02\x02\u0219\u021A\x03\x02\x02\x02\u021A[\x03\x02\x02\x02\u021B\u0219" + + "\x03\x02\x02\x02\u021C\u021D\x05> \x02\u021D\u021E\x07,\x02\x02\u021E" + + "\u021F\x05X-\x02\u021F]\x03\x02\x02\x02\u0220\u0221\x07\x03\x02\x02\u0221" + + "\u0222\x05@!\x02\u0222\u0224\x05n8\x02\u0223\u0225\x05b2\x02\u0224\u0223" + + "\x03\x02\x02\x02\u0224\u0225\x03\x02\x02\x02\u0225_\x03\x02\x02\x02\u0226" + + "\u0227\x07\x04\x02\x02\u0227\u0228\x05@!\x02\u0228\u0229\x05n8\x02\u0229" + + "a\x03\x02\x02\x02\u022A\u022F\x05d3\x02\u022B\u022C\x07\"\x02\x02\u022C" + + "\u022E\x05d3\x02\u022D\u022B\x03\x02\x02\x02\u022E\u0231\x03\x02\x02\x02" + + "\u022F\u022D\x03\x02\x02\x02\u022F\u0230\x03\x02\x02\x02\u0230c\x03\x02" + + "\x02\x02\u0231\u022F\x03\x02\x02\x02\u0232\u0233\x05B\"\x02\u0233\u0234" + + "\x07!\x02\x02\u0234\u0235\x05H%\x02\u0235e\x03\x02\x02\x02\u0236\u0237" + + "\x073\x02\x02\u0237g\x03\x02\x02\x02\u0238\u023B\x07\x1D\x02\x02\u0239" + + "\u023B\x07\x1C\x02"; + private static readonly _serializedATNSegment1: string = + "\x02\u023A\u0238\x03\x02\x02\x02\u023A\u0239\x03\x02\x02\x02\u023Bi\x03" + + "\x02\x02\x02\u023C\u023D\x07\x1D\x02\x02\u023Dk\x03\x02\x02\x02\u023E" + + "\u023F\x07\x1C\x02\x02\u023Fm\x03\x02\x02\x02\u0240\u0241\x07\x1B\x02" + + "\x02\u0241o\x03\x02\x02\x02\u0242\u0243\x074\x02\x02\u0243q\x03\x02\x02" + + "\x02\u0244\u0245\x07\x06\x02\x02\u0245\u0246\x05t;\x02\u0246s\x03\x02" + + "\x02\x02\u0247\u0248\x07%\x02\x02\u0248\u0249\x05\x04\x03\x02\u0249\u024A" + + "\x07&\x02\x02\u024Au\x03\x02\x02\x02\u024B\u024C\x07\x11\x02\x02\u024C" + + "\u0250\x071\x02\x02\u024D\u024E\x07\x11\x02\x02\u024E\u0250\x072\x02\x02" + + "\u024F\u024B\x03\x02\x02\x02\u024F\u024D\x03\x02\x02\x02\u0250w\x03\x02" + + "\x02\x02<\x83\x8A\x99\x9F\xA7\xAA\xAF\xC0\xC9\xCF\xD8\xDB\xE2\xE6\xEE" + + "\xF0\xF7\xFF\u0101\u0106\u010D\u0112\u0116\u0123\u0126\u0131\u0134\u013E" + + "\u0146\u0148\u0158\u015B\u015F\u0169\u0171\u017D\u0181\u018A\u0194\u0198" + + "\u01A1\u01AB\u01B2\u01BA\u01CD\u01D8\u01E3\u01E8\u01EC\u01F7\u01FC\u0200" + + "\u0210\u0219\u0224\u022F\u023A\u024F"; + public static readonly _serializedATN: string = Utils.join( + [ + esql_parser._serializedATNSegment0, + esql_parser._serializedATNSegment1, + ], + "", + ); + public static __ATN: ATN; + public static get _ATN(): ATN { + if (!esql_parser.__ATN) { + esql_parser.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(esql_parser._serializedATN)); + } + + return esql_parser.__ATN; + } + +} + +export class SingleStatementContext extends ParserRuleContext { + public query(): QueryContext { + return this.getRuleContext(0, QueryContext); + } + public EOF(): TerminalNode { return this.getToken(esql_parser.EOF, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_singleStatement; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterSingleStatement) { + listener.enterSingleStatement(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitSingleStatement) { + listener.exitSingleStatement(this); + } + } +} + + +export class QueryContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_query; } + public copyFrom(ctx: QueryContext): void { + super.copyFrom(ctx); + } +} +export class SingleCommandQueryContext extends QueryContext { + public sourceCommand(): SourceCommandContext { + return this.getRuleContext(0, SourceCommandContext); + } + constructor(ctx: QueryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterSingleCommandQuery) { + listener.enterSingleCommandQuery(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitSingleCommandQuery) { + listener.exitSingleCommandQuery(this); + } + } +} +export class CompositeQueryContext extends QueryContext { + public query(): QueryContext { + return this.getRuleContext(0, QueryContext); + } + public PIPE(): TerminalNode { return this.getToken(esql_parser.PIPE, 0); } + public processingCommand(): ProcessingCommandContext { + return this.getRuleContext(0, ProcessingCommandContext); + } + constructor(ctx: QueryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterCompositeQuery) { + listener.enterCompositeQuery(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitCompositeQuery) { + listener.exitCompositeQuery(this); + } + } +} + + +export class SourceCommandContext extends ParserRuleContext { + public explainCommand(): ExplainCommandContext | undefined { + return this.tryGetRuleContext(0, ExplainCommandContext); + } + public fromCommand(): FromCommandContext | undefined { + return this.tryGetRuleContext(0, FromCommandContext); + } + public rowCommand(): RowCommandContext | undefined { + return this.tryGetRuleContext(0, RowCommandContext); + } + public showCommand(): ShowCommandContext | undefined { + return this.tryGetRuleContext(0, ShowCommandContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_sourceCommand; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterSourceCommand) { + listener.enterSourceCommand(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitSourceCommand) { + listener.exitSourceCommand(this); + } + } +} + + +export class ProcessingCommandContext extends ParserRuleContext { + public evalCommand(): EvalCommandContext | undefined { + return this.tryGetRuleContext(0, EvalCommandContext); + } + public limitCommand(): LimitCommandContext | undefined { + return this.tryGetRuleContext(0, LimitCommandContext); + } + public projectCommand(): ProjectCommandContext | undefined { + return this.tryGetRuleContext(0, ProjectCommandContext); + } + public keepCommand(): KeepCommandContext | undefined { + return this.tryGetRuleContext(0, KeepCommandContext); + } + public renameCommand(): RenameCommandContext | undefined { + return this.tryGetRuleContext(0, RenameCommandContext); + } + public dropCommand(): DropCommandContext | undefined { + return this.tryGetRuleContext(0, DropCommandContext); + } + public dissectCommand(): DissectCommandContext | undefined { + return this.tryGetRuleContext(0, DissectCommandContext); + } + public grokCommand(): GrokCommandContext | undefined { + return this.tryGetRuleContext(0, GrokCommandContext); + } + public sortCommand(): SortCommandContext | undefined { + return this.tryGetRuleContext(0, SortCommandContext); + } + public statsCommand(): StatsCommandContext | undefined { + return this.tryGetRuleContext(0, StatsCommandContext); + } + public whereCommand(): WhereCommandContext | undefined { + return this.tryGetRuleContext(0, WhereCommandContext); + } + public mvExpandCommand(): MvExpandCommandContext | undefined { + return this.tryGetRuleContext(0, MvExpandCommandContext); + } + public enrichCommand(): EnrichCommandContext | undefined { + return this.tryGetRuleContext(0, EnrichCommandContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_processingCommand; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterProcessingCommand) { + listener.enterProcessingCommand(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitProcessingCommand) { + listener.exitProcessingCommand(this); + } + } +} + + +export class EnrichCommandContext extends ParserRuleContext { + public _policyName: EnrichIdentifierContext; + public _matchField: EnrichFieldIdentifierContext; + public ENRICH(): TerminalNode { return this.getToken(esql_parser.ENRICH, 0); } + public enrichIdentifier(): EnrichIdentifierContext { + return this.getRuleContext(0, EnrichIdentifierContext); + } + public ON(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ON, 0); } + public WITH(): TerminalNode | undefined { return this.tryGetToken(esql_parser.WITH, 0); } + public enrichWithClause(): EnrichWithClauseContext[]; + public enrichWithClause(i: number): EnrichWithClauseContext; + public enrichWithClause(i?: number): EnrichWithClauseContext | EnrichWithClauseContext[] { + if (i === undefined) { + return this.getRuleContexts(EnrichWithClauseContext); + } else { + return this.getRuleContext(i, EnrichWithClauseContext); + } + } + public enrichFieldIdentifier(): EnrichFieldIdentifierContext | undefined { + return this.tryGetRuleContext(0, EnrichFieldIdentifierContext); + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.COMMA); + } else { + return this.getToken(esql_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_enrichCommand; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterEnrichCommand) { + listener.enterEnrichCommand(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitEnrichCommand) { + listener.exitEnrichCommand(this); + } + } +} + + +export class EnrichWithClauseContext extends ParserRuleContext { + public _newName: EnrichFieldIdentifierContext; + public _enrichField: EnrichFieldIdentifierContext; + public enrichFieldIdentifier(): EnrichFieldIdentifierContext[]; + public enrichFieldIdentifier(i: number): EnrichFieldIdentifierContext; + public enrichFieldIdentifier(i?: number): EnrichFieldIdentifierContext | EnrichFieldIdentifierContext[] { + if (i === undefined) { + return this.getRuleContexts(EnrichFieldIdentifierContext); + } else { + return this.getRuleContext(i, EnrichFieldIdentifierContext); + } + } + public ASSIGN(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ASSIGN, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_enrichWithClause; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterEnrichWithClause) { + listener.enterEnrichWithClause(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitEnrichWithClause) { + listener.exitEnrichWithClause(this); + } + } +} + + +export class MvExpandCommandContext extends ParserRuleContext { + public MV_EXPAND(): TerminalNode { return this.getToken(esql_parser.MV_EXPAND, 0); } + public qualifiedNames(): QualifiedNamesContext { + return this.getRuleContext(0, QualifiedNamesContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_mvExpandCommand; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterMvExpandCommand) { + listener.enterMvExpandCommand(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitMvExpandCommand) { + listener.exitMvExpandCommand(this); + } + } +} + + +export class WhereCommandContext extends ParserRuleContext { + public WHERE(): TerminalNode { return this.getToken(esql_parser.WHERE, 0); } + public whereBooleanExpression(): WhereBooleanExpressionContext { + return this.getRuleContext(0, WhereBooleanExpressionContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_whereCommand; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterWhereCommand) { + listener.enterWhereCommand(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitWhereCommand) { + listener.exitWhereCommand(this); + } + } +} + + +export class WhereBooleanExpressionContext extends ParserRuleContext { + public _left: WhereBooleanExpressionContext; + public _operator: Token; + public _right: WhereBooleanExpressionContext; + public NOT(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NOT, 0); } + public whereBooleanExpression(): WhereBooleanExpressionContext[]; + public whereBooleanExpression(i: number): WhereBooleanExpressionContext; + public whereBooleanExpression(i?: number): WhereBooleanExpressionContext | WhereBooleanExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(WhereBooleanExpressionContext); + } else { + return this.getRuleContext(i, WhereBooleanExpressionContext); + } + } + public valueExpression(): ValueExpressionContext[]; + public valueExpression(i: number): ValueExpressionContext; + public valueExpression(i?: number): ValueExpressionContext | ValueExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ValueExpressionContext); + } else { + return this.getRuleContext(i, ValueExpressionContext); + } + } + public regexBooleanExpression(): RegexBooleanExpressionContext | undefined { + return this.tryGetRuleContext(0, RegexBooleanExpressionContext); + } + public AND(): TerminalNode | undefined { return this.tryGetToken(esql_parser.AND, 0); } + public OR(): TerminalNode | undefined { return this.tryGetToken(esql_parser.OR, 0); } + public IN(): TerminalNode | undefined { return this.tryGetToken(esql_parser.IN, 0); } + public LP(): TerminalNode | undefined { return this.tryGetToken(esql_parser.LP, 0); } + public RP(): TerminalNode | undefined { return this.tryGetToken(esql_parser.RP, 0); } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.COMMA); + } else { + return this.getToken(esql_parser.COMMA, i); + } + } + public WHERE_FUNCTIONS(): TerminalNode | undefined { return this.tryGetToken(esql_parser.WHERE_FUNCTIONS, 0); } + public qualifiedName(): QualifiedNameContext | undefined { + return this.tryGetRuleContext(0, QualifiedNameContext); + } + public functionExpressionArgument(): FunctionExpressionArgumentContext[]; + public functionExpressionArgument(i: number): FunctionExpressionArgumentContext; + public functionExpressionArgument(i?: number): FunctionExpressionArgumentContext | FunctionExpressionArgumentContext[] { + if (i === undefined) { + return this.getRuleContexts(FunctionExpressionArgumentContext); + } else { + return this.getRuleContext(i, FunctionExpressionArgumentContext); + } + } + public IS(): TerminalNode | undefined { return this.tryGetToken(esql_parser.IS, 0); } + public NULL(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NULL, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_whereBooleanExpression; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterWhereBooleanExpression) { + listener.enterWhereBooleanExpression(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitWhereBooleanExpression) { + listener.exitWhereBooleanExpression(this); + } + } +} + + +export class BooleanExpressionContext extends ParserRuleContext { + public _left: BooleanExpressionContext; + public _operator: Token; + public _right: BooleanExpressionContext; + public NOT(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NOT, 0); } + public booleanExpression(): BooleanExpressionContext[]; + public booleanExpression(i: number): BooleanExpressionContext; + public booleanExpression(i?: number): BooleanExpressionContext | BooleanExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(BooleanExpressionContext); + } else { + return this.getRuleContext(i, BooleanExpressionContext); + } + } + public valueExpression(): ValueExpressionContext | undefined { + return this.tryGetRuleContext(0, ValueExpressionContext); + } + public AND(): TerminalNode | undefined { return this.tryGetToken(esql_parser.AND, 0); } + public OR(): TerminalNode | undefined { return this.tryGetToken(esql_parser.OR, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_booleanExpression; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterBooleanExpression) { + listener.enterBooleanExpression(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitBooleanExpression) { + listener.exitBooleanExpression(this); + } + } } -export class SingleStatementContext extends ParserRuleContext { - public query(): QueryContext { - return this.getRuleContext(0, QueryContext); + +export class RegexBooleanExpressionContext extends ParserRuleContext { + public _kind: Token; + public _pattern: StringContext; + public valueExpression(): ValueExpressionContext { + return this.getRuleContext(0, ValueExpressionContext); } - public EOF(): TerminalNode { return this.getToken(esql_parser.EOF, 0); } + public LIKE(): TerminalNode | undefined { return this.tryGetToken(esql_parser.LIKE, 0); } + public string(): StringContext { + return this.getRuleContext(0, StringContext); + } + public NOT(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NOT, 0); } + public RLIKE(): TerminalNode | undefined { return this.tryGetToken(esql_parser.RLIKE, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_singleStatement; } + public get ruleIndex(): number { return esql_parser.RULE_regexBooleanExpression; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterSingleStatement) { - listener.enterSingleStatement(this); + if (listener.enterRegexBooleanExpression) { + listener.enterRegexBooleanExpression(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitSingleStatement) { - listener.exitSingleStatement(this); + if (listener.exitRegexBooleanExpression) { + listener.exitRegexBooleanExpression(this); } } } -export class QueryContext extends ParserRuleContext { +export class ValueExpressionContext extends ParserRuleContext { + public operatorExpression(): OperatorExpressionContext | undefined { + return this.tryGetRuleContext(0, OperatorExpressionContext); + } + public comparison(): ComparisonContext | undefined { + return this.tryGetRuleContext(0, ComparisonContext); + } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_query; } - public copyFrom(ctx: QueryContext): void { - super.copyFrom(ctx); + public get ruleIndex(): number { return esql_parser.RULE_valueExpression; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterValueExpression) { + listener.enterValueExpression(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitValueExpression) { + listener.exitValueExpression(this); + } } } -export class SingleCommandQueryContext extends QueryContext { - public sourceCommand(): SourceCommandContext { - return this.getRuleContext(0, SourceCommandContext); + + +export class ComparisonContext extends ParserRuleContext { + public _left: OperatorExpressionContext; + public _right: OperatorExpressionContext; + public comparisonOperator(): ComparisonOperatorContext { + return this.getRuleContext(0, ComparisonOperatorContext); } - constructor(ctx: QueryContext) { - super(ctx.parent, ctx.invokingState); - this.copyFrom(ctx); + public operatorExpression(): OperatorExpressionContext[]; + public operatorExpression(i: number): OperatorExpressionContext; + public operatorExpression(i?: number): OperatorExpressionContext | OperatorExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(OperatorExpressionContext); + } else { + return this.getRuleContext(i, OperatorExpressionContext); + } } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_comparison; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterSingleCommandQuery) { - listener.enterSingleCommandQuery(this); + if (listener.enterComparison) { + listener.enterComparison(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitSingleCommandQuery) { - listener.exitSingleCommandQuery(this); + if (listener.exitComparison) { + listener.exitComparison(this); } } } -export class CompositeQueryContext extends QueryContext { - public query(): QueryContext { - return this.getRuleContext(0, QueryContext); + + +export class MathFnContext extends ParserRuleContext { + public functionIdentifier(): FunctionIdentifierContext { + return this.getRuleContext(0, FunctionIdentifierContext); } - public PIPE(): TerminalNode { return this.getToken(esql_parser.PIPE, 0); } - public processingCommand(): ProcessingCommandContext { - return this.getRuleContext(0, ProcessingCommandContext); + public LP(): TerminalNode { return this.getToken(esql_parser.LP, 0); } + public RP(): TerminalNode { return this.getToken(esql_parser.RP, 0); } + public functionExpressionArgument(): FunctionExpressionArgumentContext[]; + public functionExpressionArgument(i: number): FunctionExpressionArgumentContext; + public functionExpressionArgument(i?: number): FunctionExpressionArgumentContext | FunctionExpressionArgumentContext[] { + if (i === undefined) { + return this.getRuleContexts(FunctionExpressionArgumentContext); + } else { + return this.getRuleContext(i, FunctionExpressionArgumentContext); + } } - constructor(ctx: QueryContext) { - super(ctx.parent, ctx.invokingState); - this.copyFrom(ctx); + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.COMMA); + } else { + return this.getToken(esql_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); } // @Override + public get ruleIndex(): number { return esql_parser.RULE_mathFn; } + // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterCompositeQuery) { - listener.enterCompositeQuery(this); + if (listener.enterMathFn) { + listener.enterMathFn(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitCompositeQuery) { - listener.exitCompositeQuery(this); + if (listener.exitMathFn) { + listener.exitMathFn(this); + } + } +} + + +export class MathEvalFnContext extends ParserRuleContext { + public mathFunctionIdentifier(): MathFunctionIdentifierContext { + return this.getRuleContext(0, MathFunctionIdentifierContext); + } + public LP(): TerminalNode { return this.getToken(esql_parser.LP, 0); } + public RP(): TerminalNode { return this.getToken(esql_parser.RP, 0); } + public mathFunctionExpressionArgument(): MathFunctionExpressionArgumentContext[]; + public mathFunctionExpressionArgument(i: number): MathFunctionExpressionArgumentContext; + public mathFunctionExpressionArgument(i?: number): MathFunctionExpressionArgumentContext | MathFunctionExpressionArgumentContext[] { + if (i === undefined) { + return this.getRuleContexts(MathFunctionExpressionArgumentContext); + } else { + return this.getRuleContext(i, MathFunctionExpressionArgumentContext); + } + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.COMMA); + } else { + return this.getToken(esql_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_mathEvalFn; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterMathEvalFn) { + listener.enterMathEvalFn(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitMathEvalFn) { + listener.exitMathEvalFn(this); + } + } +} + + +export class OperatorExpressionContext extends ParserRuleContext { + public _left: OperatorExpressionContext; + public _operator: Token; + public _right: OperatorExpressionContext; + public primaryExpression(): PrimaryExpressionContext | undefined { + return this.tryGetRuleContext(0, PrimaryExpressionContext); + } + public mathFn(): MathFnContext | undefined { + return this.tryGetRuleContext(0, MathFnContext); + } + public mathEvalFn(): MathEvalFnContext | undefined { + return this.tryGetRuleContext(0, MathEvalFnContext); + } + public operatorExpression(): OperatorExpressionContext[]; + public operatorExpression(i: number): OperatorExpressionContext; + public operatorExpression(i?: number): OperatorExpressionContext | OperatorExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(OperatorExpressionContext); + } else { + return this.getRuleContext(i, OperatorExpressionContext); + } + } + public MINUS(): TerminalNode | undefined { return this.tryGetToken(esql_parser.MINUS, 0); } + public PLUS(): TerminalNode | undefined { return this.tryGetToken(esql_parser.PLUS, 0); } + public ASTERISK(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ASTERISK, 0); } + public SLASH(): TerminalNode | undefined { return this.tryGetToken(esql_parser.SLASH, 0); } + public PERCENT(): TerminalNode | undefined { return this.tryGetToken(esql_parser.PERCENT, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_operatorExpression; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterOperatorExpression) { + listener.enterOperatorExpression(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitOperatorExpression) { + listener.exitOperatorExpression(this); + } + } +} + + +export class PrimaryExpressionContext extends ParserRuleContext { + public constant(): ConstantContext | undefined { + return this.tryGetRuleContext(0, ConstantContext); + } + public qualifiedName(): QualifiedNameContext | undefined { + return this.tryGetRuleContext(0, QualifiedNameContext); + } + public LP(): TerminalNode | undefined { return this.tryGetToken(esql_parser.LP, 0); } + public booleanExpression(): BooleanExpressionContext[]; + public booleanExpression(i: number): BooleanExpressionContext; + public booleanExpression(i?: number): BooleanExpressionContext | BooleanExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(BooleanExpressionContext); + } else { + return this.getRuleContext(i, BooleanExpressionContext); + } + } + public RP(): TerminalNode | undefined { return this.tryGetToken(esql_parser.RP, 0); } + public identifier(): IdentifierContext | undefined { + return this.tryGetRuleContext(0, IdentifierContext); + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.COMMA); + } else { + return this.getToken(esql_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_primaryExpression; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterPrimaryExpression) { + listener.enterPrimaryExpression(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitPrimaryExpression) { + listener.exitPrimaryExpression(this); } } } -export class SourceCommandContext extends ParserRuleContext { - public explainCommand(): ExplainCommandContext | undefined { - return this.tryGetRuleContext(0, ExplainCommandContext); - } - public fromCommand(): FromCommandContext | undefined { - return this.tryGetRuleContext(0, FromCommandContext); - } - public rowCommand(): RowCommandContext | undefined { - return this.tryGetRuleContext(0, RowCommandContext); +export class RowCommandContext extends ParserRuleContext { + public ROW(): TerminalNode { return this.getToken(esql_parser.ROW, 0); } + public fields(): FieldsContext { + return this.getRuleContext(0, FieldsContext); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_sourceCommand; } + public get ruleIndex(): number { return esql_parser.RULE_rowCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterSourceCommand) { - listener.enterSourceCommand(this); + if (listener.enterRowCommand) { + listener.enterRowCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitSourceCommand) { - listener.exitSourceCommand(this); + if (listener.exitRowCommand) { + listener.exitRowCommand(this); } } } -export class ProcessingCommandContext extends ParserRuleContext { - public evalCommand(): EvalCommandContext | undefined { - return this.tryGetRuleContext(0, EvalCommandContext); - } - public limitCommand(): LimitCommandContext | undefined { - return this.tryGetRuleContext(0, LimitCommandContext); - } - public projectCommand(): ProjectCommandContext | undefined { - return this.tryGetRuleContext(0, ProjectCommandContext); - } - public sortCommand(): SortCommandContext | undefined { - return this.tryGetRuleContext(0, SortCommandContext); - } - public statsCommand(): StatsCommandContext | undefined { - return this.tryGetRuleContext(0, StatsCommandContext); +export class FieldsContext extends ParserRuleContext { + public field(): FieldContext[]; + public field(i: number): FieldContext; + public field(i?: number): FieldContext | FieldContext[] { + if (i === undefined) { + return this.getRuleContexts(FieldContext); + } else { + return this.getRuleContext(i, FieldContext); + } } - public whereCommand(): WhereCommandContext | undefined { - return this.tryGetRuleContext(0, WhereCommandContext); + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.COMMA); + } else { + return this.getToken(esql_parser.COMMA, i); + } } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_processingCommand; } + public get ruleIndex(): number { return esql_parser.RULE_fields; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterProcessingCommand) { - listener.enterProcessingCommand(this); + if (listener.enterFields) { + listener.enterFields(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitProcessingCommand) { - listener.exitProcessingCommand(this); + if (listener.exitFields) { + listener.exitFields(this); } } } -export class WhereCommandContext extends ParserRuleContext { - public WHERE(): TerminalNode { return this.getToken(esql_parser.WHERE, 0); } +export class FieldContext extends ParserRuleContext { public booleanExpression(): BooleanExpressionContext { return this.getRuleContext(0, BooleanExpressionContext); } + public userVariable(): UserVariableContext | undefined { + return this.tryGetRuleContext(0, UserVariableContext); + } + public ASSIGN(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ASSIGN, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_whereCommand; } + public get ruleIndex(): number { return esql_parser.RULE_field; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterWhereCommand) { - listener.enterWhereCommand(this); + if (listener.enterField) { + listener.enterField(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitWhereCommand) { - listener.exitWhereCommand(this); + if (listener.exitField) { + listener.exitField(this); } } } -export class BooleanExpressionContext extends ParserRuleContext { - public _left: BooleanExpressionContext; - public _operator: Token; - public _right: BooleanExpressionContext; - public NOT(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NOT, 0); } - public booleanExpression(): BooleanExpressionContext[]; - public booleanExpression(i: number): BooleanExpressionContext; - public booleanExpression(i?: number): BooleanExpressionContext | BooleanExpressionContext[] { - if (i === undefined) { - return this.getRuleContexts(BooleanExpressionContext); - } else { - return this.getRuleContext(i, BooleanExpressionContext); - } - } - public valueExpression(): ValueExpressionContext | undefined { - return this.tryGetRuleContext(0, ValueExpressionContext); - } - public AND(): TerminalNode | undefined { return this.tryGetToken(esql_parser.AND, 0); } - public OR(): TerminalNode | undefined { return this.tryGetToken(esql_parser.OR, 0); } +export class EnrichFieldIdentifierContext extends ParserRuleContext { + public ENR_UNQUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ENR_UNQUOTED_IDENTIFIER, 0); } + public ENR_QUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ENR_QUOTED_IDENTIFIER, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_booleanExpression; } + public get ruleIndex(): number { return esql_parser.RULE_enrichFieldIdentifier; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterBooleanExpression) { - listener.enterBooleanExpression(this); + if (listener.enterEnrichFieldIdentifier) { + listener.enterEnrichFieldIdentifier(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitBooleanExpression) { - listener.exitBooleanExpression(this); + if (listener.exitEnrichFieldIdentifier) { + listener.exitEnrichFieldIdentifier(this); } } } -export class ValueExpressionContext extends ParserRuleContext { - public operatorExpression(): OperatorExpressionContext | undefined { - return this.tryGetRuleContext(0, OperatorExpressionContext); - } - public comparison(): ComparisonContext | undefined { - return this.tryGetRuleContext(0, ComparisonContext); +export class UserVariableContext extends ParserRuleContext { + public identifier(): IdentifierContext { + return this.getRuleContext(0, IdentifierContext); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_valueExpression; } + public get ruleIndex(): number { return esql_parser.RULE_userVariable; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterValueExpression) { - listener.enterValueExpression(this); + if (listener.enterUserVariable) { + listener.enterUserVariable(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitValueExpression) { - listener.exitValueExpression(this); + if (listener.exitUserVariable) { + listener.exitUserVariable(this); } } } -export class ComparisonContext extends ParserRuleContext { - public _left: OperatorExpressionContext; - public _right: OperatorExpressionContext; - public comparisonOperator(): ComparisonOperatorContext { - return this.getRuleContext(0, ComparisonOperatorContext); +export class FromCommandContext extends ParserRuleContext { + public FROM(): TerminalNode { return this.getToken(esql_parser.FROM, 0); } + public sourceIdentifier(): SourceIdentifierContext[]; + public sourceIdentifier(i: number): SourceIdentifierContext; + public sourceIdentifier(i?: number): SourceIdentifierContext | SourceIdentifierContext[] { + if (i === undefined) { + return this.getRuleContexts(SourceIdentifierContext); + } else { + return this.getRuleContext(i, SourceIdentifierContext); + } } - public operatorExpression(): OperatorExpressionContext[]; - public operatorExpression(i: number): OperatorExpressionContext; - public operatorExpression(i?: number): OperatorExpressionContext | OperatorExpressionContext[] { + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { if (i === undefined) { - return this.getRuleContexts(OperatorExpressionContext); + return this.getTokens(esql_parser.COMMA); } else { - return this.getRuleContext(i, OperatorExpressionContext); + return this.getToken(esql_parser.COMMA, i); } } + public metadata(): MetadataContext | undefined { + return this.tryGetRuleContext(0, MetadataContext); + } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_comparison; } + public get ruleIndex(): number { return esql_parser.RULE_fromCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterComparison) { - listener.enterComparison(this); + if (listener.enterFromCommand) { + listener.enterFromCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitComparison) { - listener.exitComparison(this); + if (listener.exitFromCommand) { + listener.exitFromCommand(this); } } } -export class MathFnContext extends ParserRuleContext { - public functionIdentifier(): FunctionIdentifierContext { - return this.getRuleContext(0, FunctionIdentifierContext); - } - public LP(): TerminalNode { return this.getToken(esql_parser.LP, 0); } - public RP(): TerminalNode { return this.getToken(esql_parser.RP, 0); } - public functionExpressionArgument(): FunctionExpressionArgumentContext[]; - public functionExpressionArgument(i: number): FunctionExpressionArgumentContext; - public functionExpressionArgument(i?: number): FunctionExpressionArgumentContext | FunctionExpressionArgumentContext[] { +export class MetadataContext extends ParserRuleContext { + public OPENING_BRACKET(): TerminalNode { return this.getToken(esql_parser.OPENING_BRACKET, 0); } + public METADATA(): TerminalNode { return this.getToken(esql_parser.METADATA, 0); } + public sourceIdentifier(): SourceIdentifierContext[]; + public sourceIdentifier(i: number): SourceIdentifierContext; + public sourceIdentifier(i?: number): SourceIdentifierContext | SourceIdentifierContext[] { if (i === undefined) { - return this.getRuleContexts(FunctionExpressionArgumentContext); + return this.getRuleContexts(SourceIdentifierContext); } else { - return this.getRuleContext(i, FunctionExpressionArgumentContext); + return this.getRuleContext(i, SourceIdentifierContext); } } + public CLOSING_BRACKET(): TerminalNode { return this.getToken(esql_parser.CLOSING_BRACKET, 0); } public COMMA(): TerminalNode[]; public COMMA(i: number): TerminalNode; public COMMA(i?: number): TerminalNode | TerminalNode[] { @@ -2306,241 +4413,236 @@ export class MathFnContext extends ParserRuleContext { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_mathFn; } + public get ruleIndex(): number { return esql_parser.RULE_metadata; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterMathFn) { - listener.enterMathFn(this); + if (listener.enterMetadata) { + listener.enterMetadata(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitMathFn) { - listener.exitMathFn(this); + if (listener.exitMetadata) { + listener.exitMetadata(this); } } } -export class OperatorExpressionContext extends ParserRuleContext { - public _left: OperatorExpressionContext; - public _operator: Token; - public _right: OperatorExpressionContext; - public primaryExpression(): PrimaryExpressionContext | undefined { - return this.tryGetRuleContext(0, PrimaryExpressionContext); - } - public mathFn(): MathFnContext | undefined { - return this.tryGetRuleContext(0, MathFnContext); - } - public operatorExpression(): OperatorExpressionContext[]; - public operatorExpression(i: number): OperatorExpressionContext; - public operatorExpression(i?: number): OperatorExpressionContext | OperatorExpressionContext[] { - if (i === undefined) { - return this.getRuleContexts(OperatorExpressionContext); - } else { - return this.getRuleContext(i, OperatorExpressionContext); - } +export class EvalCommandContext extends ParserRuleContext { + public EVAL(): TerminalNode { return this.getToken(esql_parser.EVAL, 0); } + public fields(): FieldsContext { + return this.getRuleContext(0, FieldsContext); } - public MINUS(): TerminalNode | undefined { return this.tryGetToken(esql_parser.MINUS, 0); } - public PLUS(): TerminalNode | undefined { return this.tryGetToken(esql_parser.PLUS, 0); } - public ASTERISK(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ASTERISK, 0); } - public SLASH(): TerminalNode | undefined { return this.tryGetToken(esql_parser.SLASH, 0); } - public PERCENT(): TerminalNode | undefined { return this.tryGetToken(esql_parser.PERCENT, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_operatorExpression; } + public get ruleIndex(): number { return esql_parser.RULE_evalCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterOperatorExpression) { - listener.enterOperatorExpression(this); + if (listener.enterEvalCommand) { + listener.enterEvalCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitOperatorExpression) { - listener.exitOperatorExpression(this); + if (listener.exitEvalCommand) { + listener.exitEvalCommand(this); } } } -export class PrimaryExpressionContext extends ParserRuleContext { - public constant(): ConstantContext | undefined { - return this.tryGetRuleContext(0, ConstantContext); - } - public qualifiedName(): QualifiedNameContext | undefined { - return this.tryGetRuleContext(0, QualifiedNameContext); - } - public LP(): TerminalNode | undefined { return this.tryGetToken(esql_parser.LP, 0); } - public booleanExpression(): BooleanExpressionContext[]; - public booleanExpression(i: number): BooleanExpressionContext; - public booleanExpression(i?: number): BooleanExpressionContext | BooleanExpressionContext[] { - if (i === undefined) { - return this.getRuleContexts(BooleanExpressionContext); - } else { - return this.getRuleContext(i, BooleanExpressionContext); - } - } - public RP(): TerminalNode | undefined { return this.tryGetToken(esql_parser.RP, 0); } - public identifier(): IdentifierContext | undefined { - return this.tryGetRuleContext(0, IdentifierContext); +export class StatsCommandContext extends ParserRuleContext { + public STATS(): TerminalNode { return this.getToken(esql_parser.STATS, 0); } + public fields(): FieldsContext | undefined { + return this.tryGetRuleContext(0, FieldsContext); } - public COMMA(): TerminalNode[]; - public COMMA(i: number): TerminalNode; - public COMMA(i?: number): TerminalNode | TerminalNode[] { - if (i === undefined) { - return this.getTokens(esql_parser.COMMA); - } else { - return this.getToken(esql_parser.COMMA, i); - } + public BY(): TerminalNode | undefined { return this.tryGetToken(esql_parser.BY, 0); } + public qualifiedNames(): QualifiedNamesContext | undefined { + return this.tryGetRuleContext(0, QualifiedNamesContext); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_primaryExpression; } + public get ruleIndex(): number { return esql_parser.RULE_statsCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterPrimaryExpression) { - listener.enterPrimaryExpression(this); + if (listener.enterStatsCommand) { + listener.enterStatsCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitPrimaryExpression) { - listener.exitPrimaryExpression(this); + if (listener.exitStatsCommand) { + listener.exitStatsCommand(this); } } } -export class RowCommandContext extends ParserRuleContext { - public ROW(): TerminalNode { return this.getToken(esql_parser.ROW, 0); } - public fields(): FieldsContext { - return this.getRuleContext(0, FieldsContext); - } +export class SourceIdentifierContext extends ParserRuleContext { + public SRC_UNQUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.SRC_UNQUOTED_IDENTIFIER, 0); } + public SRC_QUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.SRC_QUOTED_IDENTIFIER, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_rowCommand; } + public get ruleIndex(): number { return esql_parser.RULE_sourceIdentifier; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterRowCommand) { - listener.enterRowCommand(this); + if (listener.enterSourceIdentifier) { + listener.enterSourceIdentifier(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitRowCommand) { - listener.exitRowCommand(this); + if (listener.exitSourceIdentifier) { + listener.exitSourceIdentifier(this); } } } -export class FieldsContext extends ParserRuleContext { - public field(): FieldContext[]; - public field(i: number): FieldContext; - public field(i?: number): FieldContext | FieldContext[] { - if (i === undefined) { - return this.getRuleContexts(FieldContext); - } else { - return this.getRuleContext(i, FieldContext); +export class EnrichIdentifierContext extends ParserRuleContext { + public ENR_UNQUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ENR_UNQUOTED_IDENTIFIER, 0); } + public ENR_QUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ENR_QUOTED_IDENTIFIER, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_enrichIdentifier; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterEnrichIdentifier) { + listener.enterEnrichIdentifier(this); } } - public COMMA(): TerminalNode[]; - public COMMA(i: number): TerminalNode; - public COMMA(i?: number): TerminalNode | TerminalNode[] { - if (i === undefined) { - return this.getTokens(esql_parser.COMMA); - } else { - return this.getToken(esql_parser.COMMA, i); + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitEnrichIdentifier) { + listener.exitEnrichIdentifier(this); } } +} + + +export class FunctionExpressionArgumentContext extends ParserRuleContext { + public qualifiedName(): QualifiedNameContext | undefined { + return this.tryGetRuleContext(0, QualifiedNameContext); + } + public string(): StringContext | undefined { + return this.tryGetRuleContext(0, StringContext); + } + public number(): NumberContext | undefined { + return this.tryGetRuleContext(0, NumberContext); + } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_fields; } + public get ruleIndex(): number { return esql_parser.RULE_functionExpressionArgument; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterFields) { - listener.enterFields(this); + if (listener.enterFunctionExpressionArgument) { + listener.enterFunctionExpressionArgument(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitFields) { - listener.exitFields(this); + if (listener.exitFunctionExpressionArgument) { + listener.exitFunctionExpressionArgument(this); } } } -export class FieldContext extends ParserRuleContext { - public booleanExpression(): BooleanExpressionContext { - return this.getRuleContext(0, BooleanExpressionContext); +export class MathFunctionExpressionArgumentContext extends ParserRuleContext { + public qualifiedName(): QualifiedNameContext | undefined { + return this.tryGetRuleContext(0, QualifiedNameContext); } - public userVariable(): UserVariableContext | undefined { - return this.tryGetRuleContext(0, UserVariableContext); + public string(): StringContext | undefined { + return this.tryGetRuleContext(0, StringContext); + } + public number(): NumberContext | undefined { + return this.tryGetRuleContext(0, NumberContext); + } + public operatorExpression(): OperatorExpressionContext | undefined { + return this.tryGetRuleContext(0, OperatorExpressionContext); + } + public DATE_LITERAL(): TerminalNode | undefined { return this.tryGetToken(esql_parser.DATE_LITERAL, 0); } + public comparison(): ComparisonContext | undefined { + return this.tryGetRuleContext(0, ComparisonContext); } - public ASSIGN(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ASSIGN, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_field; } + public get ruleIndex(): number { return esql_parser.RULE_mathFunctionExpressionArgument; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterField) { - listener.enterField(this); + if (listener.enterMathFunctionExpressionArgument) { + listener.enterMathFunctionExpressionArgument(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitField) { - listener.exitField(this); + if (listener.exitMathFunctionExpressionArgument) { + listener.exitMathFunctionExpressionArgument(this); } } } -export class UserVariableContext extends ParserRuleContext { - public identifier(): IdentifierContext { - return this.getRuleContext(0, IdentifierContext); +export class QualifiedNameContext extends ParserRuleContext { + public identifier(): IdentifierContext[]; + public identifier(i: number): IdentifierContext; + public identifier(i?: number): IdentifierContext | IdentifierContext[] { + if (i === undefined) { + return this.getRuleContexts(IdentifierContext); + } else { + return this.getRuleContext(i, IdentifierContext); + } + } + public DOT(): TerminalNode[]; + public DOT(i: number): TerminalNode; + public DOT(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.DOT); + } else { + return this.getToken(esql_parser.DOT, i); + } } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_userVariable; } + public get ruleIndex(): number { return esql_parser.RULE_qualifiedName; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterUserVariable) { - listener.enterUserVariable(this); + if (listener.enterQualifiedName) { + listener.enterQualifiedName(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitUserVariable) { - listener.exitUserVariable(this); + if (listener.exitQualifiedName) { + listener.exitQualifiedName(this); } } } -export class FromCommandContext extends ParserRuleContext { - public FROM(): TerminalNode { return this.getToken(esql_parser.FROM, 0); } - public sourceIdentifier(): SourceIdentifierContext[]; - public sourceIdentifier(i: number): SourceIdentifierContext; - public sourceIdentifier(i?: number): SourceIdentifierContext | SourceIdentifierContext[] { +export class QualifiedNamesContext extends ParserRuleContext { + public qualifiedName(): QualifiedNameContext[]; + public qualifiedName(i: number): QualifiedNameContext; + public qualifiedName(i?: number): QualifiedNameContext | QualifiedNameContext[] { if (i === undefined) { - return this.getRuleContexts(SourceIdentifierContext); + return this.getRuleContexts(QualifiedNameContext); } else { - return this.getRuleContext(i, SourceIdentifierContext); + return this.getRuleContext(i, QualifiedNameContext); } } public COMMA(): TerminalNode[]; @@ -2556,173 +4658,209 @@ export class FromCommandContext extends ParserRuleContext { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_fromCommand; } + public get ruleIndex(): number { return esql_parser.RULE_qualifiedNames; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterFromCommand) { - listener.enterFromCommand(this); + if (listener.enterQualifiedNames) { + listener.enterQualifiedNames(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitFromCommand) { - listener.exitFromCommand(this); + if (listener.exitQualifiedNames) { + listener.exitQualifiedNames(this); } } } -export class EvalCommandContext extends ParserRuleContext { - public EVAL(): TerminalNode { return this.getToken(esql_parser.EVAL, 0); } - public fields(): FieldsContext { - return this.getRuleContext(0, FieldsContext); - } +export class IdentifierContext extends ParserRuleContext { + public UNQUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.UNQUOTED_IDENTIFIER, 0); } + public QUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.QUOTED_IDENTIFIER, 0); } + public ASTERISK(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ASTERISK, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_evalCommand; } + public get ruleIndex(): number { return esql_parser.RULE_identifier; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterEvalCommand) { - listener.enterEvalCommand(this); + if (listener.enterIdentifier) { + listener.enterIdentifier(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitEvalCommand) { - listener.exitEvalCommand(this); + if (listener.exitIdentifier) { + listener.exitIdentifier(this); } } } -export class StatsCommandContext extends ParserRuleContext { - public STATS(): TerminalNode { return this.getToken(esql_parser.STATS, 0); } - public fields(): FieldsContext { - return this.getRuleContext(0, FieldsContext); - } - public BY(): TerminalNode | undefined { return this.tryGetToken(esql_parser.BY, 0); } - public qualifiedNames(): QualifiedNamesContext | undefined { - return this.tryGetRuleContext(0, QualifiedNamesContext); - } +export class MathFunctionIdentifierContext extends ParserRuleContext { + public MATH_FUNCTION(): TerminalNode { return this.getToken(esql_parser.MATH_FUNCTION, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_statsCommand; } + public get ruleIndex(): number { return esql_parser.RULE_mathFunctionIdentifier; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterStatsCommand) { - listener.enterStatsCommand(this); + if (listener.enterMathFunctionIdentifier) { + listener.enterMathFunctionIdentifier(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitStatsCommand) { - listener.exitStatsCommand(this); + if (listener.exitMathFunctionIdentifier) { + listener.exitMathFunctionIdentifier(this); } } } -export class SourceIdentifierContext extends ParserRuleContext { - public SRC_UNQUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.SRC_UNQUOTED_IDENTIFIER, 0); } - public SRC_QUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.SRC_QUOTED_IDENTIFIER, 0); } +export class FunctionIdentifierContext extends ParserRuleContext { + public UNARY_FUNCTION(): TerminalNode { return this.getToken(esql_parser.UNARY_FUNCTION, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_sourceIdentifier; } + public get ruleIndex(): number { return esql_parser.RULE_functionIdentifier; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterSourceIdentifier) { - listener.enterSourceIdentifier(this); + if (listener.enterFunctionIdentifier) { + listener.enterFunctionIdentifier(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitSourceIdentifier) { - listener.exitSourceIdentifier(this); + if (listener.exitFunctionIdentifier) { + listener.exitFunctionIdentifier(this); } } } -export class FunctionExpressionArgumentContext extends ParserRuleContext { - public qualifiedName(): QualifiedNameContext | undefined { - return this.tryGetRuleContext(0, QualifiedNameContext); +export class ConstantContext extends ParserRuleContext { + public NULL(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NULL, 0); } + public numericValue(): NumericValueContext[]; + public numericValue(i: number): NumericValueContext; + public numericValue(i?: number): NumericValueContext | NumericValueContext[] { + if (i === undefined) { + return this.getRuleContexts(NumericValueContext); + } else { + return this.getRuleContext(i, NumericValueContext); + } } - public string(): StringContext | undefined { - return this.tryGetRuleContext(0, StringContext); + public booleanValue(): BooleanValueContext[]; + public booleanValue(i: number): BooleanValueContext; + public booleanValue(i?: number): BooleanValueContext | BooleanValueContext[] { + if (i === undefined) { + return this.getRuleContexts(BooleanValueContext); + } else { + return this.getRuleContext(i, BooleanValueContext); + } + } + public string(): StringContext[]; + public string(i: number): StringContext; + public string(i?: number): StringContext | StringContext[] { + if (i === undefined) { + return this.getRuleContexts(StringContext); + } else { + return this.getRuleContext(i, StringContext); + } + } + public OPENING_BRACKET(): TerminalNode | undefined { return this.tryGetToken(esql_parser.OPENING_BRACKET, 0); } + public CLOSING_BRACKET(): TerminalNode | undefined { return this.tryGetToken(esql_parser.CLOSING_BRACKET, 0); } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.COMMA); + } else { + return this.getToken(esql_parser.COMMA, i); + } } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_functionExpressionArgument; } + public get ruleIndex(): number { return esql_parser.RULE_constant; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterFunctionExpressionArgument) { - listener.enterFunctionExpressionArgument(this); + if (listener.enterConstant) { + listener.enterConstant(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitFunctionExpressionArgument) { - listener.exitFunctionExpressionArgument(this); + if (listener.exitConstant) { + listener.exitConstant(this); } } } -export class QualifiedNameContext extends ParserRuleContext { - public identifier(): IdentifierContext[]; - public identifier(i: number): IdentifierContext; - public identifier(i?: number): IdentifierContext | IdentifierContext[] { - if (i === undefined) { - return this.getRuleContexts(IdentifierContext); - } else { - return this.getRuleContext(i, IdentifierContext); +export class NumericValueContext extends ParserRuleContext { + public decimalValue(): DecimalValueContext | undefined { + return this.tryGetRuleContext(0, DecimalValueContext); + } + public integerValue(): IntegerValueContext | undefined { + return this.tryGetRuleContext(0, IntegerValueContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_numericValue; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterNumericValue) { + listener.enterNumericValue(this); } } - public DOT(): TerminalNode[]; - public DOT(i: number): TerminalNode; - public DOT(i?: number): TerminalNode | TerminalNode[] { - if (i === undefined) { - return this.getTokens(esql_parser.DOT); - } else { - return this.getToken(esql_parser.DOT, i); + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitNumericValue) { + listener.exitNumericValue(this); } } +} + + +export class LimitCommandContext extends ParserRuleContext { + public LIMIT(): TerminalNode { return this.getToken(esql_parser.LIMIT, 0); } + public INTEGER_LITERAL(): TerminalNode { return this.getToken(esql_parser.INTEGER_LITERAL, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_qualifiedName; } + public get ruleIndex(): number { return esql_parser.RULE_limitCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterQualifiedName) { - listener.enterQualifiedName(this); + if (listener.enterLimitCommand) { + listener.enterLimitCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitQualifiedName) { - listener.exitQualifiedName(this); + if (listener.exitLimitCommand) { + listener.exitLimitCommand(this); } } } -export class QualifiedNamesContext extends ParserRuleContext { - public qualifiedName(): QualifiedNameContext[]; - public qualifiedName(i: number): QualifiedNameContext; - public qualifiedName(i?: number): QualifiedNameContext | QualifiedNameContext[] { +export class SortCommandContext extends ParserRuleContext { + public SORT(): TerminalNode { return this.getToken(esql_parser.SORT, 0); } + public orderExpression(): OrderExpressionContext[]; + public orderExpression(i: number): OrderExpressionContext; + public orderExpression(i?: number): OrderExpressionContext | OrderExpressionContext[] { if (i === undefined) { - return this.getRuleContexts(QualifiedNameContext); + return this.getRuleContexts(OrderExpressionContext); } else { - return this.getRuleContext(i, QualifiedNameContext); + return this.getRuleContext(i, OrderExpressionContext); } } public COMMA(): TerminalNode[]; @@ -2738,260 +4876,298 @@ export class QualifiedNamesContext extends ParserRuleContext { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_qualifiedNames; } + public get ruleIndex(): number { return esql_parser.RULE_sortCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterQualifiedNames) { - listener.enterQualifiedNames(this); + if (listener.enterSortCommand) { + listener.enterSortCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitQualifiedNames) { - listener.exitQualifiedNames(this); + if (listener.exitSortCommand) { + listener.exitSortCommand(this); } } } -export class IdentifierContext extends ParserRuleContext { - public UNQUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.UNQUOTED_IDENTIFIER, 0); } - public QUOTED_IDENTIFIER(): TerminalNode | undefined { return this.tryGetToken(esql_parser.QUOTED_IDENTIFIER, 0); } +export class OrderExpressionContext extends ParserRuleContext { + public booleanExpression(): BooleanExpressionContext { + return this.getRuleContext(0, BooleanExpressionContext); + } + public ORDERING(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ORDERING, 0); } + public NULLS_ORDERING(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NULLS_ORDERING, 0); } + public NULLS_ORDERING_DIRECTION(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NULLS_ORDERING_DIRECTION, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_identifier; } + public get ruleIndex(): number { return esql_parser.RULE_orderExpression; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterIdentifier) { - listener.enterIdentifier(this); + if (listener.enterOrderExpression) { + listener.enterOrderExpression(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitIdentifier) { - listener.exitIdentifier(this); + if (listener.exitOrderExpression) { + listener.exitOrderExpression(this); } } } -export class FunctionIdentifierContext extends ParserRuleContext { - public UNARY_FUNCTION(): TerminalNode { return this.getToken(esql_parser.UNARY_FUNCTION, 0); } +export class ProjectCommandContext extends ParserRuleContext { + public PROJECT(): TerminalNode { return this.getToken(esql_parser.PROJECT, 0); } + public qualifiedNames(): QualifiedNamesContext { + return this.getRuleContext(0, QualifiedNamesContext); + } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_functionIdentifier; } + public get ruleIndex(): number { return esql_parser.RULE_projectCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterFunctionIdentifier) { - listener.enterFunctionIdentifier(this); + if (listener.enterProjectCommand) { + listener.enterProjectCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitFunctionIdentifier) { - listener.exitFunctionIdentifier(this); + if (listener.exitProjectCommand) { + listener.exitProjectCommand(this); } } } -export class ConstantContext extends ParserRuleContext { +export class KeepCommandContext extends ParserRuleContext { + public KEEP(): TerminalNode { return this.getToken(esql_parser.KEEP, 0); } + public qualifiedNames(): QualifiedNamesContext { + return this.getRuleContext(0, QualifiedNamesContext); + } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_constant; } - public copyFrom(ctx: ConstantContext): void { - super.copyFrom(ctx); - } -} -export class NullLiteralContext extends ConstantContext { - public NULL(): TerminalNode { return this.getToken(esql_parser.NULL, 0); } - constructor(ctx: ConstantContext) { - super(ctx.parent, ctx.invokingState); - this.copyFrom(ctx); - } + public get ruleIndex(): number { return esql_parser.RULE_keepCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterNullLiteral) { - listener.enterNullLiteral(this); + if (listener.enterKeepCommand) { + listener.enterKeepCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitNullLiteral) { - listener.exitNullLiteral(this); + if (listener.exitKeepCommand) { + listener.exitKeepCommand(this); } } } -export class NumericLiteralContext extends ConstantContext { - public number(): NumberContext { - return this.getRuleContext(0, NumberContext); + + +export class DropCommandContext extends ParserRuleContext { + public DROP(): TerminalNode { return this.getToken(esql_parser.DROP, 0); } + public qualifiedNames(): QualifiedNamesContext { + return this.getRuleContext(0, QualifiedNamesContext); } - constructor(ctx: ConstantContext) { - super(ctx.parent, ctx.invokingState); - this.copyFrom(ctx); + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); } // @Override + public get ruleIndex(): number { return esql_parser.RULE_dropCommand; } + // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterNumericLiteral) { - listener.enterNumericLiteral(this); + if (listener.enterDropCommand) { + listener.enterDropCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitNumericLiteral) { - listener.exitNumericLiteral(this); + if (listener.exitDropCommand) { + listener.exitDropCommand(this); } } } -export class BooleanLiteralContext extends ConstantContext { - public booleanValue(): BooleanValueContext { - return this.getRuleContext(0, BooleanValueContext); + + +export class RenameVariableContext extends ParserRuleContext { + public identifier(): IdentifierContext[]; + public identifier(i: number): IdentifierContext; + public identifier(i?: number): IdentifierContext | IdentifierContext[] { + if (i === undefined) { + return this.getRuleContexts(IdentifierContext); + } else { + return this.getRuleContext(i, IdentifierContext); + } } - constructor(ctx: ConstantContext) { - super(ctx.parent, ctx.invokingState); - this.copyFrom(ctx); + public DOT(): TerminalNode[]; + public DOT(i: number): TerminalNode; + public DOT(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.DOT); + } else { + return this.getToken(esql_parser.DOT, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); } // @Override + public get ruleIndex(): number { return esql_parser.RULE_renameVariable; } + // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterBooleanLiteral) { - listener.enterBooleanLiteral(this); + if (listener.enterRenameVariable) { + listener.enterRenameVariable(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitBooleanLiteral) { - listener.exitBooleanLiteral(this); + if (listener.exitRenameVariable) { + listener.exitRenameVariable(this); } } } -export class StringLiteralContext extends ConstantContext { - public string(): StringContext { - return this.getRuleContext(0, StringContext); + + +export class RenameCommandContext extends ParserRuleContext { + public RENAME(): TerminalNode { return this.getToken(esql_parser.RENAME, 0); } + public renameClause(): RenameClauseContext[]; + public renameClause(i: number): RenameClauseContext; + public renameClause(i?: number): RenameClauseContext | RenameClauseContext[] { + if (i === undefined) { + return this.getRuleContexts(RenameClauseContext); + } else { + return this.getRuleContext(i, RenameClauseContext); + } } - constructor(ctx: ConstantContext) { - super(ctx.parent, ctx.invokingState); - this.copyFrom(ctx); + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(esql_parser.COMMA); + } else { + return this.getToken(esql_parser.COMMA, i); + } } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_renameCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterStringLiteral) { - listener.enterStringLiteral(this); + if (listener.enterRenameCommand) { + listener.enterRenameCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitStringLiteral) { - listener.exitStringLiteral(this); + if (listener.exitRenameCommand) { + listener.exitRenameCommand(this); } } } -export class LimitCommandContext extends ParserRuleContext { - public LIMIT(): TerminalNode { return this.getToken(esql_parser.LIMIT, 0); } - public INTEGER_LITERAL(): TerminalNode { return this.getToken(esql_parser.INTEGER_LITERAL, 0); } +export class RenameClauseContext extends ParserRuleContext { + public qualifiedName(): QualifiedNameContext { + return this.getRuleContext(0, QualifiedNameContext); + } + public AS(): TerminalNode { return this.getToken(esql_parser.AS, 0); } + public renameVariable(): RenameVariableContext { + return this.getRuleContext(0, RenameVariableContext); + } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_limitCommand; } + public get ruleIndex(): number { return esql_parser.RULE_renameClause; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterLimitCommand) { - listener.enterLimitCommand(this); + if (listener.enterRenameClause) { + listener.enterRenameClause(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitLimitCommand) { - listener.exitLimitCommand(this); + if (listener.exitRenameClause) { + listener.exitRenameClause(this); } } } -export class SortCommandContext extends ParserRuleContext { - public SORT(): TerminalNode { return this.getToken(esql_parser.SORT, 0); } - public orderExpression(): OrderExpressionContext[]; - public orderExpression(i: number): OrderExpressionContext; - public orderExpression(i?: number): OrderExpressionContext | OrderExpressionContext[] { - if (i === undefined) { - return this.getRuleContexts(OrderExpressionContext); - } else { - return this.getRuleContext(i, OrderExpressionContext); - } +export class DissectCommandContext extends ParserRuleContext { + public DISSECT(): TerminalNode { return this.getToken(esql_parser.DISSECT, 0); } + public qualifiedNames(): QualifiedNamesContext { + return this.getRuleContext(0, QualifiedNamesContext); } - public COMMA(): TerminalNode[]; - public COMMA(i: number): TerminalNode; - public COMMA(i?: number): TerminalNode | TerminalNode[] { - if (i === undefined) { - return this.getTokens(esql_parser.COMMA); - } else { - return this.getToken(esql_parser.COMMA, i); - } + public string(): StringContext { + return this.getRuleContext(0, StringContext); + } + public commandOptions(): CommandOptionsContext | undefined { + return this.tryGetRuleContext(0, CommandOptionsContext); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_sortCommand; } + public get ruleIndex(): number { return esql_parser.RULE_dissectCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterSortCommand) { - listener.enterSortCommand(this); + if (listener.enterDissectCommand) { + listener.enterDissectCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitSortCommand) { - listener.exitSortCommand(this); + if (listener.exitDissectCommand) { + listener.exitDissectCommand(this); } } } -export class OrderExpressionContext extends ParserRuleContext { - public booleanExpression(): BooleanExpressionContext { - return this.getRuleContext(0, BooleanExpressionContext); +export class GrokCommandContext extends ParserRuleContext { + public GROK(): TerminalNode { return this.getToken(esql_parser.GROK, 0); } + public qualifiedNames(): QualifiedNamesContext { + return this.getRuleContext(0, QualifiedNamesContext); + } + public string(): StringContext { + return this.getRuleContext(0, StringContext); } - public ORDERING(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ORDERING, 0); } - public NULLS_ORDERING(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NULLS_ORDERING, 0); } - public NULLS_ORDERING_DIRECTION(): TerminalNode | undefined { return this.tryGetToken(esql_parser.NULLS_ORDERING_DIRECTION, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_orderExpression; } + public get ruleIndex(): number { return esql_parser.RULE_grokCommand; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterOrderExpression) { - listener.enterOrderExpression(this); + if (listener.enterGrokCommand) { + listener.enterGrokCommand(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitOrderExpression) { - listener.exitOrderExpression(this); + if (listener.exitGrokCommand) { + listener.exitGrokCommand(this); } } } -export class ProjectCommandContext extends ParserRuleContext { - public PROJECT(): TerminalNode { return this.getToken(esql_parser.PROJECT, 0); } - public projectClause(): ProjectClauseContext[]; - public projectClause(i: number): ProjectClauseContext; - public projectClause(i?: number): ProjectClauseContext | ProjectClauseContext[] { +export class CommandOptionsContext extends ParserRuleContext { + public commandOption(): CommandOptionContext[]; + public commandOption(i: number): CommandOptionContext; + public commandOption(i?: number): CommandOptionContext | CommandOptionContext[] { if (i === undefined) { - return this.getRuleContexts(ProjectClauseContext); + return this.getRuleContexts(CommandOptionContext); } else { - return this.getRuleContext(i, ProjectClauseContext); + return this.getRuleContext(i, CommandOptionContext); } } public COMMA(): TerminalNode[]; @@ -3007,50 +5183,45 @@ export class ProjectCommandContext extends ParserRuleContext { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_projectCommand; } + public get ruleIndex(): number { return esql_parser.RULE_commandOptions; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterProjectCommand) { - listener.enterProjectCommand(this); + if (listener.enterCommandOptions) { + listener.enterCommandOptions(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitProjectCommand) { - listener.exitProjectCommand(this); + if (listener.exitCommandOptions) { + listener.exitCommandOptions(this); } } } -export class ProjectClauseContext extends ParserRuleContext { - public _newName: SourceIdentifierContext; - public _oldName: SourceIdentifierContext; - public sourceIdentifier(): SourceIdentifierContext[]; - public sourceIdentifier(i: number): SourceIdentifierContext; - public sourceIdentifier(i?: number): SourceIdentifierContext | SourceIdentifierContext[] { - if (i === undefined) { - return this.getRuleContexts(SourceIdentifierContext); - } else { - return this.getRuleContext(i, SourceIdentifierContext); - } +export class CommandOptionContext extends ParserRuleContext { + public identifier(): IdentifierContext { + return this.getRuleContext(0, IdentifierContext); + } + public ASSIGN(): TerminalNode { return this.getToken(esql_parser.ASSIGN, 0); } + public constant(): ConstantContext { + return this.getRuleContext(0, ConstantContext); } - public ASSIGN(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ASSIGN, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_projectClause; } + public get ruleIndex(): number { return esql_parser.RULE_commandOption; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterProjectClause) { - listener.enterProjectClause(this); + if (listener.enterCommandOption) { + listener.enterCommandOption(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitProjectClause) { - listener.exitProjectClause(this); + if (listener.exitCommandOption) { + listener.exitCommandOption(this); } } } @@ -3128,6 +5299,50 @@ export class IntegerLiteralContext extends NumberContext { } +export class DecimalValueContext extends ParserRuleContext { + public DECIMAL_LITERAL(): TerminalNode { return this.getToken(esql_parser.DECIMAL_LITERAL, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_decimalValue; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterDecimalValue) { + listener.enterDecimalValue(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitDecimalValue) { + listener.exitDecimalValue(this); + } + } +} + + +export class IntegerValueContext extends ParserRuleContext { + public INTEGER_LITERAL(): TerminalNode { return this.getToken(esql_parser.INTEGER_LITERAL, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_integerValue; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterIntegerValue) { + listener.enterIntegerValue(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitIntegerValue) { + listener.exitIntegerValue(this); + } + } +} + + export class StringContext extends ParserRuleContext { public STRING(): TerminalNode { return this.getToken(esql_parser.STRING, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { @@ -3223,3 +5438,27 @@ export class SubqueryExpressionContext extends ParserRuleContext { } +export class ShowCommandContext extends ParserRuleContext { + public SHOW(): TerminalNode { return this.getToken(esql_parser.SHOW, 0); } + public INFO(): TerminalNode | undefined { return this.tryGetToken(esql_parser.INFO, 0); } + public FUNCTIONS(): TerminalNode | undefined { return this.tryGetToken(esql_parser.FUNCTIONS, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_showCommand; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterShowCommand) { + listener.enterShowCommand(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitShowCommand) { + listener.exitShowCommand(this); + } + } +} + + diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser_listener.ts b/packages/kbn-monaco/src/esql/antlr/esql_parser_listener.ts index 2b943a8bcff45..99a91b0bd84a3 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser_listener.ts +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser_listener.ts @@ -4,10 +4,6 @@ import { ParseTreeListener } from "antlr4ts/tree/ParseTreeListener"; -import { NullLiteralContext } from "./esql_parser"; -import { NumericLiteralContext } from "./esql_parser"; -import { BooleanLiteralContext } from "./esql_parser"; -import { StringLiteralContext } from "./esql_parser"; import { DecimalLiteralContext } from "./esql_parser"; import { IntegerLiteralContext } from "./esql_parser"; import { SingleCommandQueryContext } from "./esql_parser"; @@ -16,38 +12,61 @@ import { SingleStatementContext } from "./esql_parser"; import { QueryContext } from "./esql_parser"; import { SourceCommandContext } from "./esql_parser"; import { ProcessingCommandContext } from "./esql_parser"; +import { EnrichCommandContext } from "./esql_parser"; +import { EnrichWithClauseContext } from "./esql_parser"; +import { MvExpandCommandContext } from "./esql_parser"; import { WhereCommandContext } from "./esql_parser"; +import { WhereBooleanExpressionContext } from "./esql_parser"; import { BooleanExpressionContext } from "./esql_parser"; +import { RegexBooleanExpressionContext } from "./esql_parser"; import { ValueExpressionContext } from "./esql_parser"; import { ComparisonContext } from "./esql_parser"; import { MathFnContext } from "./esql_parser"; +import { MathEvalFnContext } from "./esql_parser"; import { OperatorExpressionContext } from "./esql_parser"; import { PrimaryExpressionContext } from "./esql_parser"; import { RowCommandContext } from "./esql_parser"; import { FieldsContext } from "./esql_parser"; import { FieldContext } from "./esql_parser"; +import { EnrichFieldIdentifierContext } from "./esql_parser"; import { UserVariableContext } from "./esql_parser"; import { FromCommandContext } from "./esql_parser"; +import { MetadataContext } from "./esql_parser"; import { EvalCommandContext } from "./esql_parser"; import { StatsCommandContext } from "./esql_parser"; import { SourceIdentifierContext } from "./esql_parser"; +import { EnrichIdentifierContext } from "./esql_parser"; import { FunctionExpressionArgumentContext } from "./esql_parser"; +import { MathFunctionExpressionArgumentContext } from "./esql_parser"; import { QualifiedNameContext } from "./esql_parser"; import { QualifiedNamesContext } from "./esql_parser"; import { IdentifierContext } from "./esql_parser"; +import { MathFunctionIdentifierContext } from "./esql_parser"; import { FunctionIdentifierContext } from "./esql_parser"; import { ConstantContext } from "./esql_parser"; +import { NumericValueContext } from "./esql_parser"; import { LimitCommandContext } from "./esql_parser"; import { SortCommandContext } from "./esql_parser"; import { OrderExpressionContext } from "./esql_parser"; import { ProjectCommandContext } from "./esql_parser"; -import { ProjectClauseContext } from "./esql_parser"; +import { KeepCommandContext } from "./esql_parser"; +import { DropCommandContext } from "./esql_parser"; +import { RenameVariableContext } from "./esql_parser"; +import { RenameCommandContext } from "./esql_parser"; +import { RenameClauseContext } from "./esql_parser"; +import { DissectCommandContext } from "./esql_parser"; +import { GrokCommandContext } from "./esql_parser"; +import { CommandOptionsContext } from "./esql_parser"; +import { CommandOptionContext } from "./esql_parser"; import { BooleanValueContext } from "./esql_parser"; import { NumberContext } from "./esql_parser"; +import { DecimalValueContext } from "./esql_parser"; +import { IntegerValueContext } from "./esql_parser"; import { StringContext } from "./esql_parser"; import { ComparisonOperatorContext } from "./esql_parser"; import { ExplainCommandContext } from "./esql_parser"; import { SubqueryExpressionContext } from "./esql_parser"; +import { ShowCommandContext } from "./esql_parser"; /** @@ -55,58 +74,6 @@ import { SubqueryExpressionContext } from "./esql_parser"; * `esql_parser`. */ export interface esql_parserListener extends ParseTreeListener { - /** - * Enter a parse tree produced by the `nullLiteral` - * labeled alternative in `esql_parser.constant`. - * @param ctx the parse tree - */ - enterNullLiteral?: (ctx: NullLiteralContext) => void; - /** - * Exit a parse tree produced by the `nullLiteral` - * labeled alternative in `esql_parser.constant`. - * @param ctx the parse tree - */ - exitNullLiteral?: (ctx: NullLiteralContext) => void; - - /** - * Enter a parse tree produced by the `numericLiteral` - * labeled alternative in `esql_parser.constant`. - * @param ctx the parse tree - */ - enterNumericLiteral?: (ctx: NumericLiteralContext) => void; - /** - * Exit a parse tree produced by the `numericLiteral` - * labeled alternative in `esql_parser.constant`. - * @param ctx the parse tree - */ - exitNumericLiteral?: (ctx: NumericLiteralContext) => void; - - /** - * Enter a parse tree produced by the `booleanLiteral` - * labeled alternative in `esql_parser.constant`. - * @param ctx the parse tree - */ - enterBooleanLiteral?: (ctx: BooleanLiteralContext) => void; - /** - * Exit a parse tree produced by the `booleanLiteral` - * labeled alternative in `esql_parser.constant`. - * @param ctx the parse tree - */ - exitBooleanLiteral?: (ctx: BooleanLiteralContext) => void; - - /** - * Enter a parse tree produced by the `stringLiteral` - * labeled alternative in `esql_parser.constant`. - * @param ctx the parse tree - */ - enterStringLiteral?: (ctx: StringLiteralContext) => void; - /** - * Exit a parse tree produced by the `stringLiteral` - * labeled alternative in `esql_parser.constant`. - * @param ctx the parse tree - */ - exitStringLiteral?: (ctx: StringLiteralContext) => void; - /** * Enter a parse tree produced by the `decimalLiteral` * labeled alternative in `esql_parser.number`. @@ -203,6 +170,39 @@ export interface esql_parserListener extends ParseTreeListener { */ exitProcessingCommand?: (ctx: ProcessingCommandContext) => void; + /** + * Enter a parse tree produced by `esql_parser.enrichCommand`. + * @param ctx the parse tree + */ + enterEnrichCommand?: (ctx: EnrichCommandContext) => void; + /** + * Exit a parse tree produced by `esql_parser.enrichCommand`. + * @param ctx the parse tree + */ + exitEnrichCommand?: (ctx: EnrichCommandContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.enrichWithClause`. + * @param ctx the parse tree + */ + enterEnrichWithClause?: (ctx: EnrichWithClauseContext) => void; + /** + * Exit a parse tree produced by `esql_parser.enrichWithClause`. + * @param ctx the parse tree + */ + exitEnrichWithClause?: (ctx: EnrichWithClauseContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.mvExpandCommand`. + * @param ctx the parse tree + */ + enterMvExpandCommand?: (ctx: MvExpandCommandContext) => void; + /** + * Exit a parse tree produced by `esql_parser.mvExpandCommand`. + * @param ctx the parse tree + */ + exitMvExpandCommand?: (ctx: MvExpandCommandContext) => void; + /** * Enter a parse tree produced by `esql_parser.whereCommand`. * @param ctx the parse tree @@ -214,6 +214,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitWhereCommand?: (ctx: WhereCommandContext) => void; + /** + * Enter a parse tree produced by `esql_parser.whereBooleanExpression`. + * @param ctx the parse tree + */ + enterWhereBooleanExpression?: (ctx: WhereBooleanExpressionContext) => void; + /** + * Exit a parse tree produced by `esql_parser.whereBooleanExpression`. + * @param ctx the parse tree + */ + exitWhereBooleanExpression?: (ctx: WhereBooleanExpressionContext) => void; + /** * Enter a parse tree produced by `esql_parser.booleanExpression`. * @param ctx the parse tree @@ -225,6 +236,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitBooleanExpression?: (ctx: BooleanExpressionContext) => void; + /** + * Enter a parse tree produced by `esql_parser.regexBooleanExpression`. + * @param ctx the parse tree + */ + enterRegexBooleanExpression?: (ctx: RegexBooleanExpressionContext) => void; + /** + * Exit a parse tree produced by `esql_parser.regexBooleanExpression`. + * @param ctx the parse tree + */ + exitRegexBooleanExpression?: (ctx: RegexBooleanExpressionContext) => void; + /** * Enter a parse tree produced by `esql_parser.valueExpression`. * @param ctx the parse tree @@ -258,6 +280,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitMathFn?: (ctx: MathFnContext) => void; + /** + * Enter a parse tree produced by `esql_parser.mathEvalFn`. + * @param ctx the parse tree + */ + enterMathEvalFn?: (ctx: MathEvalFnContext) => void; + /** + * Exit a parse tree produced by `esql_parser.mathEvalFn`. + * @param ctx the parse tree + */ + exitMathEvalFn?: (ctx: MathEvalFnContext) => void; + /** * Enter a parse tree produced by `esql_parser.operatorExpression`. * @param ctx the parse tree @@ -313,6 +346,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitField?: (ctx: FieldContext) => void; + /** + * Enter a parse tree produced by `esql_parser.enrichFieldIdentifier`. + * @param ctx the parse tree + */ + enterEnrichFieldIdentifier?: (ctx: EnrichFieldIdentifierContext) => void; + /** + * Exit a parse tree produced by `esql_parser.enrichFieldIdentifier`. + * @param ctx the parse tree + */ + exitEnrichFieldIdentifier?: (ctx: EnrichFieldIdentifierContext) => void; + /** * Enter a parse tree produced by `esql_parser.userVariable`. * @param ctx the parse tree @@ -335,6 +379,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitFromCommand?: (ctx: FromCommandContext) => void; + /** + * Enter a parse tree produced by `esql_parser.metadata`. + * @param ctx the parse tree + */ + enterMetadata?: (ctx: MetadataContext) => void; + /** + * Exit a parse tree produced by `esql_parser.metadata`. + * @param ctx the parse tree + */ + exitMetadata?: (ctx: MetadataContext) => void; + /** * Enter a parse tree produced by `esql_parser.evalCommand`. * @param ctx the parse tree @@ -368,6 +423,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitSourceIdentifier?: (ctx: SourceIdentifierContext) => void; + /** + * Enter a parse tree produced by `esql_parser.enrichIdentifier`. + * @param ctx the parse tree + */ + enterEnrichIdentifier?: (ctx: EnrichIdentifierContext) => void; + /** + * Exit a parse tree produced by `esql_parser.enrichIdentifier`. + * @param ctx the parse tree + */ + exitEnrichIdentifier?: (ctx: EnrichIdentifierContext) => void; + /** * Enter a parse tree produced by `esql_parser.functionExpressionArgument`. * @param ctx the parse tree @@ -379,6 +445,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitFunctionExpressionArgument?: (ctx: FunctionExpressionArgumentContext) => void; + /** + * Enter a parse tree produced by `esql_parser.mathFunctionExpressionArgument`. + * @param ctx the parse tree + */ + enterMathFunctionExpressionArgument?: (ctx: MathFunctionExpressionArgumentContext) => void; + /** + * Exit a parse tree produced by `esql_parser.mathFunctionExpressionArgument`. + * @param ctx the parse tree + */ + exitMathFunctionExpressionArgument?: (ctx: MathFunctionExpressionArgumentContext) => void; + /** * Enter a parse tree produced by `esql_parser.qualifiedName`. * @param ctx the parse tree @@ -412,6 +489,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitIdentifier?: (ctx: IdentifierContext) => void; + /** + * Enter a parse tree produced by `esql_parser.mathFunctionIdentifier`. + * @param ctx the parse tree + */ + enterMathFunctionIdentifier?: (ctx: MathFunctionIdentifierContext) => void; + /** + * Exit a parse tree produced by `esql_parser.mathFunctionIdentifier`. + * @param ctx the parse tree + */ + exitMathFunctionIdentifier?: (ctx: MathFunctionIdentifierContext) => void; + /** * Enter a parse tree produced by `esql_parser.functionIdentifier`. * @param ctx the parse tree @@ -434,6 +522,17 @@ export interface esql_parserListener extends ParseTreeListener { */ exitConstant?: (ctx: ConstantContext) => void; + /** + * Enter a parse tree produced by `esql_parser.numericValue`. + * @param ctx the parse tree + */ + enterNumericValue?: (ctx: NumericValueContext) => void; + /** + * Exit a parse tree produced by `esql_parser.numericValue`. + * @param ctx the parse tree + */ + exitNumericValue?: (ctx: NumericValueContext) => void; + /** * Enter a parse tree produced by `esql_parser.limitCommand`. * @param ctx the parse tree @@ -479,15 +578,103 @@ export interface esql_parserListener extends ParseTreeListener { exitProjectCommand?: (ctx: ProjectCommandContext) => void; /** - * Enter a parse tree produced by `esql_parser.projectClause`. + * Enter a parse tree produced by `esql_parser.keepCommand`. + * @param ctx the parse tree + */ + enterKeepCommand?: (ctx: KeepCommandContext) => void; + /** + * Exit a parse tree produced by `esql_parser.keepCommand`. + * @param ctx the parse tree + */ + exitKeepCommand?: (ctx: KeepCommandContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.dropCommand`. + * @param ctx the parse tree + */ + enterDropCommand?: (ctx: DropCommandContext) => void; + /** + * Exit a parse tree produced by `esql_parser.dropCommand`. + * @param ctx the parse tree + */ + exitDropCommand?: (ctx: DropCommandContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.renameVariable`. + * @param ctx the parse tree + */ + enterRenameVariable?: (ctx: RenameVariableContext) => void; + /** + * Exit a parse tree produced by `esql_parser.renameVariable`. + * @param ctx the parse tree + */ + exitRenameVariable?: (ctx: RenameVariableContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.renameCommand`. + * @param ctx the parse tree + */ + enterRenameCommand?: (ctx: RenameCommandContext) => void; + /** + * Exit a parse tree produced by `esql_parser.renameCommand`. + * @param ctx the parse tree + */ + exitRenameCommand?: (ctx: RenameCommandContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.renameClause`. + * @param ctx the parse tree + */ + enterRenameClause?: (ctx: RenameClauseContext) => void; + /** + * Exit a parse tree produced by `esql_parser.renameClause`. + * @param ctx the parse tree + */ + exitRenameClause?: (ctx: RenameClauseContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.dissectCommand`. + * @param ctx the parse tree + */ + enterDissectCommand?: (ctx: DissectCommandContext) => void; + /** + * Exit a parse tree produced by `esql_parser.dissectCommand`. + * @param ctx the parse tree + */ + exitDissectCommand?: (ctx: DissectCommandContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.grokCommand`. * @param ctx the parse tree */ - enterProjectClause?: (ctx: ProjectClauseContext) => void; + enterGrokCommand?: (ctx: GrokCommandContext) => void; /** - * Exit a parse tree produced by `esql_parser.projectClause`. + * Exit a parse tree produced by `esql_parser.grokCommand`. * @param ctx the parse tree */ - exitProjectClause?: (ctx: ProjectClauseContext) => void; + exitGrokCommand?: (ctx: GrokCommandContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.commandOptions`. + * @param ctx the parse tree + */ + enterCommandOptions?: (ctx: CommandOptionsContext) => void; + /** + * Exit a parse tree produced by `esql_parser.commandOptions`. + * @param ctx the parse tree + */ + exitCommandOptions?: (ctx: CommandOptionsContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.commandOption`. + * @param ctx the parse tree + */ + enterCommandOption?: (ctx: CommandOptionContext) => void; + /** + * Exit a parse tree produced by `esql_parser.commandOption`. + * @param ctx the parse tree + */ + exitCommandOption?: (ctx: CommandOptionContext) => void; /** * Enter a parse tree produced by `esql_parser.booleanValue`. @@ -511,6 +698,28 @@ export interface esql_parserListener extends ParseTreeListener { */ exitNumber?: (ctx: NumberContext) => void; + /** + * Enter a parse tree produced by `esql_parser.decimalValue`. + * @param ctx the parse tree + */ + enterDecimalValue?: (ctx: DecimalValueContext) => void; + /** + * Exit a parse tree produced by `esql_parser.decimalValue`. + * @param ctx the parse tree + */ + exitDecimalValue?: (ctx: DecimalValueContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.integerValue`. + * @param ctx the parse tree + */ + enterIntegerValue?: (ctx: IntegerValueContext) => void; + /** + * Exit a parse tree produced by `esql_parser.integerValue`. + * @param ctx the parse tree + */ + exitIntegerValue?: (ctx: IntegerValueContext) => void; + /** * Enter a parse tree produced by `esql_parser.string`. * @param ctx the parse tree @@ -554,5 +763,16 @@ export interface esql_parserListener extends ParseTreeListener { * @param ctx the parse tree */ exitSubqueryExpression?: (ctx: SubqueryExpressionContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.showCommand`. + * @param ctx the parse tree + */ + enterShowCommand?: (ctx: ShowCommandContext) => void; + /** + * Exit a parse tree produced by `esql_parser.showCommand`. + * @param ctx the parse tree + */ + exitShowCommand?: (ctx: ShowCommandContext) => void; } diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/comparison_commands.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/comparison_commands.ts index ec8cfe8e596c1..92b2e8f7c31d1 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/comparison_commands.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/comparison_commands.ts @@ -85,4 +85,32 @@ export const comparisonCommandsDefinitions: AutocompleteCommandDefinition[] = [ }), sortText: 'D', }, + { + label: 'like', + insertText: 'like', + kind: 11, + detail: i18n.translate('monaco.esql.autocomplete.likeDoc', { + defaultMessage: 'Filter data based on string patterns', + }), + sortText: 'D', + }, + { + label: 'rlike', + insertText: 'rlike', + kind: 11, + detail: i18n.translate('monaco.esql.autocomplete.rlikeDoc', { + defaultMessage: 'Filter data based on string regular expressions', + }), + sortText: 'D', + }, + { + label: 'in', + insertText: 'in', + kind: 11, + detail: i18n.translate('monaco.esql.autocomplete.inDoc', { + defaultMessage: + 'Tests if the value an expression takes is contained in a list of other expressions', + }), + sortText: 'D', + }, ]; diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/dynamic_commands.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/dynamic_commands.ts index aa9a9f1777ff3..4a5a147ffcbde 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/dynamic_commands.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/dynamic_commands.ts @@ -9,6 +9,23 @@ import { i18n } from '@kbn/i18n'; import type { AutocompleteCommandDefinition } from '../types'; +export const buildPoliciesDefinitions = ( + policies: Array<{ name: string; indices: string[] }> +): AutocompleteCommandDefinition[] => + policies.map(({ name: label, indices }) => ({ + label, + insertText: label, + kind: 5, + detail: i18n.translate('monaco.esql.autocomplete.policyDefinition', { + defaultMessage: `Policy defined on {count, plural, one {index} other {indices}}: {indices}`, + values: { + count: indices.length, + indices: indices.join(', '), + }, + }), + sortText: 'D', + })); + export const buildFieldsDefinitions = (fields: string[]): AutocompleteCommandDefinition[] => fields.map((label) => ({ label, @@ -20,6 +37,37 @@ export const buildFieldsDefinitions = (fields: string[]): AutocompleteCommandDef sortText: 'D', })); +export const buildNoPoliciesAvailableDefinition = (): AutocompleteCommandDefinition[] => [ + { + label: i18n.translate('monaco.esql.autocomplete.noPoliciesLabel', { + defaultMessage: 'No available policy', + }), + insertText: '', + kind: 26, + detail: i18n.translate('monaco.esql.autocomplete.noPoliciesLabelsFound', { + defaultMessage: 'No policies found', + }), + sortText: 'D', + }, +]; + +export const buildMatchingFieldsDefinition = ( + matchingField: string, + fields: string[] +): AutocompleteCommandDefinition[] => + fields.map((label) => ({ + label, + insertText: label, + kind: 4, + detail: i18n.translate('monaco.esql.autocomplete.matchingFieldDefinition', { + defaultMessage: `Use to match on {matchingField} on the policy`, + values: { + matchingField, + }, + }), + sortText: 'D', + })); + export const buildNewVarDefinition = (label: string): AutocompleteCommandDefinition => { return { label, diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/functions_commands.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/functions_commands.ts index 119a443c40190..53add21af9f1a 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/functions_commands.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/functions_commands.ts @@ -11,21 +11,408 @@ import { buildDocumentation } from './utils'; import type { AutocompleteCommandDefinition } from '../types'; -export const roundCommandDefinition: AutocompleteCommandDefinition = { - label: 'round', - insertText: 'round', - kind: 1, - detail: i18n.translate('monaco.esql.autocomplete.roundDoc', { - defaultMessage: - 'Returns a number rounded to the decimal, specified by he closest integer value. The default is to round to an integer.', - }), - documentation: { - value: buildDocumentation('round(grouped[T]): aggregated[T]', [ - 'from index where field="value" | eval rounded = round(field)', - ]), - }, - sortText: 'C', -}; +export const whereCommandDefinition: AutocompleteCommandDefinition[] = [ + { + label: 'cidr_match', + insertText: 'cidr_match', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.cidrMatchDoc', { + defaultMessage: + 'The function takes a first parameter of type IP, followed by one or more parameters evaluated to a CIDR specificatione.', + }), + documentation: { + value: buildDocumentation('cidr_match(grouped[T]): aggregated[T]', [ + 'from index | eval cidr="10.0.0.0/8" | where cidr_match(ip_field, "127.0.0.1/30", cidr)', + ]), + }, + sortText: 'C', + }, +]; + +export const mathCommandDefinition: AutocompleteCommandDefinition[] = [ + { + label: 'round', + insertText: 'round', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.roundDoc', { + defaultMessage: + 'Returns a number rounded to the decimal, specified by he closest integer value. The default is to round to an integer.', + }), + documentation: { + value: buildDocumentation('round(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval rounded = round(field)', + ]), + }, + sortText: 'C', + }, + { + label: 'abs', + insertText: 'abs', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.absDoc', { + defaultMessage: 'Returns the absolute value.', + }), + documentation: { + value: buildDocumentation('abs(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval abs_value = abs(field)', + ]), + }, + sortText: 'C', + }, + { + label: 'pow', + insertText: 'pow', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.powDoc', { + defaultMessage: + 'Returns the the value of a base (first argument) raised to a power (second argument).', + }), + documentation: { + value: buildDocumentation('pow(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval s = POW(field, exponent)', + ]), + }, + sortText: 'C', + }, + { + label: 'log10', + insertText: 'log10', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.log10Doc', { + defaultMessage: 'Returns the log base 10.', + }), + documentation: { + value: buildDocumentation('log10(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval s = log10(field)', + ]), + }, + sortText: 'C', + }, + { + label: 'concat', + insertText: 'concat', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.concatDoc', { + defaultMessage: 'Concatenates two or more strings.', + }), + documentation: { + value: buildDocumentation('concat(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval concatenated = concat(field1, "-", field2)', + ]), + }, + sortText: 'C', + }, + { + label: 'substring', + insertText: 'substring', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.substringDoc', { + defaultMessage: + '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.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval new_string = substring(field, 1, 3)', + ]), + }, + sortText: 'C', + }, + { + label: 'trim', + insertText: 'trim', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.trimDoc', { + defaultMessage: 'Removes leading and trailing whitespaces from strings.', + }), + documentation: { + value: buildDocumentation('trim(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval new_string = trim(field)', + ]), + }, + sortText: 'C', + }, + { + label: 'starts_with', + insertText: 'starts_with', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.startsWithDoc', { + defaultMessage: + 'Returns a boolean that indicates whether a keyword string starts with another string.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval new_string = starts_with(field, "a")', + ]), + }, + sortText: 'C', + }, + { + label: 'split', + insertText: 'split', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.splitDoc', { + defaultMessage: 'Splits a single valued string into multiple strings.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `ROW words="foo;bar;baz;qux;quux;corge" + | EVAL word = SPLIT(words, ";")`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_string', + insertText: 'to_string', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toStringDoc', { + defaultMessage: 'Converts to string.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL string = to_string(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_boolean', + insertText: 'to_boolean', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toBooleanDoc', { + defaultMessage: 'Converts to boolean.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL bool = to_boolean(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_datetime', + insertText: 'to_datetime', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toDateTimeDoc', { + defaultMessage: 'Converts to date.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL datetime = to_datetime(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_double', + insertText: 'to_double', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toDoubleDoc', { + defaultMessage: 'Converts to double.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL double = to_double(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_integer', + insertText: 'to_integer', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toIntegerDoc', { + defaultMessage: 'Converts to integer.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL int = to_integer(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_long', + insertText: 'to_long', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toLongDoc', { + defaultMessage: 'Converts to long.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL long = to_long(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_unsigned_long', + insertText: 'to_unsigned_long', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toUnsignedLongDoc', { + defaultMessage: 'Converts to unsigned long.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL long = to_unsigned_long(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_ip', + insertText: 'to_ip', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toIpDoc', { + defaultMessage: 'Converts to ip.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL ip = to_ip(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'to_version', + insertText: 'to_version', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.toVersionDoc', { + defaultMessage: 'Converts to version.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value"" + | EVAL version = to_version(field)`, + ]), + }, + sortText: 'C', + }, + { + label: 'date_format', + insertText: 'date_format', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.dateFormatDoc', { + defaultMessage: `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.`, + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval hired = date_format(hire_date, "YYYY-MM-dd")', + ]), + }, + sortText: 'C', + }, + { + label: 'date_trunc', + insertText: 'date_trunc', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.dateTruncDoc', { + defaultMessage: `Rounds down a date to the closest interval.`, + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval year_hired = DATE_TRUNC(hire_date, 1 year)', + ]), + }, + sortText: 'C', + }, + { + label: 'date_parse', + insertText: 'date_parse', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.dateParseDoc', { + defaultMessage: `Parse dates from strings.`, + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value" | eval year_hired = date_parse(hire_date, yyyy-MM-dd'T'HH:mm:ss.SSS'Z')`, + ]), + }, + sortText: 'C', + }, + { + label: 'auto_bucket', + insertText: 'auto_bucket', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.autoBucketDoc', { + defaultMessage: `Automatically bucket dates based on a given range and bucket target.`, + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval hd = auto_bucket(hire_date, 20, "1985-01-01T00:00:00Z", "1986-01-01T00:00:00Z")', + ]), + }, + sortText: 'C', + }, + { + label: 'is_finite', + insertText: 'is_finite', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.isFiniteDoc', { + defaultMessage: 'Returns a boolean that indicates whether its input is a finite number.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval s = is_finite(field/0)', + ]), + }, + sortText: 'C', + }, + { + label: 'is_infinite', + insertText: 'is_infinite', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.isInfiniteDoc', { + defaultMessage: 'Returns a boolean that indicates whether its input is infinite.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + 'from index where field="value" | eval s = is_infinite(field/0)', + ]), + }, + sortText: 'C', + }, + { + label: 'case', + insertText: 'case', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.caseDoc', { + defaultMessage: + '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.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value" | eval type = case( + languages <= 1, "monolingual", + languages <= 2, "bilingual", + "polyglot")`, + ]), + }, + sortText: 'C', + }, + { + label: 'length', + insertText: 'length', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.lengthDoc', { + defaultMessage: 'Returns the character length of a string.', + }), + documentation: { + value: buildDocumentation('substring(grouped[T]): aggregated[T]', [ + `from index where field="value" | eval fn_length = length(field)`, + ]), + }, + sortText: 'C', + }, +]; export const aggregationFunctionsDefinitions: AutocompleteCommandDefinition[] = [ { @@ -84,4 +471,75 @@ export const aggregationFunctionsDefinitions: AutocompleteCommandDefinition[] = }, sortText: 'C', }, + { + label: 'count', + insertText: 'count', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.countDoc', { + defaultMessage: 'Returns the count of the values in a field.', + }), + documentation: { + value: buildDocumentation('count(grouped[T]): aggregated[T]', [ + 'from index | stats count = count(field)', + ]), + }, + sortText: 'C', + }, + { + label: 'count_distinct', + insertText: 'count_distinct', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.countDistinctDoc', { + defaultMessage: 'Returns the count of distinct values in a field.', + }), + documentation: { + value: buildDocumentation('count(grouped[T]): aggregated[T]', [ + 'from index | stats count = count_distinct(field)', + ]), + }, + sortText: 'C', + }, + { + label: 'median', + insertText: 'median', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.medianDoc', { + defaultMessage: 'Returns the 50% percentile.', + }), + documentation: { + value: buildDocumentation('count(grouped[T]): aggregated[T]', [ + 'from index | stats count = median(field)', + ]), + }, + sortText: 'C', + }, + { + label: 'median_absolute_deviation', + insertText: 'median_absolute_deviation', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.medianDeviationDoc', { + defaultMessage: + 'Returns the median of each data point’s deviation from the median of the entire sample.', + }), + documentation: { + value: buildDocumentation('count(grouped[T]): aggregated[T]', [ + 'from index | stats count = median_absolute_deviation(field)', + ]), + }, + sortText: 'C', + }, + { + label: 'percentile', + insertText: 'percentile', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.percentiletDoc', { + defaultMessage: 'Returns the n percentile of a field.', + }), + documentation: { + value: buildDocumentation('percentile(grouped[T]): aggregated[T]', [ + 'from index | stats pct = percentile(field, 90)', + ]), + }, + sortText: 'C', + }, ]; diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/index.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/index.ts index ef096d678acc3..e1fb514cfa4de 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/index.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/index.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -export { aggregationFunctionsDefinitions, roundCommandDefinition } from './functions_commands'; +export { + aggregationFunctionsDefinitions, + mathCommandDefinition, + whereCommandDefinition, +} from './functions_commands'; export { sourceCommandsDefinitions } from './source_commands'; export { processingCommandsDefinitions, pipeDefinition } from './processing_commands'; @@ -17,6 +21,7 @@ export { export { mathOperatorsCommandsDefinitions, assignOperatorDefinition, + asOperatorDefinition, byOperatorDefinition, openBracketDefinition, closeBracketDefinition, diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/operators_commands.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/operators_commands.ts index 21a5f6260cedd..91ccb74cb9501 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/operators_commands.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/operators_commands.ts @@ -11,7 +11,7 @@ import type { AutocompleteCommandDefinition } from '../types'; export const byOperatorDefinition: AutocompleteCommandDefinition = { label: 'by', - insertText: 'by ', + insertText: 'by', kind: 21, detail: i18n.translate('monaco.esql.autocomplete.byDoc', { defaultMessage: 'By', @@ -19,6 +19,36 @@ export const byOperatorDefinition: AutocompleteCommandDefinition = { sortText: 'D', }; +export const onOperatorDefinition: AutocompleteCommandDefinition = { + label: 'on', + insertText: 'on', + kind: 21, + detail: i18n.translate('monaco.esql.autocomplete.onDoc', { + defaultMessage: 'On', + }), + sortText: 'D', +}; + +export const withOperatorDefinition: AutocompleteCommandDefinition = { + label: 'with', + insertText: 'with', + kind: 21, + detail: i18n.translate('monaco.esql.autocomplete.withDoc', { + defaultMessage: 'With', + }), + sortText: 'D', +}; + +export const asOperatorDefinition: AutocompleteCommandDefinition = { + label: 'as', + insertText: 'as', + kind: 11, + detail: i18n.translate('monaco.esql.autocomplete.asDoc', { + defaultMessage: 'As', + }), + sortText: 'D', +}; + export const assignOperatorDefinition: AutocompleteCommandDefinition = { label: '=', insertText: '=', diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/processing_commands.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/processing_commands.ts index 8dbc1ebe3d9c0..a53330d638158 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/processing_commands.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_definitions/processing_commands.ts @@ -66,6 +66,46 @@ export const processingCommandsDefinitions: AutocompleteCommandDefinition[] = [ }, sortText: 'B', }, + { + label: 'keep', + insertText: 'keep', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.keepDoc', { + defaultMessage: 'Rearranges fields in the input table by applying the keep clauses in fields', + }), + documentation: { + value: buildDocumentation('keep fieldSpecification `,` fieldSpecification *', [ + '… | keep a,b', + ]), + }, + sortText: 'B', + }, + { + label: 'rename', + insertText: 'rename', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.renameDoc', { + defaultMessage: 'Renames an old column to a new one', + }), + documentation: { + value: buildDocumentation('rename new as old', ['… | rename a as b']), + }, + sortText: 'B', + }, + { + label: 'drop', + insertText: 'drop', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.dropDoc', { + defaultMessage: 'Drops columns', + }), + documentation: { + value: buildDocumentation('drop fieldSpecification `,` fieldSpecification *', [ + '… | drop a,b', + ]), + }, + sortText: 'B', + }, { label: 'sort', insertText: 'sort', @@ -96,4 +136,61 @@ export const processingCommandsDefinitions: AutocompleteCommandDefinition[] = [ }, sortText: 'B', }, + { + label: 'dissect', + insertText: 'dissect', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.dissectDoc', { + defaultMessage: + 'Extracts multiple string values from a single string input, based on a pattern', + }), + documentation: { + value: buildDocumentation( + 'dissect (append_separator=)?', + ['… | dissect a "%{b} %{c}";'] + ), + }, + sortText: 'B', + }, + { + label: 'grok', + insertText: 'grok', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.grokDoc', { + defaultMessage: + 'Extracts multiple string values from a single string input, based on a pattern', + }), + documentation: { + value: buildDocumentation('grok ', [ + '… | grok a "%{b} %{c}";', + ]), + }, + sortText: 'B', + }, + { + label: 'mv_expand', + insertText: 'mv_expand', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.mvExpandDoc', { + defaultMessage: 'Expands multivalued fields into one row per value, duplicating other fields', + }), + documentation: { + value: buildDocumentation('mv_expand field', [ + 'ROW a=[1,2,3], b="b", j=["a","b"] | MV_EXPAND a', + ]), + }, + sortText: 'B', + }, + { + label: 'enrich', + insertText: 'enrich', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.enrichDoc', { + defaultMessage: 'Enrich table with another table', + }), + documentation: { + value: buildDocumentation('enrich policy', ['... | ENRICH a']), + }, + sortText: 'B', + }, ]; diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_listener.test.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_listener.test.ts index 157d111154f1f..bbcdb04060689 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_listener.test.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_listener.test.ts @@ -38,12 +38,22 @@ describe('autocomplete_listener', () => { testSuggestions('f', ['from']); testSuggestions('from ', ['SourceIdentifier']); testSuggestions('from a,', ['SourceIdentifier']); - testSuggestions('from a, b ', ['|']); + testSuggestions('from a, b ', ['SourceIdentifier']); }); describe('where', () => { - testSuggestions('from a | where ', ['FieldIdentifier']); - testSuggestions('from a | where "field" ', ['==', '!=', '<', '>', '<=', '>=']); + testSuggestions('from a | where ', ['cidr_match', 'FieldIdentifier']); + testSuggestions('from a | where "field" ', [ + '==', + '!=', + '<', + '>', + '<=', + '>=', + 'like', + 'rlike', + 'in', + ]); testSuggestions('from a | where "field" >= ', ['FieldIdentifier']); testSuggestions('from a | where "field" >= "field1" ', ['or', 'and', '|']); testSuggestions('from a | where "field" >= "field1" and ', ['FieldIdentifier']); @@ -54,9 +64,32 @@ describe('autocomplete_listener', () => { '>', '<=', '>=', + 'like', + 'rlike', + 'in', + ]); + testSuggestions('from a | stats a=avg("field") | where a ', [ + '==', + '!=', + '<', + '>', + '<=', + '>=', + 'like', + 'rlike', + 'in', + ]); + testSuggestions('from a | stats a=avg("b") | where "c" ', [ + '==', + '!=', + '<', + '>', + '<=', + '>=', + 'like', + 'rlike', + 'in', ]); - testSuggestions('from a | stats a=avg("field") | where a ', ['==', '!=', '<', '>', '<=', '>=']); - testSuggestions('from a | stats a=avg("b") | where "c" ', ['==', '!=', '<', '>', '<=', '>=']); testSuggestions('from a | where "field" >= "field1" and "field2 == ', ['FieldIdentifier']); }); @@ -72,11 +105,25 @@ describe('autocomplete_listener', () => { testSuggestions('from a | limit 4 ', ['|']); }); + describe('mv_expand', () => { + testSuggestions('from a | mv_expand ', ['FieldIdentifier']); + testSuggestions('from a | mv_expand a ', ['|']); + }); + describe('stats', () => { testSuggestions('from a | stats ', ['var0']); testSuggestions('from a | stats a ', ['=']); - testSuggestions('from a | stats a=', ['avg', 'max', 'min', 'sum', 'FieldIdentifier']); - testSuggestions('from a | stats a=b', ['|', 'by']); + testSuggestions('from a | stats a=', [ + 'avg', + 'max', + 'min', + 'sum', + 'count', + 'count_distinct', + 'median', + 'median_absolute_deviation', + 'percentile', + ]); testSuggestions('from a | stats a=b by ', ['FieldIdentifier']); testSuggestions('from a | stats a=c by d', ['|']); testSuggestions('from a | stats a=b, ', ['var0']); @@ -90,11 +137,75 @@ describe('autocomplete_listener', () => { testSuggestions('from a | stats a=min(b), b=max(', ['FieldIdentifier']); }); + describe('enrich', () => { + for (const prevCommand of [ + '', + '| enrich other-policy ', + '| enrich other-policy on b ', + '| enrich other-policy with c ', + ]) { + testSuggestions(`from a ${prevCommand}| enrich`, ['PolicyIdentifier']); + testSuggestions(`from a ${prevCommand}| enrich policy `, ['|', 'on', 'with']); + testSuggestions(`from a ${prevCommand}| enrich policy on `, [ + 'PolicyMatchingFieldIdentifier', + ]); + testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['|', 'with']); + testSuggestions(`from a ${prevCommand}| enrich policy on b with `, [ + 'var0', + 'PolicyFieldIdentifier', + ]); + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 `, ['=', '|']); + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = `, [ + 'PolicyFieldIdentifier', + ]); + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = c `, ['|']); + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = c, `, [ + 'var1', + 'PolicyFieldIdentifier', + ]); + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = c, var1 `, ['=', '|']); + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = c, var1 = `, [ + 'PolicyFieldIdentifier', + ]); + testSuggestions(`from a ${prevCommand}| enrich policy with `, [ + 'var0', + 'PolicyFieldIdentifier', + ]); + testSuggestions(`from a ${prevCommand}| enrich policy with c`, ['=', '|']); + } + }); + describe('eval', () => { testSuggestions('from a | eval ', ['var0']); testSuggestions('from a | eval a ', ['=']); - testSuggestions('from a | eval a=', ['round', 'FieldIdentifier']); - testSuggestions('from a | eval a=b', ['|', '+', '-', '/', '*']); + testSuggestions('from a | eval a=', [ + 'round', + 'abs', + 'pow', + 'log10', + 'concat', + 'substring', + 'trim', + 'starts_with', + 'split', + 'to_string', + 'to_boolean', + 'to_datetime', + 'to_double', + 'to_integer', + 'to_long', + 'to_unsigned_long', + 'to_ip', + 'to_version', + 'date_format', + 'date_trunc', + 'date_parse', + 'auto_bucket', + 'is_finite', + 'is_infinite', + 'case', + 'length', + ]); testSuggestions('from a | eval a=b, ', ['var0']); testSuggestions('from a | eval a=round', ['(']); testSuggestions('from a | eval a=round(', ['FieldIdentifier']); diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_listener.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_listener.ts index d3cda17124349..ad439caad1dce 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_listener.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/autocomplete_listener.ts @@ -10,7 +10,12 @@ import type { AutocompleteCommandDefinition, UserDefinedVariables } from './type import { DynamicAutocompleteItem } from './dymanic_item'; import { esql_parserListener as ESQLParserListener } from '../../antlr/esql_parser_listener'; -import { esql_parser, esql_parser as ESQLParser } from '../../antlr/esql_parser'; +import { + esql_parser, + esql_parser as ESQLParser, + EnrichCommandContext, + EnrichWithClauseContext, +} from '../../antlr/esql_parser'; import { processingCommandsDefinitions, @@ -26,10 +31,12 @@ import { closeBracketDefinition, mathOperatorsCommandsDefinitions, aggregationFunctionsDefinitions, - roundCommandDefinition, + mathCommandDefinition, + whereCommandDefinition, assignOperatorDefinition, buildConstantsDefinitions, buildNewVarDefinition, + asOperatorDefinition, } from './autocomplete_definitions'; import { @@ -45,22 +52,49 @@ import { SourceIdentifierContext, UserVariableContext, BooleanExpressionContext, + RegexBooleanExpressionContext, + WhereBooleanExpressionContext, LimitCommandContext, ValueExpressionContext, + KeepCommandContext, + DropCommandContext, + RenameCommandContext, + DissectCommandContext, + GrokCommandContext, + MvExpandCommandContext, } from '../../antlr/esql_parser'; +import { + onOperatorDefinition, + withOperatorDefinition, +} from './autocomplete_definitions/operators_commands'; + +export function nonNullable(v: T): v is NonNullable { + return v != null; +} export class AutocompleteListener implements ESQLParserListener { private suggestions: Array = []; private readonly userDefinedVariables: UserDefinedVariables = { sourceIdentifiers: [], + policyIdentifiers: [], }; private readonly tables: string[][] = []; private parentContext: number | undefined; - private get fields() { - return this.tables.length > 1 - ? buildConstantsDefinitions(this.tables.at(-2)!) - : [DynamicAutocompleteItem.FieldIdentifier]; + private get fields(): [DynamicAutocompleteItem] { + return [DynamicAutocompleteItem.FieldIdentifier]; + } + + private get policies(): [DynamicAutocompleteItem] { + return [DynamicAutocompleteItem.PolicyIdentifier]; + } + + private get policyFields(): [DynamicAutocompleteItem] { + return [DynamicAutocompleteItem.PolicyFieldIdentifier]; + } + + private get policyMatchingField(): [DynamicAutocompleteItem] { + return [DynamicAutocompleteItem.PolicyMatchingFieldIdentifier]; } private get hasSuggestions() { @@ -71,16 +105,23 @@ export class AutocompleteListener implements ESQLParserListener { return node && node.payload?.startIndex >= 0; } - private getEndCommandSuggestions(skipDefinitions: AutocompleteCommandDefinition[] = []) { - const suggestions = [pipeDefinition]; - - if ( - !skipDefinitions.find((i) => i === byOperatorDefinition) && - this.parentContext === ESQLParser.STATS - ) { - suggestions.push(byOperatorDefinition); + private applyConditionalSuggestion( + skipDefinitions: AutocompleteCommandDefinition[], + targetDefinition: AutocompleteCommandDefinition, + context: number + ) { + if (!skipDefinitions.find((i) => i === targetDefinition) && this.parentContext === context) { + return targetDefinition; } - return suggestions; + } + + private getEndCommandSuggestions(skipDefinitions: AutocompleteCommandDefinition[] = []) { + return [ + pipeDefinition, + this.applyConditionalSuggestion(skipDefinitions, byOperatorDefinition, ESQLParser.STATS), + this.applyConditionalSuggestion(skipDefinitions, onOperatorDefinition, ESQLParser.ENRICH), + this.applyConditionalSuggestion(skipDefinitions, withOperatorDefinition, ESQLParser.ENRICH), + ].filter(nonNullable); } private getNewVarName() { @@ -112,11 +153,13 @@ export class AutocompleteListener implements ESQLParserListener { exitSourceCommand(ctx: SourceCommandContext) { if (ctx.exception) { this.suggestions = sourceCommandsDefinitions; - } else if (!this.hasSuggestions) { - this.suggestions = this.getEndCommandSuggestions(); } } + enterSourceIdentifier(ctx: SourceIdentifierContext) { + this.suggestions = [DynamicAutocompleteItem.SourceIdentifier]; + } + exitSourceIdentifier(ctx: SourceIdentifierContext) { if (!ctx.childCount) { this.suggestions = [DynamicAutocompleteItem.SourceIdentifier]; @@ -141,6 +184,11 @@ export class AutocompleteListener implements ESQLParserListener { enterStatsCommand(ctx: StatsCommandContext) { this.suggestions = []; this.parentContext = ESQLParser.STATS; + const fn = ctx.fields(); + if (!fn) { + this.suggestions = [buildNewVarDefinition(this.getNewVarName())]; + return; + } } enterEvalCommand(ctx: EvalCommandContext) { @@ -155,7 +203,84 @@ export class AutocompleteListener implements ESQLParserListener { } } + exitKeepCommand?(ctx: KeepCommandContext) { + const qn = ctx.qualifiedNames(); + if (qn && qn.text) { + if (qn.text.slice(-1) !== ',') { + this.suggestions = this.getEndCommandSuggestions(); + } + } + } + + exitDropCommand?(ctx: DropCommandContext) { + const qn = ctx.qualifiedNames(); + if (qn && qn.text) { + if (qn.text.slice(-1) !== ',') { + this.suggestions = this.getEndCommandSuggestions(); + } + } + } + + enterRenameCommand(ctx: RenameCommandContext) { + this.parentContext = ESQLParser.RENAME; + } + + exitRenameCommand?(ctx: RenameCommandContext) { + const rc = ctx.renameClause(); + const commaExists = ctx.COMMA(); + if (!rc[0].exception) { + const qn = rc[0].renameVariable(); + const asExists = this.isTerminalNodeExists(rc[0].AS()); + if (asExists && qn && !qn.text) { + this.suggestions = []; + } + if (qn && qn.text) { + if (!commaExists.length) { + this.suggestions = this.getEndCommandSuggestions(); + } + } + } + } + + exitDissectCommand?(ctx: DissectCommandContext) { + const qn = ctx.qualifiedNames(); + const pattern = ctx.string(); + if (qn && qn.text && pattern && pattern.text && pattern.text !== '') { + this.suggestions = this.getEndCommandSuggestions(); + } + } + + exitGrokCommand?(ctx: GrokCommandContext) { + const qn = ctx.qualifiedNames(); + const pattern = ctx.string(); + if (qn && qn.text && pattern && pattern.text && pattern.text !== '') { + this.suggestions = this.getEndCommandSuggestions(); + } + } + + exitMvExpandCommand?(ctx: MvExpandCommandContext) { + const qn = ctx.qualifiedNames(); + if (qn && qn.text) { + this.suggestions = this.getEndCommandSuggestions(); + } + } + exitQualifiedName(ctx: QualifiedNameContext) { + const isInEval = this.parentContext === ESQLParser.EVAL; + const isInStats = this.parentContext === ESQLParser.STATS; + const isInRename = this.parentContext === ESQLParser.RENAME; + if (this.parentContext && isInRename) { + if (!ctx.exception && ctx.text) { + this.suggestions = [asOperatorDefinition]; + } + } + if (this.parentContext && (isInStats || isInEval)) { + this.suggestions = [ + ...this.getEndCommandSuggestions(), + ...(isInEval ? mathOperatorsCommandsDefinitions : []), + ]; + } + if ( ctx .identifier() @@ -205,12 +330,12 @@ export class AutocompleteListener implements ESQLParserListener { const ve = ctx.valueExpression(); if (!ve) { if (this.parentContext === ESQLParser.STATS) { - this.suggestions = [...aggregationFunctionsDefinitions, ...this.fields]; + this.suggestions = [...aggregationFunctionsDefinitions]; return; } if (this.parentContext === ESQLParser.EVAL) { - this.suggestions = [roundCommandDefinition, ...this.fields]; + this.suggestions = [...mathCommandDefinition]; return; } } @@ -222,7 +347,9 @@ export class AutocompleteListener implements ESQLParserListener { const isInEval = this.parentContext === ESQLParser.EVAL; if (this.parentContext && (isInStats || isInEval)) { - const hasFN = ctx.tryGetToken(esql_parser.UNARY_FUNCTION, 0); + const hasFN = + ctx.tryGetToken(esql_parser.UNARY_FUNCTION, 0) || + ctx.tryGetToken(esql_parser.MATH_FUNCTION, 0); const hasLP = ctx.tryGetToken(esql_parser.LP, 0); const hasRP = ctx.tryGetToken(esql_parser.RP, 0); @@ -239,10 +366,12 @@ export class AutocompleteListener implements ESQLParserListener { } } else { if (ctx.childCount === 1) { - this.suggestions = [ - ...this.getEndCommandSuggestions(), - ...(isInEval ? mathOperatorsCommandsDefinitions : []), - ]; + if (ctx.text && ctx.text.indexOf('(') === -1) { + this.suggestions = [ + ...(isInEval ? mathCommandDefinition : []), + ...(isInStats ? aggregationFunctionsDefinitions : []), + ]; + } return; } } @@ -250,25 +379,117 @@ export class AutocompleteListener implements ESQLParserListener { } } + enterWhereBooleanExpression(ctx: WhereBooleanExpressionContext) { + this.suggestions = []; + } + enterWhereCommand(ctx: WhereCommandContext) { this.suggestions = []; this.parentContext = ESQLParser.WHERE; } + enterEnrichCommand(ctx: EnrichCommandContext) { + this.suggestions = []; + this.parentContext = ESQLParser.ENRICH; + } + + exitEnrichCommand(ctx: EnrichCommandContext) { + const policyName = ctx.enrichIdentifier().text; + if (policyName && !this.userDefinedVariables.policyIdentifiers.includes(policyName)) { + this.userDefinedVariables.policyIdentifiers.push(policyName); + } + + if (this.parentContext === ESQLParser.WITH) { + return; + } + if (!policyName) { + this.suggestions = this.policies; + } + + if (policyName) + if (this.parentContext === ESQLParser.ENRICH) { + const hasOn = this.isTerminalNodeExists(ctx.ON()); + if (hasOn && !ctx._matchField.text) { + this.suggestions = this.policyMatchingField; + } else { + this.suggestions = this.getEndCommandSuggestions( + hasOn ? [onOperatorDefinition] : undefined + ); + } + } + } + + enterEnrichWithClause(ctx: EnrichWithClauseContext) { + this.suggestions = []; + this.parentContext = ESQLParser.WITH; + } + + exitEnrichWithClause(ctx: EnrichWithClauseContext) { + const hasAssign = this.isTerminalNodeExists(ctx.ASSIGN()); + // Note: this gets filled only after the assign operation :( + if (ctx._newName?.text) { + this.tables.at(-1)?.push(ctx._newName.text); + } + + if (!ctx.exception && ctx.enrichFieldIdentifier().length === 1) { + // if it's after the assign operator, then suggest the fields from the policy + // TODO: need to check if the enrichFieldIdentifier given is a policyField or not and decide whether append the assignOperator + this.suggestions = !hasAssign + ? [assignOperatorDefinition, ...this.getEndCommandSuggestions()] + : this.policyFields; + } else { + this.suggestions = []; + if (!hasAssign) { + this.suggestions.push(buildNewVarDefinition(this.getNewVarName())); + } + if (!ctx._enrichField?.text) { + this.suggestions.push(...this.policyFields); + } + if (this.suggestions.length === 0) { + this.suggestions = this.getEndCommandSuggestions([ + onOperatorDefinition, + withOperatorDefinition, + ]); + } + } + } + exitWhereCommand(ctx: WhereCommandContext) { - const booleanExpression = ctx.booleanExpression(); + const booleanExpression = ctx.whereBooleanExpression(); if (booleanExpression.exception) { + if (!booleanExpression.text) { + this.suggestions = [...whereCommandDefinition, ...this.fields]; + return; + } this.suggestions = this.fields; return; } else { - const innerBooleanExpressions = booleanExpression.getRuleContexts(BooleanExpressionContext); + const innerBooleanExpressions = booleanExpression.getRuleContexts( + WhereBooleanExpressionContext + ); + const regexBooleanExpression = booleanExpression.getRuleContexts( + RegexBooleanExpressionContext + ); + + if (booleanExpression.WHERE_FUNCTIONS()) { + if (booleanExpression.COMMA().length) { + this.suggestions = []; + return; + } + } + + if (regexBooleanExpression.length) { + this.suggestions = []; + return; + } + if (innerBooleanExpressions.some((be) => be.exception)) { this.suggestions = this.fields; return; } } - if (!this.hasSuggestions) { + if (!this.hasSuggestions && !booleanExpression.WHERE_FUNCTIONS()) { this.suggestions = comparisonCommandsDefinitions; } } diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/dymanic_item.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/dymanic_item.ts index b819dc34059a1..621c8900447a0 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/dymanic_item.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/dymanic_item.ts @@ -9,10 +9,13 @@ export enum DynamicAutocompleteItem { SourceIdentifier = 'SourceIdentifier', FieldIdentifier = 'FieldIdentifier', + PolicyIdentifier = 'PolicyIdentifier', + PolicyFieldIdentifier = 'PolicyFieldIdentifier', + PolicyMatchingFieldIdentifier = 'PolicyMatchingFieldIdentifier', } +const DynamicAutocompleteItems = Object.values(DynamicAutocompleteItem); + export function isDynamicAutocompleteItem(v: unknown): v is DynamicAutocompleteItem { - return ( - v === DynamicAutocompleteItem.SourceIdentifier || v === DynamicAutocompleteItem.FieldIdentifier - ); + return DynamicAutocompleteItems.some((dai) => dai === v); } diff --git a/packages/kbn-monaco/src/esql/lib/autocomplete/types.ts b/packages/kbn-monaco/src/esql/lib/autocomplete/types.ts index 58438baa298a9..0b64f0871b27a 100644 --- a/packages/kbn-monaco/src/esql/lib/autocomplete/types.ts +++ b/packages/kbn-monaco/src/esql/lib/autocomplete/types.ts @@ -12,17 +12,21 @@ import { monaco } from '../../../..'; export interface ESQLCustomAutocompleteCallbacks { getSourceIdentifiers?: CallbackFn; getFieldsIdentifiers?: CallbackFn; + getPoliciesIdentifiers?: CallbackFn<{ name: string; indices: string[] }>; + getPolicyFieldsIdentifiers?: CallbackFn; + getPolicyMatchingFieldIdentifiers?: CallbackFn; } /** @internal **/ -type CallbackFn = (ctx: { +type CallbackFn = (ctx: { word: string; userDefinedVariables: UserDefinedVariables; -}) => string[] | Promise; +}) => T[] | Promise; /** @internal **/ export interface UserDefinedVariables { sourceIdentifiers: string[]; + policyIdentifiers: string[]; } /** @internal **/ diff --git a/packages/kbn-monaco/src/esql/lib/monaco/esql_completion_provider.ts b/packages/kbn-monaco/src/esql/lib/monaco/esql_completion_provider.ts index 40393fe1b844d..4a407c3519769 100644 --- a/packages/kbn-monaco/src/esql/lib/monaco/esql_completion_provider.ts +++ b/packages/kbn-monaco/src/esql/lib/monaco/esql_completion_provider.ts @@ -11,7 +11,11 @@ import { DynamicAutocompleteItem, isDynamicAutocompleteItem } from '../autocompl import { buildFieldsDefinitions, buildSourcesDefinitions, + buildPoliciesDefinitions, + buildNoPoliciesAvailableDefinition, + buildMatchingFieldsDefinition, } from '../autocomplete/autocomplete_definitions/dynamic_commands'; +import { pipeDefinition } from '../autocomplete/autocomplete_definitions'; import type { AutocompleteCommandDefinition, @@ -20,11 +24,6 @@ import type { } from '../autocomplete/types'; import type { ESQLWorker } from '../../worker/esql_worker'; -const emptyCompletionList: monaco.languages.CompletionList = { - incomplete: false, - suggestions: [], -}; - export class ESQLCompletionAdapter implements monaco.languages.CompletionItemProvider { constructor( private worker: (...uris: monaco.Uri[]) => Promise, @@ -40,16 +39,20 @@ export class ESQLCompletionAdapter implements monaco.languages.CompletionItemPro userDefinedVariables: UserDefinedVariables; } ): Promise { - let result: AutocompleteCommandDefinition[] = []; - - for (const suggestion of suggestions) { - if (isDynamicAutocompleteItem(suggestion)) { + const allSuggestions: AutocompleteCommandDefinition[][] = await Promise.all( + suggestions.map(async (suggestion) => { + if (!isDynamicAutocompleteItem(suggestion)) { + return [suggestion]; + } let dynamicItems: AutocompleteCommandDefinition[] = []; if (suggestion === DynamicAutocompleteItem.SourceIdentifier) { dynamicItems = buildSourcesDefinitions( (await this.callbacks?.getSourceIdentifiers?.(ctx)) ?? [] ); + if (!ctx.word && ctx.userDefinedVariables.sourceIdentifiers.length) { + dynamicItems = [pipeDefinition]; + } } if (suggestion === DynamicAutocompleteItem.FieldIdentifier) { @@ -57,13 +60,34 @@ export class ESQLCompletionAdapter implements monaco.languages.CompletionItemPro (await this.callbacks?.getFieldsIdentifiers?.(ctx)) ?? [] ); } - result = [...result, ...dynamicItems]; - } else { - result = [...result, suggestion]; - } - } - return result; + if (suggestion === DynamicAutocompleteItem.PolicyIdentifier) { + const results = await this.callbacks?.getPoliciesIdentifiers?.(ctx); + dynamicItems = results?.length + ? buildPoliciesDefinitions(results) + : buildNoPoliciesAvailableDefinition(); + } + + if (suggestion === DynamicAutocompleteItem.PolicyFieldIdentifier) { + dynamicItems = buildFieldsDefinitions( + (await this.callbacks?.getPolicyFieldsIdentifiers?.(ctx)) || [] + ); + } + + if (suggestion === DynamicAutocompleteItem.PolicyMatchingFieldIdentifier) { + const [fields = [], matchingField] = await Promise.all([ + this.callbacks?.getFieldsIdentifiers?.(ctx), + this.callbacks?.getPolicyMatchingFieldIdentifiers?.(ctx), + ]); + dynamicItems = matchingField?.length + ? buildMatchingFieldsDefinition(matchingField[0], fields) + : buildFieldsDefinitions(fields); + } + return dynamicItems; + }) + ); + + return allSuggestions.flat(); } async provideCompletionItems( @@ -72,21 +96,23 @@ export class ESQLCompletionAdapter implements monaco.languages.CompletionItemPro ): Promise { const lines = model.getLineCount(); - if ( + const currentLineChars = model.getValueInRange({ + startLineNumber: 0, + startColumn: 0, + endLineNumber: position.lineNumber, + endColumn: position.column, + }); + const wordInfo = model.getWordUntilPosition(position); + const worker = await this.worker(model.uri); + const providedSuggestions = lines !== position.lineNumber || model.getLineContent(position.lineNumber).trimEnd().length >= position.column - ) { - return emptyCompletionList; - } - - const worker = await this.worker(model.uri); - const wordInfo = model.getWordUntilPosition(position); - - const providedSuggestions = await worker.provideAutocompleteSuggestions(model.uri.toString(), { - word: wordInfo.word, - line: position.lineNumber, - index: position.column, - }); + ? await worker.provideAutocompleteSuggestionsFromString(currentLineChars) + : await worker.provideAutocompleteSuggestions(model.uri.toString(), { + word: wordInfo.word, + line: position.lineNumber, + index: position.column, + }); const withDynamicItems = providedSuggestions ? await this.injectDynamicAutocompleteItems(providedSuggestions.suggestions, { @@ -96,7 +122,6 @@ export class ESQLCompletionAdapter implements monaco.languages.CompletionItemPro : []; return { - incomplete: true, suggestions: withDynamicItems.map((i) => ({ ...i, range: { diff --git a/packages/kbn-monaco/src/esql/lib/monaco/esql_theme.ts b/packages/kbn-monaco/src/esql/lib/monaco/esql_theme.ts index 94c3c6bbe6897..6fc6caee2886f 100644 --- a/packages/kbn-monaco/src/esql/lib/monaco/esql_theme.ts +++ b/packages/kbn-monaco/src/esql/lib/monaco/esql_theme.ts @@ -27,7 +27,6 @@ export const buildESQlTheme = (): monaco.editor.IStandaloneThemeData => ({ 'explain', 'row', 'limit', - 'project', 'ws', 'assign', 'comma', @@ -55,25 +54,48 @@ export const buildESQlTheme = (): monaco.editor.IStandaloneThemeData => ({ ...buildRuleGroup( [ 'from', + 'metadata', + 'mv_expand', 'stats', + 'dissect', + 'grok', + 'project', + 'keep', + 'rename', + 'drop', 'eval', 'sort', 'by', 'where', + 'not', + 'is', + 'like', + 'rlike', + 'in', + 'as', 'expr_ws', 'row', + 'show', 'limit', + 'cidr_match', 'nulls_ordering_direction', 'nulls_ordering', 'null', 'boolean_value', 'comparison_operator', + 'enrich', + 'on', + 'with', ], euiThemeVars.euiColorPrimaryText ), - // math functions + // aggregation functions ...buildRuleGroup(['unary_function'], euiThemeVars.euiColorPrimaryText), + // is null functions + ...buildRuleGroup(['where_functions'], euiThemeVars.euiColorPrimaryText), + // math functions + ...buildRuleGroup(['math_function'], euiThemeVars.euiColorPrimaryText), // operators ...buildRuleGroup( diff --git a/packages/kbn-monaco/src/esql/worker/esql_worker.ts b/packages/kbn-monaco/src/esql/worker/esql_worker.ts index 4d52c2b1094cb..4656ac9e9db7c 100644 --- a/packages/kbn-monaco/src/esql/worker/esql_worker.ts +++ b/packages/kbn-monaco/src/esql/worker/esql_worker.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CharStreams } from 'antlr4ts'; +import { CharStreams, type CodePointCharStream } from 'antlr4ts'; import { monaco } from '../../monaco_imports'; import { AutocompleteListener } from '../lib/autocomplete/autocomplete_listener'; import type { BaseWorkerDefinition } from '../../types'; @@ -43,16 +43,9 @@ export class ESQLWorker implements BaseWorkerDefinition { return []; } - public async provideAutocompleteSuggestions( - modelUri: string, - meta: { - word: string; - line: number; - index: number; - } + private async provideAutocompleteSuggestionFromRawString( + inputStream: CodePointCharStream | undefined ) { - const inputStream = this.getModelCharStream(modelUri); - if (inputStream) { const errorListener = new ANTLREErrorListener(); const parseListener = new AutocompleteListener(); @@ -63,4 +56,19 @@ export class ESQLWorker implements BaseWorkerDefinition { return parseListener.getAutocompleteSuggestions(); } } + + public async provideAutocompleteSuggestions( + modelUri: string, + meta: { + word: string; + line: number; + index: number; + } + ) { + return this.provideAutocompleteSuggestionFromRawString(this.getModelCharStream(modelUri)); + } + + public async provideAutocompleteSuggestionsFromString(text: string) { + return this.provideAutocompleteSuggestionFromRawString(CharStreams.fromString(text)); + } } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index dc07f316c5244..a3bee52c09267 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -28,7 +28,7 @@ pageLoadAssetSize: dashboard: 82025 dashboardEnhanced: 65646 data: 454087 - dataViewEditor: 13000 + dataViewEditor: 28082 dataViewFieldEditor: 27000 dataViewManagement: 5000 dataViews: 47000 @@ -36,7 +36,6 @@ pageLoadAssetSize: devTools: 38637 discover: 99999 discoverEnhanced: 42730 - discoverLogExplorer: 39045 embeddable: 87309 embeddableEnhanced: 22107 enterpriseSearch: 50858 @@ -87,6 +86,7 @@ pageLoadAssetSize: licenseManagement: 41817 licensing: 29004 lists: 22900 + logExplorer: 39045 logsShared: 281060 logstash: 53548 management: 46112 @@ -99,6 +99,7 @@ pageLoadAssetSize: noDataPage: 5000 observability: 115443 observabilityAIAssistant: 25000 + observabilityLogExplorer: 23686 observabilityOnboarding: 19573 observabilityShared: 52256 osquery: 107090 @@ -121,7 +122,7 @@ pageLoadAssetSize: security: 81771 securitySolution: 66738 securitySolutionEss: 16573 - securitySolutionServerless: 40000 + securitySolutionServerless: 45000 serverless: 16573 serverlessObservability: 68747 serverlessSearch: 71995 @@ -141,6 +142,7 @@ pageLoadAssetSize: triggersActionsUi: 135613 uiActions: 35121 uiActionsEnhanced: 38494 + unifiedDocViewer: 25099 unifiedHistogram: 19928 unifiedSearch: 71059 upgradeAssistant: 81241 diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts index e3d1ca81e2c26..7a3cf7f85ddf2 100644 --- a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -54,7 +54,8 @@ it('builds a generated plugin into a viable archive', async () => { }; expect(filterLogs(generateProc.all)).toMatchInlineSnapshot(` - " succ 🎉 + "Kibana is currently running with legacy OpenSSL providers enabled! For details and instructions on how to disable see https://www.elastic.co/guide/en/kibana/current/production.html#openssl-legacy-provider + succ 🎉 Your plugin has been created in plugins/foo_test_plugin " @@ -73,7 +74,8 @@ it('builds a generated plugin into a viable archive', async () => { ); expect(filterLogs(buildProc.all)).toMatchInlineSnapshot(` - " info deleting the build and target directories + "Kibana is currently running with legacy OpenSSL providers enabled! For details and instructions on how to disable see https://www.elastic.co/guide/en/kibana/current/production.html#openssl-legacy-provider + info deleting the build and target directories info run bazel and build required artifacts for the optimizer succ bazel run successfully and artifacts were created info running @kbn/optimizer diff --git a/packages/kbn-search-api-panels/types.ts b/packages/kbn-search-api-panels/types.ts index a02afebef5c87..5aba2d7b46bc0 100644 --- a/packages/kbn-search-api-panels/types.ts +++ b/packages/kbn-search-api-panels/types.ts @@ -23,6 +23,9 @@ export interface LanguageDefinitionSnippetArguments { url: string; apiKey: string; indexName?: string; + cloudId?: string; + ingestPipeline?: string; + extraIngestDocumentValues?: Record; } type CodeSnippet = string | ((args: LanguageDefinitionSnippetArguments) => string); diff --git a/packages/kbn-securitysolution-list-api/src/api/index.ts b/packages/kbn-securitysolution-list-api/src/api/index.ts index 01a74756ce9be..98b83b6279953 100644 --- a/packages/kbn-securitysolution-list-api/src/api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/api/index.ts @@ -581,6 +581,7 @@ export const getExceptionFilterFromExceptionListIds = async ({ }: GetExceptionFilterFromExceptionListIdsProps): Promise => http.fetch(INTERNAL_EXCEPTION_FILTER, { method: 'POST', + version: '1', body: JSON.stringify({ exception_list_ids: exceptionListIds, type: 'exception_list_ids', @@ -609,6 +610,7 @@ export const getExceptionFilterFromExceptions = async ({ }: GetExceptionFilterFromExceptionsProps): Promise => http.fetch(INTERNAL_EXCEPTION_FILTER, { method: 'POST', + version: '1', body: JSON.stringify({ exceptions, type: 'exception_items', diff --git a/packages/kbn-securitysolution-list-api/src/list_api/index.ts b/packages/kbn-securitysolution-list-api/src/list_api/index.ts index 81f4e9c94b3f0..5c280c7959763 100644 --- a/packages/kbn-securitysolution-list-api/src/list_api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/list_api/index.ts @@ -117,6 +117,7 @@ const findListsBySize = async ({ }: ApiParams & FindListSchemaEncoded): Promise => { return http.fetch(`${INTERNAL_FIND_LISTS_BY_SIZE}`, { method: 'GET', + version: '1', query: { cursor, page, @@ -272,6 +273,7 @@ export { readListIndexWithValidation as readListIndex }; // TODO add types and validation export const readListPrivileges = async ({ http, signal }: ApiParams): Promise => http.fetch(LIST_PRIVILEGES_URL, { + version: '2023-10-31', method: 'GET', signal, }); diff --git a/packages/kbn-test/index.ts b/packages/kbn-test/index.ts index f779803d794b1..5be415161a4a5 100644 --- a/packages/kbn-test/index.ts +++ b/packages/kbn-test/index.ts @@ -38,6 +38,7 @@ export { kibanaTestUser, adminTestUser, systemIndicesSuperuser, + kibanaTestSuperuserServerless, } from './src/kbn'; // @internal @@ -58,3 +59,5 @@ export * from './src/kbn_archiver_cli'; export * from './src/kbn_client'; export * from './src/find_test_plugin_paths'; + +export { getDockerFileMountPath } from '@kbn/es'; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index bbe3cb2923280..ed5a174e2db80 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -105,7 +105,7 @@ module.exports = { transformIgnorePatterns: [ // ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import() // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) - '[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|vscode-languageserver-types|react-monaco-editor|d3-interpolate|d3-color|langchain|langsmith))[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|vscode-languageserver-types|react-monaco-editor|d3-interpolate|d3-color|langchain|langsmith|@cfworker))[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js', '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/[/\\\\].+\\.js$', '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/util/[/\\\\].+\\.js$', diff --git a/packages/kbn-test/kbn_test_config.ts b/packages/kbn-test/kbn_test_config.ts index 1e656f3347909..7a4d868d324a5 100644 --- a/packages/kbn-test/kbn_test_config.ts +++ b/packages/kbn-test/kbn_test_config.ts @@ -18,12 +18,17 @@ export interface UrlParts { password?: string; } +interface UserAuth { + username: string; + password: string; +} + export const kbnTestConfig = new (class KbnTestConfig { getPort() { return this.getUrlParts().port; } - getUrlParts(): UrlParts { + getUrlParts(user: UserAuth = kibanaTestUser): UrlParts { // allow setting one complete TEST_KIBANA_URL for ES like https://elastic:changeme@example.com:9200 if (process.env.TEST_KIBANA_URL) { const testKibanaUrl = url.parse(process.env.TEST_KIBANA_URL); @@ -37,8 +42,8 @@ export const kbnTestConfig = new (class KbnTestConfig { }; } - const username = process.env.TEST_KIBANA_USERNAME || kibanaTestUser.username; - const password = process.env.TEST_KIBANA_PASSWORD || kibanaTestUser.password; + const username = process.env.TEST_KIBANA_USERNAME || user.username; + const password = process.env.TEST_KIBANA_PASSWORD || user.password; return { protocol: process.env.TEST_KIBANA_PROTOCOL || 'http', hostname: process.env.TEST_KIBANA_HOSTNAME || 'localhost', diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index 9b2a3b8010be2..580840b6b35a8 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -143,6 +143,14 @@ export interface CreateTestEsClusterOptions { * this caller to react appropriately. If this is not passed then an uncatchable exception will be thrown */ onEarlyExit?: (msg: string) => void; + /** + * Is this a serverless project + */ + serverless?: boolean; + /** + * Files to mount inside ES containers + */ + files?: string[]; } export function createTestEsCluster< @@ -164,6 +172,7 @@ export function createTestEsCluster< ssl, transportPort, onEarlyExit, + files, } = options; const clusterName = `${CI_PARALLEL_PROCESS_PREFIX}${customClusterName}`; @@ -218,6 +227,18 @@ export function createTestEsCluster< installPath = (await firstNode.installSource(config)).installPath; } else if (esFrom === 'snapshot') { installPath = (await firstNode.installSnapshot(config)).installPath; + } else if (esFrom === 'serverless') { + return await firstNode.runServerless({ + basePath, + esArgs: customEsArgs, + port, + clean: true, + teardown: true, + ssl: true, + background: true, + files, + kill: true, // likely don't need this but avoids any issues where the ESS cluster wasn't cleaned up + }); } else if (Path.isAbsolute(esFrom)) { installPath = esFrom; } else { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 03c0cbc07e644..27bdee55da128 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -210,6 +210,7 @@ export const schema = Joi.object() scheme: /https?/, }), }), + files: Joi.array().items(Joi.string()), }) .default(), diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index 40d4da7d76d76..09e251d70a25b 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -46,6 +46,8 @@ function getEsConfig({ : config.get('servers.elasticsearch.password'); const dataArchive: string | undefined = config.get('esTestCluster.dataArchive'); + const serverless: boolean = config.get('serverless'); + const files: string[] | undefined = config.get('esTestCluster.files'); return { ssl, @@ -58,6 +60,8 @@ function getEsConfig({ password, dataArchive, ccsConfig, + serverless, + files, }; } @@ -140,6 +144,8 @@ async function startEsNode({ ], transportPort: config.transportPort, onEarlyExit, + serverless: config.serverless, + files: config.files, }); await cluster.start(); diff --git a/packages/kbn-test/src/functional_tests/run_tests/flags.test.ts b/packages/kbn-test/src/functional_tests/run_tests/flags.test.ts index dbb8b1e9762e5..0c9dae3a25794 100644 --- a/packages/kbn-test/src/functional_tests/run_tests/flags.test.ts +++ b/packages/kbn-test/src/functional_tests/run_tests/flags.test.ts @@ -41,7 +41,7 @@ describe('parse runTest flags', () => { /foo, ], "dryRun": false, - "esFrom": "snapshot", + "esFrom": undefined, "esVersion": , "grep": undefined, "installDir": undefined, @@ -108,7 +108,7 @@ describe('parse runTest flags', () => { it('validates esFrom', () => { expect(() => test({ esFrom: 'foo' })).toThrowErrorMatchingInlineSnapshot( - `"invalid --esFrom, expected one of \\"snapshot\\", \\"source\\""` + `"invalid --esFrom, expected one of \\"snapshot\\", \\"source\\", \\"serverless\\""` ); }); diff --git a/packages/kbn-test/src/functional_tests/run_tests/flags.ts b/packages/kbn-test/src/functional_tests/run_tests/flags.ts index 9f91bf2728cbe..f4dd6beb26e80 100644 --- a/packages/kbn-test/src/functional_tests/run_tests/flags.ts +++ b/packages/kbn-test/src/functional_tests/run_tests/flags.ts @@ -36,7 +36,7 @@ export const FLAG_OPTIONS: FlagOptions = { help: ` --config Define a FTR config that should be executed. Can be specified multiple times --journey Define a Journey that should be executed. Can be specified multiple times - --esFrom Build Elasticsearch from source or run from snapshot. Default: $TEST_ES_FROM or "snapshot" + --esFrom Build Elasticsearch from source or run snapshot or serverless. Default: $TEST_ES_FROM or "snapshot" --include-tag Tags that suites must include to be run, can be included multiple times --exclude-tag Tags that suites must NOT include to be run, can be included multiple times --include Files that must included to be run, can be included multiple times @@ -74,7 +74,7 @@ export function parseFlags(flags: FlagsReader) { logsDir: flags.boolean('logToFile') ? Path.resolve(REPO_ROOT, 'data/ftr_servers_logs', uuidV4()) : undefined, - esFrom: flags.enum('esFrom', ['snapshot', 'source']) ?? 'snapshot', + esFrom: flags.enum('esFrom', ['snapshot', 'source', 'serverless']), installDir: flags.path('kibana-install-dir'), grep: flags.string('grep'), suiteTags: { diff --git a/packages/kbn-test/src/functional_tests/run_tests/run_tests.ts b/packages/kbn-test/src/functional_tests/run_tests/run_tests.ts index b8edfeadbdf08..81c0039001197 100644 --- a/packages/kbn-test/src/functional_tests/run_tests/run_tests.ts +++ b/packages/kbn-test/src/functional_tests/run_tests/run_tests.ts @@ -42,6 +42,9 @@ export async function runTests(log: ToolingLog, options: RunTestsOptions) { dryRun: options.dryRun, grep: options.grep, }, + esTestCluster: { + from: options.esFrom, + }, kbnTestServer: { installDir: options.installDir, }, diff --git a/packages/kbn-test/src/functional_tests/start_servers/flags.ts b/packages/kbn-test/src/functional_tests/start_servers/flags.ts index 0f53ca6866fa8..ad2a143f6db7a 100644 --- a/packages/kbn-test/src/functional_tests/start_servers/flags.ts +++ b/packages/kbn-test/src/functional_tests/start_servers/flags.ts @@ -23,7 +23,7 @@ export const FLAG_OPTIONS: FlagOptions = { help: ` --config Define a FTR config that should be executed. Can be specified multiple times --journey Define a Journey that should be executed. Can be specified multiple times - --esFrom Build Elasticsearch from source or run from snapshot. Default: $TEST_ES_FROM or "snapshot" + --esFrom Build Elasticsearch from source or run snapshot or serverless. Default: $TEST_ES_FROM or "snapshot" --kibana-install-dir Run Kibana from existing install directory instead of from source --logToFile Write the log output from Kibana/ES to files instead of to stdout `, @@ -40,7 +40,7 @@ export function parseFlags(flags: FlagsReader) { return { config: configs[0], - esFrom: flags.enum('esFrom', ['source', 'snapshot']), + esFrom: flags.enum('esFrom', ['source', 'snapshot', 'serverless']), esVersion: EsVersion.getDefault(), installDir: flags.string('kibana-install-dir'), logsDir: flags.boolean('logToFile') diff --git a/packages/kbn-test/src/kbn/index.ts b/packages/kbn-test/src/kbn/index.ts index cf0cac046a8fa..6f2c009ce7370 100644 --- a/packages/kbn-test/src/kbn/index.ts +++ b/packages/kbn-test/src/kbn/index.ts @@ -11,4 +11,5 @@ export { kibanaServerTestUser, adminTestUser, systemIndicesSuperuser, + kibanaTestSuperuserServerless, } from './users'; diff --git a/packages/kbn-test/src/kbn/users.ts b/packages/kbn-test/src/kbn/users.ts index b0db9e88ffc40..9a68a55beb6eb 100644 --- a/packages/kbn-test/src/kbn/users.ts +++ b/packages/kbn-test/src/kbn/users.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { SYSTEM_INDICES_SUPERUSER } from '@kbn/es'; +import { + SYSTEM_INDICES_SUPERUSER, + ELASTIC_SERVERLESS_SUPERUSER, + ELASTIC_SERVERLESS_SUPERUSER_PASSWORD, +} from '@kbn/es'; const env = process.env; @@ -32,3 +36,8 @@ export const systemIndicesSuperuser = { username: SYSTEM_INDICES_SUPERUSER, password: env.TEST_ES_PASS || 'changeme', }; + +export const kibanaTestSuperuserServerless = { + username: ELASTIC_SERVERLESS_SUPERUSER, + password: ELASTIC_SERVERLESS_SUPERUSER_PASSWORD, +}; diff --git a/packages/kbn-text-based-editor/index.ts b/packages/kbn-text-based-editor/index.ts index fd7b19839b87f..01f106af6639f 100644 --- a/packages/kbn-text-based-editor/index.ts +++ b/packages/kbn-text-based-editor/index.ts @@ -7,6 +7,7 @@ */ export type { TextBasedLanguagesEditorProps } from './src/text_based_languages_editor'; +export { fetchFieldsFromESQL } from './src/fetch_fields_from_esql'; import { TextBasedLanguagesEditor } from './src/text_based_languages_editor'; // React.lazy support diff --git a/packages/kbn-text-based-editor/src/__stories__/text_based_editor.stories.mdx b/packages/kbn-text-based-editor/src/__stories__/text_based_editor.stories.mdx index 0874305b1975b..f6e0a751e7755 100644 --- a/packages/kbn-text-based-editor/src/__stories__/text_based_editor.stories.mdx +++ b/packages/kbn-text-based-editor/src/__stories__/text_based_editor.stories.mdx @@ -31,7 +31,7 @@ The TextBasedLanguagesEditor component is a reusable component and can be used t name='compact mode' args={ { - query: { sql: 'SELECT field1, field2 FROM DATAVIEW' }, + query: { esql: 'from dataview | keep field1, field2' }, isCodeEditorExpanded:false, 'data-test-subj':'test-id' } @@ -51,7 +51,7 @@ When there are errors to the query the UI displays the errors to the editor: name='with errors' args={ { - query: { sql: 'SELECT field1, field2 FROM DATAVIEW' }, + query: { esql: 'from dataview | keep field1, field2' }, isCodeEditorExpanded:false, 'data-test-subj':'test-id', errors: [ @@ -76,7 +76,7 @@ When there the query is long and the editor is on the compact view: name='with long query' args={ { - query: { sql: 'SELECT field1, field2, field 3, field 4, field 5 FROM DATAVIEW WHERE field5 IS NOT NULL AND field4 IS NULL' }, + query: { esql: 'from dataview | keep field1, field2, field 3, field 4, field 5 | where field5 > 5 | stats var = avg(field3)' }, isCodeEditorExpanded:false, 'data-test-subj':'test-id', } @@ -97,7 +97,7 @@ The editor also works on the expanded mode: name='on expanded mode' args={ { - query: { sql: 'SELECT field1, field2 FROM DATAVIEW' }, + query: { esql: 'from dataview | keep field1, field2' }, isCodeEditorExpanded:true, 'data-test-subj':'test-id', } @@ -110,6 +110,27 @@ The editor also works on the expanded mode: +The editor also works on the expanded mode with the minimize button hidden: + + + + {Template.bind({})} + + + ## Component props The component exposes the following properties: diff --git a/packages/kbn-text-based-editor/src/editor_footer.tsx b/packages/kbn-text-based-editor/src/editor_footer.tsx index 3119d548098de..f89a14d06f106 100644 --- a/packages/kbn-text-based-editor/src/editor_footer.tsx +++ b/packages/kbn-text-based-editor/src/editor_footer.tsx @@ -28,22 +28,145 @@ import type { MonacoError } from './helpers'; const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; const COMMAND_KEY = isMac ? '⌘' : '^'; +const getConstsByType = (type: 'error' | 'warning', count: number) => { + if (type === 'error') { + return { + color: 'danger', + message: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.errorCount', { + defaultMessage: '{count} {count, plural, one {error} other {errors}}', + values: { count }, + }), + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.errorsTitle', { + defaultMessage: 'Errors', + }), + }; + } else { + return { + color: 'warning', + message: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.warningCount', { + defaultMessage: '{count} {count, plural, one {warning} other {warnings}}', + values: { count }, + }), + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.warningsTitle', { + defaultMessage: 'Warnings', + }), + }; + } +}; + +export function ErrorsWarningsPopover({ + isPopoverOpen, + items, + type, + refreshErrors, + setIsPopoverOpen, + onErrorClick, +}: { + isPopoverOpen: boolean; + items: MonacoError[]; + type: 'error' | 'warning'; + refreshErrors: () => void; + setIsPopoverOpen: (flag: boolean) => void; + onErrorClick: (error: MonacoError) => void; +}) { + const strings = getConstsByType(type, items.length); + return ( + + + + + + + { + refreshErrors(); + setIsPopoverOpen(!isPopoverOpen); + }} + > +

{strings.message}

+ + } + ownFocus={false} + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + > +
+ {strings.label} + + {items.map((item, index) => { + return ( + onErrorClick(item)} + > + + + + + + + + {i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.lineNumber', + { + defaultMessage: 'Line {lineNumber}', + values: { lineNumber: item.startLineNumber }, + } + )} + + + + + {item.message} + + + + ); + })} + +
+
+
+
+
+ ); +} + interface EditorFooterProps { lines: number; containerCSS: Interpolation; errors?: MonacoError[]; + warning?: MonacoError[]; detectTimestamp: boolean; onErrorClick: (error: MonacoError) => void; refreshErrors: () => void; + hideRunQueryText?: boolean; } export const EditorFooter = memo(function EditorFooter({ lines, containerCSS, errors, + warning, detectTimestamp, onErrorClick, refreshErrors, + hideRunQueryText, }: EditorFooterProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); return ( @@ -57,97 +180,24 @@ export const EditorFooter = memo(function EditorFooter({ {errors && errors.length > 0 && ( - - - - - - - { - refreshErrors(); - setIsPopoverOpen(!isPopoverOpen); - }} - > -

- {i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.errorCount', - { - defaultMessage: '{count} {count, plural, one {error} other {errors}}', - values: { count: errors.length }, - } - )} -

- - } - ownFocus={false} - isOpen={isPopoverOpen} - closePopover={() => setIsPopoverOpen(false)} - > -
- - {i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.errorsTitle', - { - defaultMessage: 'Errors', - } - )} - - - {errors.map((error, index) => { - return ( - onErrorClick(error)} - > - - - - - - - - {i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.lineNumber', - { - defaultMessage: 'Line {lineNumber}', - values: { lineNumber: error.startLineNumber }, - } - )} - - - - - {error.message} - - - - ); - })} - -
-
-
-
-
+ + )} + {warning && warning.length > 0 && ( + )} @@ -187,27 +237,29 @@ export const EditorFooter = memo(function EditorFooter({
- - - - -

- {i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.runQuery', { - defaultMessage: 'Run query', - })} -

-
-
- - {`${COMMAND_KEY} + Enter`} - -
-
+ {!hideRunQueryText && ( + + + + +

+ {i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.runQuery', { + defaultMessage: 'Run query', + })} +

+
+
+ + {`${COMMAND_KEY} + Enter`} + +
+
+ )} ); }); diff --git a/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx b/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx new file mode 100644 index 0000000000000..a3b4ef893a771 --- /dev/null +++ b/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx @@ -0,0 +1,2318 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { Markdown } from '@kbn/kibana-react-plugin/public'; + +export const initialSection = ( + +); + +export const sourceCommands = { + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.sourceCommands', { + defaultMessage: 'Source commands', + }), + description: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.commandsDescription', + { + defaultMessage: `A source command produces a table, typically with data from Elasticsearch. ES|QL supports the following source commands.`, + } + ), + items: [ + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from', + { + defaultMessage: 'FROM', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.row', + { + defaultMessage: 'ROW', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.show', + { + defaultMessage: 'SHOW', + } + ), + description: ( + \` 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. + `, + description: + 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', + } + )} + /> + ), + }, + ], +}; + +export const processingCommands = { + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.processingCommands', { + defaultMessage: 'Processing commands', + }), + description: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.processingCommandsDescription', + { + defaultMessage: `Processing commands change an input table by adding, removing, or changing rows and columns. ES|QL supports the following processing commands.`, + } + ), + items: [ + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect', + { + defaultMessage: 'DISSECT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.drop', + { + defaultMessage: 'DROP', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.enrich', + { + defaultMessage: 'ENRICH', + } + ), + description: ( + \`; if it’s not specified, the match will be performed on a field with the same name as the match field defined in the enrich policy. + +\`\`\` +ROW a = "1" +| ENRICH languages_policy ON a +\`\`\` + +You can specify which attributes (between those defined as enrich fields in the policy) have to be added to the result, using \`WITH , ...\` syntax. + +\`\`\` +ROW a = "1" +| ENRICH languages_policy ON a WITH language_name +\`\`\` + +Attributes can also be renamed using \`WITH new_name=\` + +\`\`\` +ROW a = "1" +| ENRICH languages_policy ON a WITH name = language_name +\`\`\` + +By default (if no \`WITH\` is defined), \`ENRICH\` will add all the enrich fields defined in the enrich policy to the result. + +In case of name collisions, the newly created fields will override the existing fields. + `, + description: + 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', + } + )} + /> + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.eval', + { + defaultMessage: 'EVAL', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.grok', + { + defaultMessage: 'GROK', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.keep', + { + defaultMessage: 'KEEP', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.limit', + { + defaultMessage: 'LIMIT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvExpand', + { + defaultMessage: 'MV_EXPAND', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rename', + { + defaultMessage: 'RENAME', + } + ), + description: ( + = +\`\`\` + +For example: + +\`\`\` +FROM employees +| KEEP first_name, last_name, still_hired +| RENAME employed = still_hired +\`\`\` + +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: + +\`\`\` +FROM employees +| KEEP first_name, last_name +| RENAME fn = first_name, ln = last_name +\`\`\` + `, + description: + 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', + } + )} + /> + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sort', + { + defaultMessage: 'SORT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.statsby', + { + defaultMessage: 'STATS ... BY', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where', + { + defaultMessage: 'WHERE', + } + ), + description: ( + \` +* larger than or equal: \`>=\` + +The \`IN\` operator allows testing whether a field or expression equals an element in a list of literals, fields or expressions: + +\`\`\` +ROW a = 1, b = 4, c = 3 +| WHERE c-a IN (3, b / 2, a) +\`\`\` + +For string comparison using wildcards or regular expressions, use \`LIKE\` or \`RLIKE\`: + +* Use \`LIKE\` to match strings using wildcards. The following wildcard characters are supported: + * \`*\` matches zero or more characters. + * \`?\` matches one character. + + \`\`\` + FROM employees + | WHERE first_name LIKE "?b*" + | KEEP first_name, last_name + \`\`\` + +* Use \`RLIKE\` to match strings using [regular expressions](https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html): + + \`\`\` + FROM employees + | WHERE first_name RLIKE ".leja.*" + | KEEP first_name, last_name + \`\`\` + +You can use the following boolean operators: + +* \`AND\` +* \`OR\` +* \`NOT\` + +\`\`\` +FROM employees +| KEEP first_name, last_name, height, still_hired +| WHERE height > 2 AND NOT still_hired +\`\`\` + +#### Functions +\`WHERE\` supports various functions for calculating values. Refer to Functions for more information. + `, + description: + 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', + } + )} + /> + ), + }, + ], +}; + +export const functions = { + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.functions', { + defaultMessage: 'Functions', + }), + description: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription', + { + defaultMessage: `Functions are supported by ROW, EVAL and WHERE.`, + } + ), + items: [ + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.absFunction', + { + defaultMessage: 'ABS', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.autoBucketFunction', + { + defaultMessage: 'AUTO_BUCKET', + } + ), + description: ( + = "1985-01-01T00:00:00Z" AND hire_date < "1986-01-01T00:00:00Z" +| EVAL bucket = AUTO_BUCKET(hire_date, 20, "1985-01-01T00:00:00Z", "1986-01-01T00:00:00Z") +| STATS AVG(salary) BY bucket +| SORT bucket +\`\`\` + +Returning: +\`\`\` +46305.0 | 1985-02-01T00:00:00.000Z +44817.0 | 1985-05-01T00:00:00.000Z +62405.0 | 1985-07-01T00:00:00.000Z +49095.0 | 1985-09-01T00:00:00.000Z +51532.0 | 1985-10-01T00:00:00.000Z +54539.75 | 1985-11-01T00:00:00.000 +\`\`\` + +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. + `, + description: + 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', + } + )} + /> + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.caseFunction', + { + defaultMessage: 'CASE', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cidrMatchFunction', + { + defaultMessage: 'CIDR_MATCH', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.concatFunction', + { + defaultMessage: 'CONCAT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dateFormatFunction', + { + defaultMessage: 'DATE_FORMAT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dateTruncFunction', + { + defaultMessage: 'DATE_TRUNC', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.eFunction', + { + defaultMessage: 'E', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.isFiniteFunction', + { + defaultMessage: 'IS_FINITE', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.isInfiniteFunction', + { + defaultMessage: 'IS_INFINITE', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.isNanFunction', + { + defaultMessage: 'IS_NAN', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.isNullFunction', + { + defaultMessage: 'IS_NULL', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.lengthFunction', + { + defaultMessage: 'LENGTH', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log10Function', + { + defaultMessage: 'LOG10', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvAvgFunction', + { + defaultMessage: 'MV_AVG', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvConcatFunction', + { + defaultMessage: 'MV_CONCAT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvCountFunction', + { + defaultMessage: 'MV_COUNT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvDedupeFunction', + { + defaultMessage: 'MV_DEDUPE', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvMaxFunction', + { + defaultMessage: 'MV_MAX', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvMedianFunction', + { + defaultMessage: 'MV_MEDIAN', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvMinFunction', + { + defaultMessage: 'MV_MIN', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvSumFunction', + { + defaultMessage: 'MV_SUM', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.piFunction', + { + defaultMessage: 'PI', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.powFunction', + { + defaultMessage: 'POW', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.roundFunction', + { + defaultMessage: 'ROUND', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.splitFunction', + { + defaultMessage: 'SPLIT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.startsWithFunction', + { + defaultMessage: 'STARTS_WITH', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.substringFunction', + { + defaultMessage: 'SUBSTRING', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tauFunction', + { + defaultMessage: 'TAU', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toBooleanFunction', + { + defaultMessage: 'TO_BOOLEAN', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toDatetimeFunction', + { + defaultMessage: 'TO_DATETIME', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toDoubleFunction', + { + defaultMessage: 'TO_DOUBLE', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toIntegerFunction', + { + defaultMessage: 'TO_INTEGER', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toIpFunction', + { + defaultMessage: 'TO_IP', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toLongFunction', + { + defaultMessage: 'TO_LONG', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toStringFunction', + { + defaultMessage: 'TO_STRING', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toVersionFunction', + { + defaultMessage: 'TO_VERSION', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trimFunction', + { + defaultMessage: 'TRIM', + } + ), + description: ( + + ), + }, + ], +}; + +export const aggregationFunctions = { + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctions', { + defaultMessage: 'Aggregation functions', + }), + description: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctionsDocumentationESQLDescription', + { + defaultMessage: `These functions can by used with STATS...BY:`, + } + ), + items: [ + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.avgFunction', + { + defaultMessage: 'AVG', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.countFunction', + { + defaultMessage: 'COUNT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.countDistinctFunction', + { + defaultMessage: 'COUNT_DISTINCT', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.maxFunction', + { + defaultMessage: 'MAX', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.medianFunction', + { + defaultMessage: 'MEDIAN', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.medianAbsoluteDeviationFunction', + { + defaultMessage: 'MEDIAN_ABSOLUTE_DEVIATION', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.minFunction', + { + defaultMessage: 'MIN', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.percentileFunction', + { + defaultMessage: 'PERCENTILE', + } + ), + description: ( + + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sumFunction', + { + defaultMessage: 'SUM', + } + ), + description: ( + + ), + }, + ], +}; diff --git a/packages/kbn-text-based-editor/src/fetch_fields_from_esql.ts b/packages/kbn-text-based-editor/src/fetch_fields_from_esql.ts new file mode 100644 index 0000000000000..7b9ebd66d76c6 --- /dev/null +++ b/packages/kbn-text-based-editor/src/fetch_fields_from_esql.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pluck } from 'rxjs/operators'; +import { lastValueFrom } from 'rxjs'; +import { Query, AggregateQuery, TimeRange } from '@kbn/es-query'; +import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; +import type { Datatable } from '@kbn/expressions-plugin/public'; +import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common'; + +interface TextBasedLanguagesErrorResponse { + error: { + message: string; + }; + type: 'error'; +} + +export function fetchFieldsFromESQL( + query: Query | AggregateQuery, + expressions: ExpressionsStart, + time?: TimeRange +) { + return textBasedQueryStateToAstWithValidation({ + query, + time, + }) + .then((ast) => { + if (ast) { + const execution = expressions.run(ast, null); + let finalData: Datatable; + let error: string | undefined; + execution.pipe(pluck('result')).subscribe((resp) => { + const response = resp as Datatable | TextBasedLanguagesErrorResponse; + if (response.type === 'error') { + error = response.error.message; + } else { + finalData = response; + } + }); + return lastValueFrom(execution).then(() => { + if (error) { + throw new Error(error); + } else { + return finalData; + } + }); + } + return undefined; + }) + .catch((err) => { + throw new Error(err.message); + }); +} diff --git a/packages/kbn-text-based-editor/src/helpers.test.ts b/packages/kbn-text-based-editor/src/helpers.test.ts index a56d0ecee141c..74c2387fde2fa 100644 --- a/packages/kbn-text-based-editor/src/helpers.test.ts +++ b/packages/kbn-text-based-editor/src/helpers.test.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { parseErrors, getInlineEditorText } from './helpers'; +import { parseErrors, parseWarning, getInlineEditorText } from './helpers'; describe('helpers', function () { describe('parseErrors', function () { - it('should return the correct error object from SQL ES response for an one liner query', function () { + it('should return the correct error object from ESQL ES response for an one liner query', function () { const error = new Error( '[essql] > Unexpected error from Elasticsearch: verification_exception - Found 1 problem\nline 1:8: Unknown column [miaou]' ); @@ -27,7 +27,7 @@ describe('helpers', function () { ]); }); - it('should return the correct error object from SQL ES response for an multi liner query', function () { + it('should return the correct error object from ESQL ES response for an multi liner query', function () { const error = new Error( '[essql] > Unexpected error from Elasticsearch: verification_exception - Found 1 problem line 3:7: Condition expression needs to be boolean, found [TEXT]' ); @@ -54,7 +54,7 @@ describe('helpers', function () { it('should return the generic error object for an error of unknown format', function () { const error = new Error('I am an unknown error'); const errors = [error]; - expect(parseErrors(errors, `SELECT * FROM "kibana_sample_data_ecommerce"`)).toEqual([ + expect(parseErrors(errors, `FROM "kibana_sample_data_ecommerce"`)).toEqual([ { endColumn: 10, endLineNumber: 1, @@ -67,29 +67,73 @@ describe('helpers', function () { }); }); + describe('parseWarning', function () { + it('should return the correct warning object from ESQL ES response for an one liner query', function () { + const warning = + '299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Line 1:52: evaluation of [date_parse(geo.dest)] failed, treating result as null. Only first 20 failures recorded."'; + expect(parseWarning(warning)).toEqual([ + { + endColumn: 138, + endLineNumber: 1, + message: + 'evaluation of [date_parse(geo.dest)] failed, treating result as null. Only first 20 failures recorded.', + severity: 8, + startColumn: 52, + startLineNumber: 1, + }, + ]); + }); + + it('should return the correct array of warnings if multiple warnins are detected', function () { + const warning = + '299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Line 1:52: evaluation of [date_parse(geo.dest)] failed, treating result as null. Only first 20 failures recorded.", 299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Line 1:84: evaluation of [date_parse(geo.src)] failed, treating result as null. Only first 20 failures recorded."'; + expect(parseWarning(warning)).toEqual([ + { + endColumn: 138, + endLineNumber: 1, + message: + 'evaluation of [date_parse(geo.dest)] failed, treating result as null. Only first 20 failures recorded.', + severity: 8, + startColumn: 52, + startLineNumber: 1, + }, + { + endColumn: 169, + endLineNumber: 1, + message: + 'evaluation of [date_parse(geo.src)] failed, treating result as null. Only first 20 failures recorded.', + severity: 8, + startColumn: 84, + startLineNumber: 1, + }, + ]); + }); + }); + describe('getInlineEditorText', function () { it('should return the entire query if it is one liner', function () { - const text = getInlineEditorText( - 'SELECT field1, count(*) FROM index1 ORDER BY field1', - false - ); + const text = getInlineEditorText('FROM index1 | keep field1, field2 | order field1', false); expect(text).toEqual(text); }); it('should return the query on one line with extra space if is multiliner', function () { const text = getInlineEditorText( - 'SELECT field1, count(*)\nFROM index1 ORDER BY field1', + 'FROM index1 | keep field1, field2\n| keep field1, field2 | order field1', true ); - expect(text).toEqual('SELECT field1, count(*) FROM index1 ORDER BY field1'); + expect(text).toEqual( + 'FROM index1 | keep field1, field2 | keep field1, field2 | order field1' + ); }); it('should return the query on one line with extra spaces removed if is multiliner', function () { const text = getInlineEditorText( - 'SELECT field1, count(*)\nFROM index1 \n ORDER BY field1', + 'FROM index1 | keep field1, field2\n| keep field1, field2 \n | order field1', true ); - expect(text).toEqual('SELECT field1, count(*) FROM index1 ORDER BY field1'); + expect(text).toEqual( + 'FROM index1 | keep field1, field2 | keep field1, field2 | order field1' + ); }); }); }); diff --git a/packages/kbn-text-based-editor/src/helpers.ts b/packages/kbn-text-based-editor/src/helpers.ts index d1deae5bf0d80..ca5e3d2fca663 100644 --- a/packages/kbn-text-based-editor/src/helpers.ts +++ b/packages/kbn-text-based-editor/src/helpers.ts @@ -42,6 +42,43 @@ export const useDebounceWithOptions = ( ); }; +export const parseWarning = (warning: string): MonacoError[] => { + if (warning.includes('Line')) { + const splitByLine = warning.split('Line'); + splitByLine.shift(); + return splitByLine.map((item) => { + const [lineNumber, startPosition, warningMessage] = item.split(':'); + const [trimmedMessage] = warningMessage.split('"'); + // initialize the length to 10 in case no error word found + let errorLength = 10; + const [_, wordWithError] = trimmedMessage.split('['); + if (wordWithError) { + errorLength = wordWithError.length - 1; + } + return { + message: trimmedMessage.trimStart(), + startColumn: Number(startPosition), + startLineNumber: Number(lineNumber), + endColumn: Number(startPosition) + errorLength, + endLineNumber: Number(lineNumber), + severity: monaco.MarkerSeverity.Error, + }; + }); + } else { + // unknown warning message + return [ + { + message: warning, + startColumn: 1, + startLineNumber: 1, + endColumn: 10, + endLineNumber: 1, + severity: monaco.MarkerSeverity.Error, + }, + ]; + } +}; + export const parseErrors = (errors: Error[], code: string): MonacoError[] => { return errors.map((error) => { if (error.message.includes('line')) { @@ -101,6 +138,21 @@ export const getDocumentationSections = async (language: string) => { initialSection, }; } + if (language === 'esql') { + const { sourceCommands, processingCommands, initialSection, functions, aggregationFunctions } = + await import('./esql_documentation_sections'); + groups.push({ + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.esql', { + defaultMessage: 'ES|QL', + }), + items: [], + }); + groups.push(sourceCommands, processingCommands, functions, aggregationFunctions); + return { + groups, + initialSection, + }; + } }; export const getInlineEditorText = (queryString: string, isMultiLine: boolean) => { diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.styles.ts b/packages/kbn-text-based-editor/src/text_based_languages_editor.styles.ts index 86a68b5ba342b..f7d57a6d60213 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.styles.ts +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.styles.ts @@ -18,7 +18,9 @@ export const textBasedLanguagedEditorStyles = ( editorHeight: number, isCodeEditorExpanded: boolean, hasErrors: boolean, - isCodeEditorExpandedFocused: boolean + hasWarning: boolean, + isCodeEditorExpandedFocused: boolean, + hasReference: boolean ) => { let position = isCompactFocused ? ('absolute' as 'absolute') : ('relative' as 'relative'); // cast string to type 'relative' | 'absolute' if (isCodeEditorExpanded) { @@ -40,7 +42,7 @@ export const textBasedLanguagedEditorStyles = ( }, resizableContainer: { display: 'flex', - width: isCodeEditorExpanded ? '100%' : 'calc(100% - 80px)', + width: isCodeEditorExpanded ? '100%' : `calc(100% - ${hasReference ? 80 : 40}px)`, alignItems: isCompactFocused ? 'flex-start' : 'center', border: !isCompactFocused ? euiTheme.border.thin : 'none', borderTopLeftRadius: '6px', @@ -51,7 +53,7 @@ export const textBasedLanguagedEditorStyles = ( linesBadge: { position: 'absolute' as 'absolute', // cast string to type 'absolute', zIndex: 1, - right: hasErrors ? '60px' : '12px', + right: hasErrors || hasWarning ? '60px' : '12px', top: '50%', transform: 'translate(0, -50%)', }, diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx index 90c973a609874..0be4c38eed749 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx @@ -16,6 +16,22 @@ import { TextBasedLanguagesEditor, TextBasedLanguagesEditorProps, } from './text_based_languages_editor'; +import { ReactWrapper } from 'enzyme'; + +jest.mock('./helpers', () => { + const module = jest.requireActual('./helpers'); + return { + ...module, + getDocumentationSections: () => ({ + groups: [ + { + label: 'How it works', + items: [], + }, + ], + }), + }; +}); import { of } from 'rxjs'; describe('TextBasedLanguagesEditor', () => { @@ -45,7 +61,7 @@ describe('TextBasedLanguagesEditor', () => { let props: TextBasedLanguagesEditorProps; beforeEach(() => { props = { - query: { sql: 'SELECT * FROM test' }, + query: { esql: 'from test' }, isCodeEditorExpanded: false, onTextLangQueryChange: jest.fn(), onTextLangQuerySubmit: jest.fn(), @@ -108,16 +124,32 @@ describe('TextBasedLanguagesEditor', () => { }); }); - it('should render the correct buttons for the inline code editor mode', async () => { + it('should render the warnings badge for the inline mode by default if warning are provides', async () => { + const newProps = { + ...props, + warning: 'Line 1: 20: Warning', + }; await act(async () => { - const component = mount(renderTextBasedLanguagesEditorComponent({ ...props })); - expect(component.find('[data-test-subj="TextBasedLangEditor-expand"]').length).not.toBe(0); + const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); expect( - component.find('[data-test-subj="TextBasedLangEditor-inline-documentation"]').length + component.find('[data-test-subj="TextBasedLangEditor-inline-warning-badge"]').length ).not.toBe(0); }); }); + it('should render the correct buttons for the inline code editor mode', async () => { + let component: ReactWrapper; + + await act(async () => { + component = mount(renderTextBasedLanguagesEditorComponent({ ...props })); + }); + component!.update(); + expect(component!.find('[data-test-subj="TextBasedLangEditor-expand"]').length).not.toBe(0); + expect( + component!.find('[data-test-subj="TextBasedLangEditor-inline-documentation"]').length + ).not.toBe(0); + }); + it('should call the expand editor function when expand button is clicked', async () => { const expandCodeEditorSpy = jest.fn(); const newProps = { @@ -136,12 +168,36 @@ describe('TextBasedLanguagesEditor', () => { ...props, isCodeEditorExpanded: true, }; + let component: ReactWrapper; + await act(async () => { + component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + }); + component!.update(); + expect( + component!.find('[data-test-subj="TextBasedLangEditor-toggleWordWrap"]').length + ).not.toBe(0); + expect(component!.find('[data-test-subj="TextBasedLangEditor-minimize"]').length).not.toBe(0); + expect(component!.find('[data-test-subj="TextBasedLangEditor-documentation"]').length).not.toBe( + 0 + ); + }); + + it('should not render the minimize button for the expanded code editor mode if the prop is set to true', async () => { + const newProps = { + ...props, + isCodeEditorExpanded: true, + hideMinimizeButton: true, + }; + let component: ReactWrapper; + await act(async () => { + component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + }); + component!.update(); await act(async () => { - const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); expect( component.find('[data-test-subj="TextBasedLangEditor-toggleWordWrap"]').length ).not.toBe(0); - expect(component.find('[data-test-subj="TextBasedLangEditor-minimize"]').length).not.toBe(0); + expect(component.find('[data-test-subj="TextBasedLangEditor-minimize"]').length).toBe(0); expect( component.find('[data-test-subj="TextBasedLangEditor-documentation"]').length ).not.toBe(0); @@ -186,4 +242,27 @@ describe('TextBasedLanguagesEditor', () => { ).toBe('1 line'); }); }); + + it('should render the run query text', async () => { + const newProps = { + ...props, + isCodeEditorExpanded: true, + }; + await act(async () => { + const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + expect(component.find('[data-test-subj="TextBasedLangEditor-run-query"]').length).not.toBe(0); + }); + }); + + it('should not render the run query text if the hideRunQueryText prop is set to true', async () => { + const newProps = { + ...props, + isCodeEditorExpanded: true, + hideRunQueryText: true, + }; + await act(async () => { + const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + expect(component.find('[data-test-subj="TextBasedLangEditor-run-query"]').length).toBe(0); + }); + }); }); diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx index 5f432c090eb8c..3aada71f81ab0 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx @@ -6,16 +6,27 @@ * Side Public License, v 1. */ -import React, { useRef, useEffect, useState, useCallback, memo } from 'react'; +import React, { useRef, memo, useEffect, useState, useCallback } from 'react'; import classNames from 'classnames'; -import { SQLLang, monaco } from '@kbn/monaco'; +import { + SQLLang, + monaco, + ESQL_LANG_ID, + ESQL_THEME_ID, + ESQLLang, + ESQLCustomAutocompleteCallbacks, +} from '@kbn/monaco'; import type { AggregateQuery } from '@kbn/es-query'; -import { getAggregateQueryMode } from '@kbn/es-query'; +import { getAggregateQueryMode, getLanguageDisplayName } from '@kbn/es-query'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; +import type { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public'; +import type { SerializedEnrichPolicy } from '@kbn/index-management-plugin/common'; import { type LanguageDocumentationSections, LanguageDocumentationPopover, } from '@kbn/language-documentation-popover'; - +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { i18n } from '@kbn/i18n'; import { EuiBadge, @@ -40,12 +51,14 @@ import { import { useDebounceWithOptions, parseErrors, + parseWarning, getInlineEditorText, getDocumentationSections, MonacoError, } from './helpers'; import { EditorFooter } from './editor_footer'; import { ResizableButton } from './resizable_button'; +import { fetchFieldsFromESQL } from './fetch_fields_from_esql'; import './overwrite.scss'; @@ -57,9 +70,18 @@ export interface TextBasedLanguagesEditorProps { isCodeEditorExpanded: boolean; detectTimestamp?: boolean; errors?: Error[]; + warning?: string; isDisabled?: boolean; isDarkMode?: boolean; dataTestSubj?: string; + hideMinimizeButton?: boolean; + hideRunQueryText?: boolean; +} + +interface TextBasedEditorDeps { + dataViews: DataViewsPublicPluginStart; + expressions: ExpressionsStart; + indexManagementApiService?: IndexManagementPluginSetup['apiService']; } const MAX_COMPACT_VIEW_LENGTH = 250; @@ -72,6 +94,9 @@ const KEYCODE_ARROW_DOWN = 40; const languageId = (language: string) => { switch (language) { + case 'esql': { + return ESQL_LANG_ID; + } case 'sql': default: { return SQLLang.ID; @@ -82,6 +107,8 @@ const languageId = (language: string) => { let clickedOutside = false; let initialRender = true; let updateLinesFromModel = false; +let currentCursorContent = ''; + export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ query, onTextLangQueryChange, @@ -90,13 +117,18 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ isCodeEditorExpanded, detectTimestamp = false, errors, + warning, isDisabled, isDarkMode, + hideMinimizeButton, + hideRunQueryText, dataTestSubj, }: TextBasedLanguagesEditorProps) { const { euiTheme } = useEuiTheme(); const language = getAggregateQueryMode(query); const queryString: string = query[language] ?? ''; + const kibana = useKibana(); + const { dataViews, expressions, indexManagementApiService } = kibana.services; const [lines, setLines] = useState(1); const [code, setCode] = useState(queryString ?? ''); const [codeOneLiner, setCodeOneLiner] = useState(''); @@ -108,16 +140,21 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ const [isCodeEditorExpandedFocused, setIsCodeEditorExpandedFocused] = useState(false); const [isWordWrapped, setIsWordWrapped] = useState(true); const [editorErrors, setEditorErrors] = useState([]); + const [editorWarning, setEditorWarning] = useState([]); + const [documentationSections, setDocumentationSections] = useState(); + const policiesRef = useRef([]); const styles = textBasedLanguagedEditorStyles( euiTheme, isCompactFocused, editorHeight, isCodeEditorExpanded, Boolean(errors?.length), - isCodeEditorExpandedFocused + Boolean(warning), + isCodeEditorExpandedFocused, + Boolean(documentationSections) ); const isDark = isDarkMode; const editorModel = useRef(); @@ -209,6 +246,18 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ if (updateLinesFromModel) { setLines(editorModel.current?.getLineCount() || 1); } + if (editor1?.current) { + const currentPosition = editor1.current?.getPosition(); + const content = editorModel.current?.getValueInRange({ + startLineNumber: 0, + startColumn: 0, + endLineNumber: currentPosition?.lineNumber ?? 1, + endColumn: currentPosition?.column ?? 1, + }); + if (content) { + currentCursorContent = content || editor1.current?.getValue(); + } + } }); editor1.current?.onDidFocusEditorText(() => { setIsCompactFocused(true); @@ -227,6 +276,12 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ if (!isCodeEditorExpanded) { editor1.current?.onDidContentSizeChange(updateHeight); } + if (warning && (!errors || !errors.length)) { + const parsedWarning = parseWarning(warning); + setEditorWarning(parsedWarning); + } else { + setEditorWarning([]); + } if (errors && errors.length) { const parsedErrors = parseErrors(errors, code); setEditorErrors(parsedErrors); @@ -238,7 +293,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ }, { skipFirstRender: false }, 256, - [errors] + [errors, warning] ); const onErrorClick = useCallback(({ startLineNumber, startColumn }: MonacoError) => { @@ -275,7 +330,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ const text = getInlineEditorText(queryString, Boolean(hasLines)); const queryLength = text.length; const unusedSpace = - errors && errors.length + (errors && errors.length) || warning ? EDITOR_ONE_LINER_UNUSED_SPACE_WITH_ERRORS : EDITOR_ONE_LINER_UNUSED_SPACE; const charactersAlowed = Math.floor((width - unusedSpace) / FONT_WIDTH); @@ -288,7 +343,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ } } }, - [queryString, errors, isCompactFocused] + [isCompactFocused, queryString, errors, warning] ); useEffect(() => { @@ -329,6 +384,78 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ } }, [language, documentationSections]); + const getSourceIdentifiers: ESQLCustomAutocompleteCallbacks['getSourceIdentifiers'] = + useCallback(async () => { + const indices = await dataViews.getIndices({ + showAllIndices: false, + pattern: '*', + isRollupIndex: () => false, + }); + return indices.map((i) => i.name); + }, [dataViews]); + + const getFieldsIdentifiers: ESQLCustomAutocompleteCallbacks['getFieldsIdentifiers'] = useCallback( + async (ctx) => { + const pipes = currentCursorContent?.split('|'); + pipes?.pop(); + const validContent = pipes?.join('|'); + if (validContent) { + // ES|QL with limit 0 returns only the columns and is more performant + const esqlQuery = { + esql: `${validContent} | limit 0`, + }; + try { + const table = await fetchFieldsFromESQL(esqlQuery, expressions); + return table?.columns.map((c) => c.name) || []; + } catch (e) { + // no action yet + } + } + return []; + }, + [expressions] + ); + + const getPoliciesIdentifiers: ESQLCustomAutocompleteCallbacks['getPoliciesIdentifiers'] = + useCallback( + async (ctx) => { + const { data: policies, error } = + (await indexManagementApiService?.getAllEnrichPolicies()) || {}; + policiesRef.current = policies || []; + if (error || !policies) { + return []; + } + return policies.map(({ name, sourceIndices }) => ({ name, indices: sourceIndices })); + }, + [indexManagementApiService] + ); + + const getPolicyFieldsIdentifiers: ESQLCustomAutocompleteCallbacks['getPolicyFieldsIdentifiers'] = + useCallback( + async (ctx) => + policiesRef.current + .filter(({ name }) => ctx.userDefinedVariables.policyIdentifiers.includes(name)) + .flatMap(({ enrichFields }) => enrichFields), + [] + ); + + const getPolicyMatchingFieldIdentifiers: ESQLCustomAutocompleteCallbacks['getPolicyMatchingFieldIdentifiers'] = + useCallback( + async (ctx) => { + // try to load the list if none is present yet but + // at least one policy is declared in the userDefinedVariables + // (this happens if the user pastes an ESQL statement with the policy name in it) + if (!policiesRef.current.length && ctx.userDefinedVariables.policyIdentifiers.length) { + await getPoliciesIdentifiers(ctx); + } + const matchingField = policiesRef.current.find(({ name }) => + ctx.userDefinedVariables.policyIdentifiers.includes(name) + )?.matchField; + return matchingField ? [matchingField] : []; + }, + [getPoliciesIdentifiers] + ); + const codeEditorOptions: CodeEditorProps['options'] = { automaticLayout: false, accessibilitySupport: 'off', @@ -343,7 +470,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ minimap: { enabled: false }, wordWrap: isWordWrapped ? 'on' : 'off', lineNumbers: showLineNumbers ? 'on' : 'off', - theme: isDark ? 'vs-dark' : 'vs', + theme: language === 'esql' ? ESQL_THEME_ID : isDark ? 'vs-dark' : 'vs', lineDecorationsWidth: 12, autoIndent: 'none', wrappingIndent: 'none', @@ -355,7 +482,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ vertical: 'auto', }, overviewRulerBorder: false, - readOnly: isDisabled, + readOnly: + isDisabled || Boolean(!isCompactFocused && codeOneLiner && codeOneLiner.includes('...')), }; if (isCompactFocused) { @@ -423,48 +551,54 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ - - - + { - expandCodeEditor(false); - updateLinesFromModel = false; - }} - /> - - + > + { + expandCodeEditor(false); + updateLinesFromModel = false; + }} + /> + + + )} - + {documentationSections && ( + + + + )} @@ -515,11 +649,33 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ {errors.length} )} + {!isCompactFocused && warning && (!errors || errors.length === 0) && ( + + {editorWarning.length} + + )} { editor1.current = editor; @@ -537,6 +693,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ lines={lines} containerCSS={styles.bottomContainer} errors={editorErrors} + warning={editorWarning} onErrorClick={onErrorClick} refreshErrors={onTextLangQuerySubmit} detectTimestamp={detectTimestamp} @@ -569,7 +726,14 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ onClick={() => expandCodeEditor(true)} data-test-subj="TextBasedLangEditor-expand" css={{ - borderRadius: 0, + ...(documentationSections + ? { + borderRadius: 0, + } + : { + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }), backgroundColor: isDark ? euiTheme.colors.lightestShade : '#e9edf3', border: '1px solid rgb(17 43 134 / 10%) !important', }} @@ -577,28 +741,34 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ - + + sections={documentationSections} + buttonProps={{ + display: 'empty', + 'data-test-subj': 'TextBasedLangEditor-inline-documentation', + 'aria-label': i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationLabel', + { + defaultMessage: 'Documentation', + } + ), + size: 'm', + css: { + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + backgroundColor: isDark ? euiTheme.colors.lightestShade : '#e9edf3', + border: '1px solid rgb(17 43 134 / 10%) !important', + borderLeft: 'transparent !important', + }, + }} + /> + + )} @@ -609,9 +779,11 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ lines={lines} containerCSS={styles.bottomContainer} errors={editorErrors} + warning={editorWarning} onErrorClick={onErrorClick} refreshErrors={onTextLangQuerySubmit} detectTimestamp={detectTimestamp} + hideRunQueryText={hideRunQueryText} /> )} {isCodeEditorExpanded && ( diff --git a/packages/kbn-text-based-editor/tsconfig.json b/packages/kbn-text-based-editor/tsconfig.json index cefbe0335575b..63222d0d6026b 100644 --- a/packages/kbn-text-based-editor/tsconfig.json +++ b/packages/kbn-text-based-editor/tsconfig.json @@ -19,7 +19,11 @@ "@kbn/core", "@kbn/kibana-react-plugin", "@kbn/language-documentation-popover", - "@kbn/test-jest-helpers" + "@kbn/test-jest-helpers", + "@kbn/data-plugin", + "@kbn/expressions-plugin", + "@kbn/data-views-plugin", + "@kbn/index-management-plugin" ], "exclude": [ "target/**/*", diff --git a/packages/kbn-unified-data-table/README.md b/packages/kbn-unified-data-table/README.md new file mode 100644 index 0000000000000..576a676289d7a --- /dev/null +++ b/packages/kbn-unified-data-table/README.md @@ -0,0 +1,241 @@ +# @kbn/unified-data-table + +This package contains components and services for the unified data table UI (as used in Discover). + +## UnifiedDataTable Component +Props description: +| Property | Type | Description | +| :--- | :--- | :--- | +| **ariaLabelledBy** | string | Determines which element labels the grid for ARIA. | +| **className** | (optional) string | Optional class name to apply. | +| **columns** | string[] | Determines ids of the columns which are displayed. | +| **expandedDoc** | (optional) DataTableRecord | If set, the given document is displayed in a flyout. | +| **dataView** | DataView | The used data view. | +| **loadingState** | DataLoadingState | Determines if data is currently loaded. | +| **onFilter** | DocViewFilterFn | Function to add a filter in the grid cell or document flyout. | +| **onResize** | (optional)(colSettings: { columnId: string; width: number }) => void; | Function triggered when a column is resized by the user. | +| **onSetColumns** | (columns: string[], hideTimeColumn: boolean) => void; | Function to set all columns. | +| **onSort** | (optional)(sort: string[][]) => void; | Function to change sorting of the documents, skipped when isSortEnabled is set to false. | +| **rows** | (optional)DataTableRecord[] | Array of documents provided by Elasticsearch. | +| **sampleSize** | number | The max size of the documents returned by Elasticsearch. | +| **setExpandedDoc** | (optional)(doc?: DataTableRecord) => void; | Function to set the expanded document, which is displayed in a flyout. | +| **settings** | (optional)UnifiedDataTableSettings | Grid display settings persisted in Elasticsearch (e.g. column width). | +| **searchDescription** | (optional)string | Search description. | +| **searchTitle** | (optional)string | Search title. | +| **showTimeCol** | boolean | Determines whether the time columns should be displayed (legacy settings). | +| **showFullScreenButton** | (optional)boolean | Determines whether the full screen button should be displayed. | +| **isSortEnabled** | (optional)boolean | Manage user sorting control. | +| **sort** | SortOrder[] | Current sort setting. | +| **useNewFieldsApi** | boolean | How the data is fetched. | +| **isPaginationEnabled** | (optional)boolean | Manage pagination control. | +| **controlColumnIds** | (optional)string[] | List of used control columns (available: 'openDetails', 'select'). | +| **rowHeightState** | (optional)number | Row height from state. | +| **onUpdateRowHeight** | (optional)(rowHeight: number) => void; | Update row height state. | +| **isPlainRecord** | (optional)boolean | Is text base lang mode enabled. | +| **rowsPerPageState** | (optional)number | Current state value for rowsPerPage. | +| **onUpdateRowsPerPage** | (optional)(rowsPerPage: number) => void; | Update rows per page state. | +| **onFieldEdited** | (optional)() => void; | Callback to execute on edit runtime field. | +| **cellActionsTriggerId** | (optional)string | Optional triggerId to retrieve the column cell actions that will override the default ones. | +| **services** | See Required **services** list below | Service dependencies. | +| **renderDocumentView** | (optional)(hit: DataTableRecord,displayedRows: DataTableRecord[],displayedColumns: string[]) => JSX.Element | undefined; | Callback to render DocumentView when the document is expanded. | +| **configRowHeight** | (optional)number | Optional value for providing configuration setting for UnifiedDataTable rows height. | +| **showMultiFields** | (optional)boolean | Optional value for providing configuration setting for enabling to display the complex fields in the table. Default is true. | +| **maxDocFieldsDisplayed** | (optional)number | Optional value for providing configuration setting for maximum number of document fields to display in the table. Default is 50. | +| **externalControlColumns** | (optional)EuiDataGridControlColumn[] | Optional value for providing EuiDataGridControlColumn list of the additional leading control columns. UnifiedDataTable includes two control columns: Open Details and Select. | +| **totalHits** | (optional)number | Number total hits from ES. | +| **onFetchMoreRecords** | (optional)() => void | To fetch more. | +| **externalAdditionalControls** | (optional)React.ReactNode | Optional value for providing the additional controls available in the UnifiedDataTable toolbar to manage it's records or state. UnifiedDataTable includes Columns, Sorting and Bulk Actions. | +| **rowsPerPageOptions** | (optional)number[] | Optional list of number type values to set custom UnifiedDataTable paging options to display the records per page. | +| **renderCustomGridBody** | (optional)(args: EuiDataGridCustomBodyProps) => React.ReactNode; | An optional function called to completely customize and control the rendering of EuiDataGrid's body and cell placement. | +| **trailingControlColumns** | (optional)EuiDataGridControlColumn[] | An optional list of the EuiDataGridControlColumn type for setting trailing control columns standard for EuiDataGrid. | +| **visibleCellActions** | (optional)number | An optional value for a custom number of the visible cell actions in the table. By default is up to 3. | +| **externalCustomRenderers** | (optional)Record React.ReactNode>; | An optional settings for a specified fields rendering like links. Applied only for the listed fields rendering. | +| **consumer** | (optional)string | Name of the UnifiedDataTable consumer component or application. | +| **componentsTourSteps** | (optional)Record | Optional key/value pairs to set guided onboarding steps ids for a data table components included to guided tour. | + +*Required **services** list: +``` + theme: ThemeServiceStart; + fieldFormats: FieldFormatsStart; + uiSettings: IUiSettingsClient; + dataViewFieldEditor: DataViewFieldEditorStart; + toastNotifications: ToastsStart; + storage: Storage; + data: DataPublicPluginStart; +``` + +Usage example: + +``` + // Memoize unified data table to avoid the unnecessary re-renderings + const DataTableMemoized = React.memo(UnifiedDataTable); + + // Add memoized component with all needed props + { + // Add logic to refetch the data when the filter by field was added/removed. Refetch data. + }} + onResize={(colSettings: { columnId: string; width: number }) => { + // Update the table state with the new width for the column + }} + onSetColumns={(columns: string[], hideTimeColumn: boolean) => { + // Update table state with the new columns. Refetch data. + }} + onSort={!isTextBasedQuery ? onSort : undefined + // Update table state with the new sorting settings. Refetch data. + } + rows={searchResultRows} + sampleSize={500} + setExpandedDoc={() => { + // Callback function to do the logic when the document is expanded + }} + settings={tableSettings} + showTimeCol={true} + isSortEnabled={true} + sort={sortingColumns} + rowHeightState={3} + onUpdateRowHeight={(rowHeight: number) => { + // Do the state update with the new setting of the row height + }} + isPlainRecord={isTextBasedQuery} + rowsPerPageState={50} + onUpdateRowsPerPage={(rowHeight: number) => { + // Do the state update with the new number of the rows per page + } + onFieldEdited={() => + // Callback to execute on edit runtime field. Refetch data. + } + cellActionsTriggerId={SecurityCellActionsTrigger.DEFAULT} + services={{ + theme, + fieldFormats, + storage, + toastNotifications: toastsService, + uiSettings, + dataViewFieldEditor, + data: dataPluginContract, + }} + visibleCellActions={3} + externalCustomRenderers={{ + // Set the record style definition for the specific fields rendering Record React.ReactNode> + }} + renderDocumentView={() => + // Implement similar callback to render the Document flyout + const renderDetailsPanel = useCallback( + () => ( + + ), + [browserFields, handleOnPanelClosed, runtimeMappings, timelineId] + ); + } + externalControlColumns={leadingControlColumns} + externalAdditionalControls={additionalControls} + trailingControlColumns={trailingControlColumns} + renderCustomGridBody={renderCustomGridBody} + rowsPerPageOptions={[10, 30, 40, 100]} + showFullScreenButton={false} + useNewFieldsApi={true} + maxDocFieldsDisplayed={50} + consumer="timeline" + totalHits={ + // total number of the documents in the search query result. For example: 1200 + } + onFetchMoreRecords={() => { + // Do some data fetch to get more data + }} + configRowHeight={3} + showMultiFields={true} + componentsTourSteps={'expandButton': DISCOVER_TOUR_STEP_ANCHOR_IDS.expandDocument} + /> +``` + +## JsonCodeEditorCommon Component +Props description: +| Property | Type | Description | +| :--- | :--- | :--- | +| **width** | (optional) string or number | Editor component width. | +| **height** | (optional) string or number | Editor component height. | +| **hasLineNumbers** | (optional) boolean | Define if the editor component has line numbers style. | +| **hideCopyButton** | (optional) boolean | Show/hide setting for Copy button. | +| **onEditorDidMount** | (editor: monaco.editor.IStandaloneCodeEditor) => void | Do some logic to update the state with the edotor component value. | + +Usage example: + +``` + setEditor(editorNode)} +/> + +``` + +## Utils + +* `getRowsPerPageOptions(currentRowsPerPage)` - gets list of the table defaults for perPage options. + +* `getDisplayedColumns(currentRowsPerPage)` - gets list of the table columns with the logic to define the empty list with _source column. + +* `popularizeField(...)` - helper function to define the dataView persistance and save indexPattern update capabilities. + +## Hooks + +* `useColumns(...)` - this hook define the state update for the columns event handlers and allows to use them for external components outside the UnifiedDataTable. + +An example of using hooks for defining event handlers for columns management with setting the consumer specific setAppState: + +``` +const { + columns: currentColumns, + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + } = useColumns({ + capabilities, + defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING), + dataView, + dataViews, + setAppState: stateContainer.appState.update, + useNewFieldsApi, + columns, + sort, + }); + +// Use onAddColumn, onRemoveColumn handlers in the DocumentView + +const renderDocumentView = useCallback( + (hit: DataTableRecord, displayedRows: DataTableRecord[], displayedColumns: string[]) => ( + setExpandedDoc(undefined)} + setExpandedDoc={setExpandedDoc} + query={query} + /> + ), + [dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc] + ); +``` \ No newline at end of file diff --git a/packages/kbn-unified-data-table/__mocks__/config.ts b/packages/kbn-unified-data-table/__mocks__/config.ts new file mode 100644 index 0000000000000..b8ae4a531038c --- /dev/null +++ b/packages/kbn-unified-data-table/__mocks__/config.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Config } from '@kbn/config'; + +export type ConfigMock = jest.Mocked; + +const createConfigMock = (): ConfigMock => ({ + has: jest.fn(), + get: jest.fn(), + set: jest.fn(), + getFlattenedPaths: jest.fn(), + toRaw: jest.fn(), +}); + +export const configMock = { + create: createConfigMock, +}; diff --git a/packages/kbn-unified-data-table/__mocks__/data_view_complex.ts b/packages/kbn-unified-data-table/__mocks__/data_view_complex.ts new file mode 100644 index 0000000000000..8122103c9f4d7 --- /dev/null +++ b/packages/kbn-unified-data-table/__mocks__/data_view_complex.ts @@ -0,0 +1,419 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataView } from '@kbn/data-views-plugin/public'; +import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__'; + +const fields = [ + { + count: 0, + name: '_id', + type: 'string', + esTypes: ['_id'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_index', + type: 'string', + esTypes: ['_index'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: '_score', + type: 'number', + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_source', + type: '_source', + esTypes: ['_source'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_type', + type: 'string', + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 2, + name: 'array_objects.description', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'array_objects.description.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'array_objects.description', + }, + }, + }, + { + count: 0, + name: 'array_objects.name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'array_objects.name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'array_objects.name', + }, + }, + }, + { + count: 0, + name: 'array_tags', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'array_tags.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'array_tags', + }, + }, + }, + { + count: 0, + name: 'binary_blob', + type: 'unknown', + esTypes: ['binary'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'bool_enabled', + type: 'boolean', + esTypes: ['boolean'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'date', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 1, + name: 'date_nanos', + type: 'date', + esTypes: ['date_nanos'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'flattened_labels', + type: 'unknown', + esTypes: ['flattened'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geo_point', + type: 'geo_point', + esTypes: ['geo_point'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 1, + name: 'geometry', + type: 'unknown', + esTypes: ['shape'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 1, + name: 'histogram', + type: 'histogram', + esTypes: ['histogram'], + scripted: false, + searchable: false, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'ip_addr', + type: 'ip', + esTypes: ['ip'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 4, + name: 'keyword_key', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'nested_user.first', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { + nested: { + path: 'nested_user', + }, + }, + }, + { + count: 0, + name: 'nested_user.first.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'nested_user.first', + }, + nested: { + path: 'nested_user', + }, + }, + }, + { + count: 0, + name: 'nested_user.last', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { + nested: { + path: 'nested_user', + }, + }, + }, + { + count: 0, + name: 'nested_user.last.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'nested_user.last', + }, + nested: { + path: 'nested_user', + }, + }, + }, + { + count: 3, + name: 'number_amount', + type: 'number', + esTypes: ['long'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 3, + name: 'number_price', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'object_user.first', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'object_user.last', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'range_time_frame', + type: 'date_range', + esTypes: ['date_range'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 1, + name: 'rank_features', + type: 'unknown', + esTypes: ['rank_features'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 5, + name: 'text_message', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'vector', + type: 'unknown', + esTypes: ['dense_vector'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'version', + type: 'string', + esTypes: ['version'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 1, + script: 'return "hi there"', + lang: 'painless', + name: 'scripted_string', + type: 'string', + scripted: true, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: 'runtime_number', + type: 'number', + esTypes: ['double'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, +] as DataView['fields']; + +export const dataViewComplexMock = buildDataViewMock({ + name: 'data-view-with-various-field-types', + fields, + timeFieldName: 'data', +}); diff --git a/packages/kbn-unified-data-table/__mocks__/data_view_with_timefield.ts b/packages/kbn-unified-data-table/__mocks__/data_view_with_timefield.ts new file mode 100644 index 0000000000000..374b6b23f837b --- /dev/null +++ b/packages/kbn-unified-data-table/__mocks__/data_view_with_timefield.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataView } from '@kbn/data-views-plugin/public'; +import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__'; + +const fields = [ + { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'timestamp', + displayName: 'timestamp', + type: 'date', + scripted: false, + filterable: true, + aggregatable: true, + sortable: true, + }, + { + name: 'message', + displayName: 'message', + type: 'string', + scripted: false, + filterable: false, + }, + { + name: 'extension', + displayName: 'extension', + type: 'string', + scripted: false, + filterable: true, + aggregatable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + scripted: false, + filterable: true, + aggregatable: true, + }, + { + name: 'scripted', + displayName: 'scripted', + type: 'number', + scripted: true, + filterable: false, + }, +] as DataView['fields']; + +export const dataViewWithTimefieldMock = buildDataViewMock({ + name: 'index-pattern-with-timefield', + fields, + timeFieldName: 'timestamp', +}); diff --git a/packages/kbn-unified-data-table/__mocks__/data_views.ts b/packages/kbn-unified-data-table/__mocks__/data_views.ts new file mode 100644 index 0000000000000..bdbf63ecfbfbe --- /dev/null +++ b/packages/kbn-unified-data-table/__mocks__/data_views.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; +import { dataViewComplexMock } from './data_view_complex'; +import { dataViewWithTimefieldMock } from './data_view_with_timefield'; + +export const dataViewMockList = [dataViewMock, dataViewComplexMock, dataViewWithTimefieldMock]; + +export function createDataViewsMock() { + return { + getCache: async () => { + return [dataViewMock]; + }, + get: async (id: string) => { + if (id === 'invalid-data-view-id') { + return Promise.reject('Invalid'); + } + const dataView = dataViewMockList.find((dv) => dv.id === id); + if (dataView) { + return Promise.resolve(dataView); + } else { + return Promise.reject(`DataView ${id} not found`); + } + }, + getDefaultDataView: jest.fn(() => dataViewMock), + updateSavedObject: jest.fn(), + getIdsWithTitle: jest.fn(() => { + return Promise.resolve(dataViewMockList); + }), + createFilter: jest.fn(), + create: jest.fn(), + clearInstanceCache: jest.fn(), + getFieldsForIndexPattern: jest.fn((dataView) => dataView.fields), + refreshFields: jest.fn(), + } as unknown as jest.Mocked; +} + +export const dataViewsMock = createDataViewsMock(); diff --git a/src/plugins/discover/public/__mocks__/es_hits_complex.ts b/packages/kbn-unified-data-table/__mocks__/es_hits_complex.ts similarity index 100% rename from src/plugins/discover/public/__mocks__/es_hits_complex.ts rename to packages/kbn-unified-data-table/__mocks__/es_hits_complex.ts diff --git a/packages/kbn-unified-data-table/__mocks__/external_control_columns.tsx b/packages/kbn-unified-data-table/__mocks__/external_control_columns.tsx new file mode 100644 index 0000000000000..d67afccc01559 --- /dev/null +++ b/packages/kbn-unified-data-table/__mocks__/external_control_columns.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiCheckbox, + EuiButtonIcon, + EuiPopover, + EuiFlexGroup, + EuiFlexItem, + EuiPopoverTitle, + EuiSpacer, + EuiDataGridControlColumn, +} from '@elastic/eui'; + +const SelectionHeaderCell = () => { + return ( +
+ null} /> +
+ ); +}; + +const SimpleHeaderCell = () => { + return ( +
+ {'Additional Actions'} +
+ ); +}; + +const SelectionRowCell = ({ rowIndex }: { rowIndex: number }) => { + return ( +
+ null} + /> +
+ ); +}; + +const TestTrailingColumn = () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + return ( + setIsPopoverOpen(!isPopoverOpen)} + /> + } + data-test-subj="test-trailing-column-popover-button" + closePopover={() => setIsPopoverOpen(false)} + > + {'Actions'} +
+ + + +
+
+ ); +}; + +export const testTrailingControlColumns = [ + { + id: 'actions', + width: 96, + headerCellRender: SimpleHeaderCell, + rowCellRender: TestTrailingColumn, + }, +]; + +export const testLeadingControlColumn: EuiDataGridControlColumn = { + id: 'test-leading-control', + headerCellRender: SelectionHeaderCell, + rowCellRender: SelectionRowCell, + width: 100, +}; diff --git a/packages/kbn-unified-data-table/__mocks__/local_storage_mock.ts b/packages/kbn-unified-data-table/__mocks__/local_storage_mock.ts new file mode 100644 index 0000000000000..42cd33d2eb699 --- /dev/null +++ b/packages/kbn-unified-data-table/__mocks__/local_storage_mock.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export class LocalStorageMock { + private store: Record; + constructor(defaultStore: Record) { + this.store = defaultStore; + } + clear() { + this.store = {}; + } + get(key: string) { + return this.store[key] || null; + } + set(key: string, value: unknown) { + this.store[key] = String(value); + } + remove(key: string) { + delete this.store[key]; + } +} diff --git a/packages/kbn-unified-data-table/__mocks__/services.ts b/packages/kbn-unified-data-table/__mocks__/services.ts new file mode 100644 index 0000000000000..2c74668644497 --- /dev/null +++ b/packages/kbn-unified-data-table/__mocks__/services.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { of } from 'rxjs'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; +import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks'; +import { chromeServiceMock, coreMock } from '@kbn/core/public/mocks'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; +import { IUiSettingsClient, ToastsStart } from '@kbn/core/public'; +import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; + +export function createServicesMock() { + const expressionsPlugin = expressionsPluginMock.createStartContract(); + + expressionsPlugin.run = jest.fn(() => + of({ + partial: false, + result: { + rows: [], + }, + }) + ) as unknown as typeof expressionsPlugin.run; + + const corePluginMock = coreMock.createStart(); + + const uiSettingsMock: Partial = { + get: jest.fn(), + isDefault: jest.fn((key: string) => { + return true; + }), + }; + + corePluginMock.uiSettings = { + ...corePluginMock.uiSettings, + ...uiSettingsMock, + }; + + const theme = { + theme$: of({ darkMode: false }), + }; + + corePluginMock.theme = theme; + + const dataPlugin = dataPluginMock.createStartContract(); + + return { + core: corePluginMock, + charts: chartPluginMock.createSetupContract(), + chrome: chromeServiceMock.createStartContract(), + history: () => ({ + location: { + search: '', + }, + listen: jest.fn(), + }), + fieldFormats: fieldFormatsMock, + filterManager: jest.fn(), + inspector: { + open: jest.fn(), + }, + uiActions: uiActionsPluginMock.createStartContract(), + uiSettings: uiSettingsMock as IUiSettingsClient, + http: { + basePath: '/', + }, + dataViewEditor: { + openEditor: jest.fn(), + userPermissions: { + editDataView: jest.fn(() => true), + }, + }, + dataViewFieldEditor: { + openEditor: jest.fn(), + userPermissions: { + editIndexPattern: jest.fn(() => true), + }, + } as unknown as DataViewFieldEditorStart, + theme, + storage: { + clear: jest.fn(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + }, + toastNotifications: { + addInfo: jest.fn(), + addWarning: jest.fn(), + addDanger: jest.fn(), + addSuccess: jest.fn(), + } as unknown as ToastsStart, + expressions: expressionsPlugin, + savedObjectsTagging: { + ui: { + getTagIdsFromReferences: jest.fn().mockResolvedValue([]), + updateTagsReferences: jest.fn(), + }, + }, + dataViews: jest.fn(), + locator: { + useUrl: jest.fn(() => ''), + navigate: jest.fn(), + getUrl: jest.fn(() => Promise.resolve('')), + }, + contextLocator: { getRedirectUrl: jest.fn(() => '') }, + singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, + data: dataPlugin, + }; +} + +export const servicesMock = createServicesMock(); diff --git a/src/plugins/discover/public/__mocks__/grid_context.ts b/packages/kbn-unified-data-table/__mocks__/table_context.ts similarity index 69% rename from src/plugins/discover/public/__mocks__/grid_context.ts rename to packages/kbn-unified-data-table/__mocks__/table_context.ts index 8949945c2b0d8..4a4a75b0fa9e5 100644 --- a/src/plugins/discover/public/__mocks__/grid_context.ts +++ b/packages/kbn-unified-data-table/__mocks__/table_context.ts @@ -10,13 +10,13 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import { dataViewMock, esHitsMock } from '@kbn/discover-utils/src/__mocks__'; import { dataViewComplexMock } from './data_view_complex'; import { esHitsComplex } from './es_hits_complex'; -import { discoverServiceMock } from './services'; -import { GridContext } from '../components/discover_grid/discover_grid_context'; -import { convertValueToString } from '../utils/convert_value_to_string'; +import { servicesMock } from './services'; +import { DataTableContext } from '../src/table_context'; +import { convertValueToString } from '../src/utils/convert_value_to_string'; import { buildDataTableRecord } from '@kbn/discover-utils'; import type { EsHitRecord } from '@kbn/discover-utils/types'; -const buildGridContext = (dataView: DataView, rows: EsHitRecord[]): GridContext => { +const buildTableContext = (dataView: DataView, rows: EsHitRecord[]): DataTableContext => { const usedRows = rows.map((row) => { return buildDataTableRecord(row, dataView); }); @@ -34,7 +34,7 @@ const buildGridContext = (dataView: DataView, rows: EsHitRecord[]): GridContext convertValueToString({ rowIndex, columnId, - fieldFormats: discoverServiceMock.fieldFormats, + fieldFormats: servicesMock.fieldFormats, rows: usedRows, dataView, options, @@ -42,6 +42,6 @@ const buildGridContext = (dataView: DataView, rows: EsHitRecord[]): GridContext }; }; -export const discoverGridContextMock = buildGridContext(dataViewMock, esHitsMock); +export const dataTableContextMock = buildTableContext(dataViewMock, esHitsMock); -export const discoverGridContextComplexMock = buildGridContext(dataViewComplexMock, esHitsComplex); +export const dataTableContextComplexMock = buildTableContext(dataViewComplexMock, esHitsComplex); diff --git a/packages/kbn-unified-data-table/index.ts b/packages/kbn-unified-data-table/index.ts new file mode 100644 index 0000000000000..2c5e995619436 --- /dev/null +++ b/packages/kbn-unified-data-table/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { UnifiedDataTable, DataLoadingState } from './src/components/data_table'; +export type { UnifiedDataTableProps } from './src/components/data_table'; +export { getDisplayedColumns } from './src/utils/columns'; + +export { JSONCodeEditorCommonMemoized } from './src/components/json_code_editor/json_code_editor_common'; + +export * from './src/types'; +export * as columnActions from './src/components/actions/columns'; + +export { getRowsPerPageOptions } from './src/utils/rows_per_page'; +export { popularizeField } from './src/utils/popularize_field'; + +export { useColumns } from './src/hooks/use_data_grid_columns'; diff --git a/packages/kbn-unified-data-table/jest.config.js b/packages/kbn-unified-data-table/jest.config.js new file mode 100644 index 0000000000000..5256221baacf4 --- /dev/null +++ b/packages/kbn-unified-data-table/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-unified-data-table'], +}; diff --git a/packages/kbn-unified-data-table/kibana.jsonc b/packages/kbn-unified-data-table/kibana.jsonc new file mode 100644 index 0000000000000..de49c4caff1e5 --- /dev/null +++ b/packages/kbn-unified-data-table/kibana.jsonc @@ -0,0 +1,6 @@ +{ + "type": "shared-common", + "id": "@kbn/unified-data-table", + "description": "Contains functionality for the unified data table which can be integrated into apps", + "owner": "@elastic/kibana-data-discovery" +} diff --git a/packages/kbn-unified-data-table/package.json b/packages/kbn-unified-data-table/package.json new file mode 100644 index 0000000000000..79d4157293c05 --- /dev/null +++ b/packages/kbn-unified-data-table/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/unified-data-table", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "sideEffects": [ + "*.css", + "*.scss" + ] +} diff --git a/src/plugins/discover/public/components/doc_table/actions/columns.test.ts b/packages/kbn-unified-data-table/src/components/actions/columns.test.ts similarity index 90% rename from src/plugins/discover/public/components/doc_table/actions/columns.test.ts rename to packages/kbn-unified-data-table/src/components/actions/columns.test.ts index c95ff0d8d7252..d8480cf2067b4 100644 --- a/src/plugins/discover/public/components/doc_table/actions/columns.test.ts +++ b/packages/kbn-unified-data-table/src/components/actions/columns.test.ts @@ -7,15 +7,13 @@ */ import { getStateColumnActions } from './columns'; -import { configMock } from '../../../__mocks__/config'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import { dataViewsMock } from '../../../__mocks__/data_views'; import { Capabilities } from '@kbn/core/types'; -import { DiscoverAppState } from '../../../application/main/services/discover_app_state_container'; +import { dataViewsMock } from '../../../__mocks__/data_views'; function getStateColumnAction( - state: DiscoverAppState, - setAppState: (state: Partial) => void + state: { columns?: string[]; sort?: string[][] }, + setAppState: (state: { columns: string[]; sort?: string[][] }) => void ) { return getStateColumnActions({ capabilities: { @@ -23,13 +21,13 @@ function getStateColumnAction( save: false, }, } as unknown as Capabilities, - config: configMock, dataView: dataViewMock, dataViews: dataViewsMock, useNewFieldsApi: true, setAppState, columns: state.columns, sort: state.sort, + defaultOrder: 'desc', }); } diff --git a/src/plugins/discover/public/components/doc_table/actions/columns.ts b/packages/kbn-unified-data-table/src/components/actions/columns.ts similarity index 83% rename from src/plugins/discover/public/components/doc_table/actions/columns.ts rename to packages/kbn-unified-data-table/src/components/actions/columns.ts index b45d95433165a..3355902ece86e 100644 --- a/src/plugins/discover/public/components/doc_table/actions/columns.ts +++ b/packages/kbn-unified-data-table/src/components/actions/columns.ts @@ -5,13 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { Capabilities, IUiSettingsClient } from '@kbn/core/public'; -import { DataViewsContract } from '@kbn/data-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils'; -import { DiscoverAppStateContainer } from '../../../application/main/services/discover_app_state_container'; -import { GetStateReturn as ContextGetStateReturn } from '../../../application/context/services/context_state'; -import { popularizeField } from '../../../utils/popularize_field'; +import { Capabilities } from '@kbn/core/public'; +import type { DataViewsContract } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { popularizeField } from '../../utils/popularize_field'; /** * Helper function to provide a fallback to a single _source column if the given array of columns @@ -57,27 +54,26 @@ export function moveColumn(columns: string[], columnName: string, newIndex: numb export function getStateColumnActions({ capabilities, - config, dataView, dataViews, useNewFieldsApi, setAppState, columns, sort, + defaultOrder, }: { capabilities: Capabilities; - config: IUiSettingsClient; dataView: DataView; dataViews: DataViewsContract; useNewFieldsApi: boolean; - setAppState: DiscoverAppStateContainer['update'] | ContextGetStateReturn['setAppState']; + setAppState: (state: { columns: string[]; sort?: string[][] }) => void; columns?: string[]; sort: string[][] | undefined; + defaultOrder: string; }) { function onAddColumn(columnName: string) { popularizeField(dataView, columnName, dataViews, capabilities); const nextColumns = addColumn(columns || [], columnName, useNewFieldsApi); - const defaultOrder = config.get(SORT_DEFAULT_ORDER_SETTING); const nextSort = columnName === '_score' && !sort?.length ? [['_score', defaultOrder]] : sort; setAppState({ columns: nextColumns, sort: nextSort }); } diff --git a/src/plugins/discover/public/components/discover_grid/build_copy_column_button.test.tsx b/packages/kbn-unified-data-table/src/components/build_copy_column_button.test.tsx similarity index 86% rename from src/plugins/discover/public/components/discover_grid/build_copy_column_button.test.tsx rename to packages/kbn-unified-data-table/src/components/build_copy_column_button.test.tsx index dda3a904bda3f..02a3c6e7e425e 100644 --- a/src/plugins/discover/public/components/discover_grid/build_copy_column_button.test.tsx +++ b/packages/kbn-unified-data-table/src/components/build_copy_column_button.test.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { discoverServiceMock } from '../../__mocks__/services'; -import { discoverGridContextMock } from '../../__mocks__/grid_context'; +import { servicesMock } from '../../__mocks__/services'; +import { dataTableContextMock } from '../../__mocks__/table_context'; import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button'; const execCommandMock = (global.document.execCommand = jest.fn()); @@ -20,7 +20,7 @@ describe('Build a column button to copy to clipboard', () => { it('should copy a column name to clipboard on click', () => { const { label, iconType, onClick } = buildCopyColumnNameButton({ columnDisplayName: 'test-field-name', - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, }); execCommandMock.mockImplementationOnce(() => true); @@ -49,9 +49,9 @@ describe('Build a column button to copy to clipboard', () => { const { label, iconType, onClick } = buildCopyColumnValuesButton({ columnId: 'extension', columnDisplayName: 'custom_extension', - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, rowsCount: 3, - valueToStringConverter: discoverGridContextMock.valueToStringConverter, + valueToStringConverter: dataTableContextMock.valueToStringConverter, }); const wrapper = mountWithIntl( @@ -72,8 +72,8 @@ describe('Build a column button to copy to clipboard', () => { } = buildCopyColumnValuesButton({ columnId: '_source', columnDisplayName: 'Document', - toastNotifications: discoverServiceMock.toastNotifications, - valueToStringConverter: discoverGridContextMock.valueToStringConverter, + toastNotifications: servicesMock.toastNotifications, + valueToStringConverter: dataTableContextMock.valueToStringConverter, rowsCount: 3, }); @@ -101,7 +101,7 @@ describe('Build a column button to copy to clipboard', () => { it('should not copy to clipboard on click', () => { const { label, iconType, onClick } = buildCopyColumnNameButton({ columnDisplayName: 'test-field-name', - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, }); execCommandMock.mockImplementationOnce(() => false); diff --git a/src/plugins/discover/public/components/discover_grid/build_copy_column_button.tsx b/packages/kbn-unified-data-table/src/components/build_copy_column_button.tsx similarity index 90% rename from src/plugins/discover/public/components/discover_grid/build_copy_column_button.tsx rename to packages/kbn-unified-data-table/src/components/build_copy_column_button.tsx index 111e80dd62e95..d1bfff1f1da41 100644 --- a/src/plugins/discover/public/components/discover_grid/build_copy_column_button.tsx +++ b/packages/kbn-unified-data-table/src/components/build_copy_column_button.tsx @@ -13,8 +13,8 @@ import type { ToastsStart } from '@kbn/core/public'; import { copyColumnValuesToClipboard, copyColumnNameToClipboard, -} from '../../utils/copy_value_to_clipboard'; -import type { ValueToStringConverter } from '../../types'; +} from '../utils/copy_value_to_clipboard'; +import type { ValueToStringConverter } from '../types'; function buildCopyColumnButton({ label, @@ -47,7 +47,7 @@ export function buildCopyColumnNameButton({ return buildCopyColumnButton({ label: ( ), @@ -72,7 +72,7 @@ export function buildCopyColumnValuesButton({ return buildCopyColumnButton({ label: ( ), diff --git a/src/plugins/discover/public/components/discover_grid/build_edit_field_button.test.tsx b/packages/kbn-unified-data-table/src/components/build_edit_field_button.test.tsx similarity index 81% rename from src/plugins/discover/public/components/discover_grid/build_edit_field_button.test.tsx rename to packages/kbn-unified-data-table/src/components/build_edit_field_button.test.tsx index c78918976e88d..a9d1a37ab6eda 100644 --- a/src/plugins/discover/public/components/discover_grid/build_edit_field_button.test.tsx +++ b/packages/kbn-unified-data-table/src/components/build_edit_field_button.test.tsx @@ -11,7 +11,7 @@ import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import { discoverServiceMock } from '../../__mocks__/services'; +import { servicesMock } from '../../__mocks__/services'; import { buildEditFieldButton } from './build_edit_field_button'; const dataView = buildDataViewMock({ @@ -50,8 +50,7 @@ describe('buildEditFieldButton', () => { it('should return null if the field is not editable', () => { const field = dataView.getFieldByName('unknown_field') as DataViewField; const button = buildEditFieldButton({ - hasEditDataViewPermission: () => - discoverServiceMock.dataViewEditor.userPermissions.editDataView(), + hasEditDataViewPermission: () => servicesMock.dataViewEditor.userPermissions.editDataView(), dataView, field, editField: jest.fn(), @@ -61,12 +60,11 @@ describe('buildEditFieldButton', () => { it('should return null if the data view is not editable', () => { jest - .spyOn(discoverServiceMock.dataViewEditor.userPermissions, 'editDataView') + .spyOn(servicesMock.dataViewEditor.userPermissions, 'editDataView') .mockReturnValueOnce(false); const field = dataView.getFieldByName('bytes') as DataViewField; const button = buildEditFieldButton({ - hasEditDataViewPermission: () => - discoverServiceMock.dataViewEditor.userPermissions.editDataView(), + hasEditDataViewPermission: () => servicesMock.dataViewEditor.userPermissions.editDataView(), dataView, field, editField: jest.fn(), @@ -77,8 +75,7 @@ describe('buildEditFieldButton', () => { it('should return null if passed the _source field', () => { const field = dataView.getFieldByName('_source') as DataViewField; const button = buildEditFieldButton({ - hasEditDataViewPermission: () => - discoverServiceMock.dataViewEditor.userPermissions.editDataView(), + hasEditDataViewPermission: () => servicesMock.dataViewEditor.userPermissions.editDataView(), dataView, field, editField: jest.fn(), @@ -89,8 +86,7 @@ describe('buildEditFieldButton', () => { it('should return EuiListGroupItemProps if the field and data view are editable', () => { const field = dataView.getFieldByName('bytes') as DataViewField; const button = buildEditFieldButton({ - hasEditDataViewPermission: () => - discoverServiceMock.dataViewEditor.userPermissions.editDataView(), + hasEditDataViewPermission: () => servicesMock.dataViewEditor.userPermissions.editDataView(), dataView, field, editField: jest.fn(), @@ -105,7 +101,7 @@ describe('buildEditFieldButton', () => { "iconType": "pencil", "label": , "onClick": [Function], @@ -118,8 +114,7 @@ describe('buildEditFieldButton', () => { const field = dataView.getFieldByName('bytes') as DataViewField; const editField = jest.fn(); const buttonProps = buildEditFieldButton({ - hasEditDataViewPermission: () => - discoverServiceMock.dataViewEditor.userPermissions.editDataView(), + hasEditDataViewPermission: () => servicesMock.dataViewEditor.userPermissions.editDataView(), dataView, field, editField, diff --git a/src/plugins/discover/public/components/discover_grid/build_edit_field_button.tsx b/packages/kbn-unified-data-table/src/components/build_edit_field_button.tsx similarity index 87% rename from src/plugins/discover/public/components/discover_grid/build_edit_field_button.tsx rename to packages/kbn-unified-data-table/src/components/build_edit_field_button.tsx index 71da6ca691f93..87d405f608c8c 100644 --- a/src/plugins/discover/public/components/discover_grid/build_edit_field_button.tsx +++ b/packages/kbn-unified-data-table/src/components/build_edit_field_button.tsx @@ -10,7 +10,7 @@ import { EuiListGroupItemProps } from '@elastic/eui'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; -import { getFieldCapabilities } from '../../utils/get_field_capabilities'; +import { getFieldCapabilities } from '../utils/get_field_capabilities'; export const buildEditFieldButton = ({ hasEditDataViewPermission, @@ -37,7 +37,10 @@ export const buildEditFieldButton = ({ const editFieldButton: EuiListGroupItemProps = { size: 'xs', label: ( - + ), iconType: 'pencil', iconProps: { size: 'm' }, diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.scss b/packages/kbn-unified-data-table/src/components/data_table.scss similarity index 78% rename from src/plugins/discover/public/components/discover_grid/discover_grid.scss rename to packages/kbn-unified-data-table/src/components/data_table.scss index 0e870d366b609..8b0f8719a450f 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.scss +++ b/packages/kbn-unified-data-table/src/components/data_table.scss @@ -1,4 +1,4 @@ -.dscDiscoverGrid { +.unifiedDataTable { width: 100%; max-width: 100%; height: 100%; @@ -19,8 +19,8 @@ border-right: none; } - .dscDiscoverGrid__table .euiDataGridRowCell:first-of-type, - .dscDiscoverGrid__table .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type { + .unifiedDataTable__table .euiDataGridRowCell:first-of-type, + .unifiedDataTable__table .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type { border-left: none; border-right: none; } @@ -31,11 +31,11 @@ } } -.dscDiscoverGrid__cellValue { +.unifiedDataTable__cellValue { font-family: $euiCodeFontFamily; } -.dscDiscoverGrid__cellPopover { +.unifiedDataTable__cellPopover { // Fixes https://github.com/elastic/kibana/issues/145216 in Chrome .lines-content.monaco-editor-background { overflow: unset !important; @@ -43,7 +43,7 @@ } } -.dscDiscoverGrid__cellPopoverValue { +.unifiedDataTable__cellPopoverValue { font-family: $euiCodeFontFamily; font-size: $euiFontSizeS; } @@ -52,24 +52,32 @@ white-space: pre-wrap; } -.dscDiscoverGrid__inner { +.unifiedDataTable__inner { display: flex; flex-direction: column; flex-wrap: nowrap; height: 100%; } -.dscDiscoverGrid__table { +.unifiedDataTable__table { flex-grow: 1; flex-shrink: 1; min-height: 0; } -.dscTable__flyoutHeader { +.unifiedDataTable__footer { + flex-shrink: 0; + background-color: $euiColorLightShade; + padding: $euiSize / 2 $euiSize; + margin-top: $euiSize / 4; + text-align: center; +} + +.unifiedDataTable__flyoutHeader { white-space: nowrap; } -.dscTable__flyoutDocumentNavigation { +.unifiedDataTable__flyoutDocumentNavigation { justify-content: flex-end; } @@ -106,11 +114,11 @@ width: 100%; } -.dscFormatSource { +.unifiedDataTableFormatSource { @include euiTextTruncate; } -.dscDiscoverGrid__descriptionListDescription { +.unifiedDataTable__descriptionListDescription { word-break: break-all; white-space: normal; @@ -132,7 +140,7 @@ @include euiBreakpoint('xs', 's', 'm') { // EUI issue to hide 'of' text https://github.com/elastic/eui/issues/4654 - .dscTable__flyoutDocumentNavigation .euiPagination__compressedText { + .unifiedDataTable__flyoutDocumentNavigation .euiPagination__compressedText { display: none; } } diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.test.tsx b/packages/kbn-unified-data-table/src/components/data_table.test.tsx similarity index 61% rename from src/plugins/discover/public/components/discover_grid/discover_grid.test.tsx rename to packages/kbn-unified-data-table/src/components/data_table.test.tsx index 153126b4d471c..7ca0888230749 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.test.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.test.tsx @@ -7,16 +7,23 @@ */ import React from 'react'; import { ReactWrapper } from 'enzyme'; -import { EuiCopy } from '@elastic/eui'; +import { + EuiButton, + EuiCopy, + EuiDataGridCellValueElementProps, + EuiDataGridCustomBodyProps, +} from '@elastic/eui'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { act } from 'react-dom/test-utils'; import { findTestSubject } from '@elastic/eui/lib/test'; import { buildDataViewMock, deepMockedFields, esHitsMock } from '@kbn/discover-utils/src/__mocks__'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { DiscoverGrid, DiscoverGridProps, DataLoadingState } from './discover_grid'; +import { DataLoadingState, UnifiedDataTable, UnifiedDataTableProps } from './data_table'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { discoverServiceMock } from '../../__mocks__/services'; +import { servicesMock } from '../../__mocks__/services'; import { buildDataTableRecord, getDocId } from '@kbn/discover-utils'; -import type { EsHitRecord } from '@kbn/discover-utils/types'; +import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types'; +import { testLeadingControlColumn } from '../../__mocks__/external_control_columns'; const mockUseDataGridColumnsCellActions = jest.fn((prop: unknown) => []); jest.mock('@kbn/cell-actions', () => ({ @@ -30,8 +37,8 @@ export const dataViewMock = buildDataViewMock({ timeFieldName: '@timestamp', }); -function getProps() { - const services = discoverServiceMock; +function getProps(): UnifiedDataTableProps { + const services = servicesMock; services.dataViewFieldEditor.userPermissions.editIndexPattern = jest.fn().mockReturnValue(true); return { @@ -40,9 +47,7 @@ function getProps() { dataView: dataViewMock, loadingState: DataLoadingState.loaded, expandedDoc: undefined, - onAddColumn: jest.fn(), onFilter: jest.fn(), - onRemoveColumn: jest.fn(), onResize: jest.fn(), onSetColumns: jest.fn(), onSort: jest.fn(), @@ -55,14 +60,22 @@ function getProps() { showTimeCol: true, sort: [], useNewFieldsApi: true, - services, + services: { + fieldFormats: services.fieldFormats, + uiSettings: services.uiSettings, + dataViewFieldEditor: services.dataViewFieldEditor, + toastNotifications: services.toastNotifications, + storage: services.storage as unknown as Storage, + data: services.data, + theme: services.theme, + }, }; } -async function getComponent(props: DiscoverGridProps = getProps()) { - const Proxy = (innerProps: DiscoverGridProps) => ( - - +async function getComponent(props: UnifiedDataTableProps = getProps()) { + const Proxy = (innerProps: UnifiedDataTableProps) => ( + + ); @@ -74,7 +87,7 @@ async function getComponent(props: DiscoverGridProps = getProps()) { return component; } -function getSelectedDocNr(component: ReactWrapper) { +function getSelectedDocNr(component: ReactWrapper) { const gridSelectionBtn = findTestSubject(component, 'dscGridSelectionBtn'); if (!gridSelectionBtn.length) { return 0; @@ -83,7 +96,7 @@ function getSelectedDocNr(component: ReactWrapper) { return Number(selectedNr); } -function getDisplayedDocNr(component: ReactWrapper) { +function getDisplayedDocNr(component: ReactWrapper) { const gridSelectionBtn = findTestSubject(component, 'discoverDocTable'); if (!gridSelectionBtn.length) { return 0; @@ -93,7 +106,7 @@ function getDisplayedDocNr(component: ReactWrapper) { } async function toggleDocSelection( - component: ReactWrapper, + component: ReactWrapper, document: EsHitRecord ) { act(() => { @@ -103,13 +116,13 @@ async function toggleDocSelection( component.update(); } -describe('DiscoverGrid', () => { +describe('UnifiedDataTable', () => { afterEach(async () => { jest.clearAllMocks(); }); describe('Document selection', () => { - let component: ReactWrapper; + let component: ReactWrapper; beforeEach(async () => { component = await getComponent(); }); @@ -287,4 +300,136 @@ describe('DiscoverGrid', () => { `); }); }); + + describe('externalControlColumns', () => { + it('should render external leading control columns', async () => { + const component = await getComponent({ + ...getProps(), + expandedDoc: { + id: 'test', + raw: { + _index: 'test_i', + _id: 'test', + }, + flattened: { test: jest.fn() }, + }, + setExpandedDoc: jest.fn(), + renderDocumentView: jest.fn(), + externalControlColumns: [testLeadingControlColumn], + }); + + expect(findTestSubject(component, 'docTableExpandToggleColumn').exists()).toBeTruthy(); + expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy(); + }); + }); + + it('should render provided in renderDocumentView DocumentView on expand clicked', async () => { + const component = await getComponent({ + ...getProps(), + expandedDoc: { + id: 'test', + raw: { + _index: 'test_i', + _id: 'test', + }, + flattened: { test: jest.fn() }, + }, + setExpandedDoc: jest.fn(), + renderDocumentView: ( + hit: DataTableRecord, + displayedRows: DataTableRecord[], + displayedColumns: string[] + ) =>
{hit.id}
, + externalControlColumns: [testLeadingControlColumn], + }); + + findTestSubject(component, 'docTableExpandToggleColumn').first().simulate('click'); + expect(findTestSubject(component, 'test-document-view').exists()).toBeTruthy(); + }); + + describe('externalAdditionalControls', () => { + it('should render external additional toolbar controls', async () => { + const component = await getComponent({ + ...getProps(), + columns: ['message'], + externalAdditionalControls: , + }); + + expect(findTestSubject(component, 'test-additional-control').exists()).toBeTruthy(); + expect(findTestSubject(component, 'dataGridColumnSelectorButton').exists()).toBeTruthy(); + }); + }); + + describe('externalCustomRenderers', () => { + it('should render only host column with the custom renderer, message should be rendered with the default cell renderer', async () => { + const component = await getComponent({ + ...getProps(), + columns: ['message', 'host'], + externalCustomRenderers: { + host: (props: EuiDataGridCellValueElementProps) => ( +
{props.columnId}
+ ), + }, + }); + + expect(findTestSubject(component, 'test-renderer-host').exists()).toBeTruthy(); + expect(findTestSubject(component, 'test-renderer-message').exists()).toBeFalsy(); + }); + }); + + describe('renderCustomGridBody', () => { + it('should render custom grid body for each row', async () => { + const component = await getComponent({ + ...getProps(), + columns: ['message', 'host'], + trailingControlColumns: [ + { + id: 'row-details', + + // The header cell should be visually hidden, but available to screen readers + width: 0, + headerCellRender: () => <>, + headerCellProps: { className: 'euiScreenReaderOnly' }, + + // The footer cell can be hidden to both visual & SR users, as it does not contain meaningful information + footerCellProps: { style: { display: 'none' } }, + + // When rendering this custom cell, we'll want to override + // the automatic width/heights calculated by EuiDataGrid + rowCellRender: jest.fn(), + }, + ], + renderCustomGridBody: (props: EuiDataGridCustomBodyProps) => ( +
+ +
+ ), + }); + + expect(findTestSubject(component, 'test-renderer-custom-grid-body').exists()).toBeTruthy(); + }); + }); + + describe('componentsTourSteps', () => { + it('should render tour step for the first row of leading control column expandButton', async () => { + const component = await getComponent({ + ...getProps(), + expandedDoc: { + id: 'test', + raw: { + _index: 'test_i', + _id: 'test', + }, + flattened: { test: jest.fn() }, + }, + setExpandedDoc: jest.fn(), + renderDocumentView: jest.fn(), + componentsTourSteps: { expandButton: 'test-expand' }, + }); + + const gridExpandBtn = findTestSubject(component, 'docTableExpandToggleColumn').first(); + const tourStep = gridExpandBtn.getDOMNode().getAttribute('id'); + expect(tourStep).toEqual('test-expand'); + }); + }); }); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/packages/kbn-unified-data-table/src/components/data_table.tsx similarity index 64% rename from src/plugins/discover/public/components/discover_grid/discover_grid.tsx rename to packages/kbn-unified-data-table/src/components/data_table.tsx index c9ef3119ae421..e5f5e5dbbba39 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.tsx @@ -11,7 +11,8 @@ import classnames from 'classnames'; import { FormattedMessage } from '@kbn/i18n-react'; import { of } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; -import './discover_grid.scss'; +import './data_table.scss'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import { EuiDataGridSorting, EuiDataGrid, @@ -23,44 +24,42 @@ import { EuiIcon, EuiDataGridRefProps, EuiDataGridInMemory, + EuiDataGridControlColumn, + EuiDataGridCustomBodyProps, + EuiDataGridCellValueElementProps, } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; -import type { SortOrder } from '@kbn/saved-search-plugin/public'; import { useDataGridColumnsCellActions, type UseDataGridColumnsCellActionsProps, } from '@kbn/cell-actions'; -import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { ToastsStart, IUiSettingsClient, HttpStart, CoreStart } from '@kbn/core/public'; -import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; -import { Serializable } from '@kbn/utility-types'; +import type { ToastsStart, IUiSettingsClient } from '@kbn/core/public'; +import type { Serializable } from '@kbn/utility-types'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { getShouldShowFieldHandler } from '@kbn/discover-utils'; +import { getShouldShowFieldHandler, DOC_HIDE_TIME_COLUMN_SETTING } from '@kbn/discover-utils'; +import type { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { ThemeServiceStart } from '@kbn/react-kibana-context-common'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { UnifiedDataTableSettings, ValueToStringConverter } from '../types'; +import { getDisplayedColumns } from '../utils/columns'; +import { convertValueToString } from '../utils/convert_value_to_string'; +import { getRowsPerPageOptions } from '../utils/rows_per_page'; +import { getRenderCellValueFn } from '../utils/get_render_cell_value'; +import { getEuiGridColumns, getLeadControlColumns, getVisibleColumns } from './data_table_columns'; +import { UnifiedDataTableContext } from '../table_context'; +import { getSchemaDetectors } from './data_table_schema'; +import { DataTableDocumentToolbarBtn } from './data_table_document_selection'; +import { useRowHeightsOptions } from '../hooks/use_row_heights_options'; import { - DOC_HIDE_TIME_COLUMN_SETTING, - MAX_DOC_FIELDS_DISPLAYED, - SHOW_MULTIFIELDS, -} from '@kbn/discover-utils'; -import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; -import { getSchemaDetectors } from './discover_grid_schema'; -import { DiscoverGridFlyout } from './discover_grid_flyout'; -import { DiscoverGridContext } from './discover_grid_context'; -import { getRenderCellValueFn } from './get_render_cell_value'; -import { DiscoverGridSettings } from './types'; -import { - getEuiGridColumns, - getLeadControlColumns, - getVisibleColumns, -} from './discover_grid_columns'; -import { GRID_STYLE, toolbarVisibility as toolbarVisibilityDefaults } from './constants'; -import { getDisplayedColumns } from '../../utils/columns'; -import { DiscoverGridDocumentToolbarBtn } from './discover_grid_document_selection'; -import { DiscoverGridFooter } from './discover_grid_footer'; -import type { ValueToStringConverter } from '../../types'; -import { useRowHeightsOptions } from '../../hooks/use_row_heights_options'; -import { convertValueToString } from '../../utils/convert_value_to_string'; -import { getRowsPerPageOptions, getDefaultRowsPerPage } from '../../utils/rows_per_page'; + DEFAULT_ROWS_PER_PAGE, + GRID_STYLE, + toolbarVisibility as toolbarVisibilityDefaults, +} from '../constants'; +import { UnifiedDataTableFooter } from './data_table_footer'; + +export type SortOrder = [string, string]; export enum DataLoadingState { loading = 'loading', @@ -75,7 +74,7 @@ interface SortObj { direction: string; } -export interface DiscoverGridProps { +export interface UnifiedDataTableProps { /** * Determines which element labels the grid for ARIA */ @@ -85,7 +84,7 @@ export interface DiscoverGridProps { */ className?: string; /** - * Determines which columns are displayed + * Determines ids of the columns which are displayed */ columns: string[]; /** @@ -100,19 +99,10 @@ export interface DiscoverGridProps { * Determines if data is currently loaded */ loadingState: DataLoadingState; - /** - * Function used to add a column in the document flyout - */ - onAddColumn: (column: string) => void; /** * Function to add a filter in the grid cell or document flyout */ onFilter: DocViewFilterFn; - /** - * Function used in the grid header and flyout to remove a column - * @param column - */ - onRemoveColumn: (column: string) => void; /** * Function triggered when a column is resized by the user */ @@ -140,13 +130,13 @@ export interface DiscoverGridProps { /** * Grid display settings persisted in Elasticsearch (e.g. column width) */ - settings?: DiscoverGridSettings; + settings?: UnifiedDataTableSettings; /** - * Saved search description + * Search description */ searchDescription?: string; /** - * Saved search title + * Search title */ searchTitle?: string; /** @@ -201,22 +191,6 @@ export interface DiscoverGridProps { * Callback to execute on edit runtime field */ onFieldEdited?: () => void; - /** - * Filters applied by saved search embeddable - */ - filters?: Filter[]; - /** - * Query applied by KQL bar or text based editor - */ - query?: Query | AggregateQuery; - /** - * Saved search id used for links to single doc and surrounding docs in the flyout - */ - savedSearchId?: string; - /** - * Document detail view component - */ - DocumentView?: typeof DiscoverGridFlyout; /** * Optional triggerId to retrieve the column cell actions that will override the default ones */ @@ -225,13 +199,38 @@ export interface DiscoverGridProps { * Service dependencies */ services: { - core: CoreStart; + theme: ThemeServiceStart; fieldFormats: FieldFormatsStart; - addBasePath: HttpStart['basePath']['prepend']; uiSettings: IUiSettingsClient; dataViewFieldEditor: DataViewFieldEditorStart; toastNotifications: ToastsStart; + storage: Storage; + data: DataPublicPluginStart; }; + /** + * Callback to render DocumentView when the document is expanded + */ + renderDocumentView?: ( + hit: DataTableRecord, + displayedRows: DataTableRecord[], + displayedColumns: string[] + ) => JSX.Element | undefined; + /** + * Optional value for providing configuration setting for UnifiedDataTable rows height + */ + configRowHeight?: number; + /** + * Optional value for providing configuration setting for enabling to display the complex fields in the table. Default is true. + */ + showMultiFields?: boolean; + /** + * Optional value for providing configuration setting for maximum number of document fields to display in the table. Default is 50. + */ + maxDocFieldsDisplayed?: number; + /** + * Optional value for providing EuiDataGridControlColumn list of the additional leading control columns. UnifiedDataTable includes two control columns: Open Details and Select. + */ + externalControlColumns?: EuiDataGridControlColumn[]; /** * Number total hits from ES */ @@ -240,24 +239,62 @@ export interface DiscoverGridProps { * To fetch more */ onFetchMoreRecords?: () => void; + /** + * Optional value for providing the additional controls available in the UnifiedDataTable toolbar to manage it's records or state. UnifiedDataTable includes Columns, Sorting and Bulk Actions. + */ + externalAdditionalControls?: React.ReactNode; + /** + * Optional list of number type values to set custom UnifiedDataTable paging options to display the records per page. + */ + rowsPerPageOptions?: number[]; + /** + * An optional function called to completely customize and control the rendering of + * EuiDataGrid's body and cell placement. This can be used to, e.g. remove EuiDataGrid's + * virtualization library, or roll your own. + * + * This component is **only** meant as an escape hatch for extremely custom use cases. + * + * Behind the scenes, this function is treated as a React component, + * allowing hooks, context, and other React concepts to be used. + * It receives #EuiDataGridCustomBodyProps as its only argument. + */ + renderCustomGridBody?: (args: EuiDataGridCustomBodyProps) => React.ReactNode; + /** + * An optional list of the EuiDataGridControlColumn type for setting trailing control columns standard for EuiDataGrid. + */ + trailingControlColumns?: EuiDataGridControlColumn[]; + /** + * An optional value for a custom number of the visible cell actions in the table. By default is up to 3. + **/ + visibleCellActions?: number; + /** + * An optional settings for a specified fields rendering like links. Applied only for the listed fields rendering. + */ + externalCustomRenderers?: Record< + string, + (props: EuiDataGridCellValueElementProps) => React.ReactNode + >; + /** + * Name of the UnifiedDataTable consumer component or application + */ + consumer?: string; + /** + * Optional key/value pairs to set guided onboarding steps ids for a data table components included to guided tour. + */ + componentsTourSteps?: Record; } export const EuiDataGridMemoized = React.memo(EuiDataGrid); const CONTROL_COLUMN_IDS_DEFAULT = ['openDetails', 'select']; -export const DiscoverGrid = ({ +export const UnifiedDataTable = ({ ariaLabelledBy, columns, + controlColumnIds = CONTROL_COLUMN_IDS_DEFAULT, dataView, loadingState, - expandedDoc, - onAddColumn, - filters, - query, - savedSearchId, onFilter, - onRemoveColumn, onResize, onSetColumns, onSort, @@ -265,7 +302,6 @@ export const DiscoverGrid = ({ sampleSize, searchDescription, searchTitle, - setExpandedDoc, settings, showTimeCol, showFullScreenButton = true, @@ -273,7 +309,6 @@ export const DiscoverGrid = ({ useNewFieldsApi, isSortEnabled = true, isPaginationEnabled = true, - controlColumnIds = CONTROL_COLUMN_IDS_DEFAULT, cellActionsTriggerId, className, rowHeightState, @@ -282,13 +317,28 @@ export const DiscoverGrid = ({ rowsPerPageState, onUpdateRowsPerPage, onFieldEdited, - DocumentView, services, + renderCustomGridBody, + trailingControlColumns, totalHits, onFetchMoreRecords, -}: DiscoverGridProps) => { - const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings } = services; - const { darkMode } = useObservable(services.core.theme?.theme$ ?? of(themeDefault), themeDefault); + renderDocumentView, + setExpandedDoc, + expandedDoc, + configRowHeight, + showMultiFields = true, + maxDocFieldsDisplayed = 50, + externalControlColumns, + externalAdditionalControls, + rowsPerPageOptions, + visibleCellActions, + externalCustomRenderers, + consumer = 'discover', + componentsTourSteps, +}: UnifiedDataTableProps) => { + const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } = + services; + const { darkMode } = useObservable(services.theme?.theme$ ?? of(themeDefault), themeDefault); const dataGridRef = useRef(null); const [selectedDocs, setSelectedDocs] = useState([]); const [isFilterActive, setIsFilterActive] = useState(false); @@ -300,7 +350,7 @@ export const DiscoverGrid = ({ } const idMap = rows.reduce((map, row) => map.set(row.id, true), new Map()); // filter out selected docs that are no longer part of the current data - const result = selectedDocs.filter((docId) => idMap.get(docId)); + const result = selectedDocs.filter((docId) => !!idMap.get(docId)); if (result.length === 0 && isFilterActive) { setIsFilterActive(false); } @@ -336,17 +386,45 @@ export const DiscoverGrid = ({ [displayedRows, dataView, fieldFormats] ); + const unifiedDataTableContextValue = useMemo( + () => ({ + expanded: expandedDoc, + setExpanded: setExpandedDoc, + rows: displayedRows, + onFilter, + dataView, + isDarkMode: darkMode, + selectedDocs: usedSelectedDocs, + setSelectedDocs: (newSelectedDocs: React.SetStateAction) => { + setSelectedDocs(newSelectedDocs); + if (isFilterActive && newSelectedDocs.length === 0) { + setIsFilterActive(false); + } + }, + valueToStringConverter, + componentsTourSteps, + }), + [ + componentsTourSteps, + darkMode, + dataView, + displayedRows, + expandedDoc, + isFilterActive, + onFilter, + setExpandedDoc, + usedSelectedDocs, + valueToStringConverter, + ] + ); + /** * Pagination */ - const defaultRowsPerPage = useMemo( - () => getDefaultRowsPerPage(services.uiSettings), - [services.uiSettings] - ); const currentPageSize = typeof rowsPerPageState === 'number' && rowsPerPageState > 0 ? rowsPerPageState - : defaultRowsPerPage; + : DEFAULT_ROWS_PER_PAGE; const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: currentPageSize, @@ -371,10 +449,17 @@ export const DiscoverGrid = ({ onChangePage, pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, pageSize: pagination.pageSize, - pageSizeOptions: getRowsPerPageOptions(pagination.pageSize), + pageSizeOptions: rowsPerPageOptions ?? getRowsPerPageOptions(pagination.pageSize), } : undefined; - }, [pagination, pageCount, isPaginationEnabled, onUpdateRowsPerPage]); + }, [ + isPaginationEnabled, + pagination.pageIndex, + pagination.pageSize, + pageCount, + rowsPerPageOptions, + onUpdateRowsPerPage, + ]); useEffect(() => { setPagination((paginationData) => @@ -403,8 +488,6 @@ export const DiscoverGrid = ({ [onSort, isSortEnabled, isPlainRecord, setInmemorySortingColumns] ); - const showMultiFields = services.uiSettings.get(SHOW_MULTIFIELDS); - const shouldShowFieldHandler = useMemo(() => { const dataViewFields = dataView.fields.getAll().map((fld) => fld.name); return getShouldShowFieldHandler(dataViewFields, dataView, showMultiFields); @@ -420,10 +503,20 @@ export const DiscoverGrid = ({ displayedRows, useNewFieldsApi, shouldShowFieldHandler, - services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED), - () => dataGridRef.current?.closeCellPopover() + () => dataGridRef.current?.closeCellPopover(), + services.fieldFormats, + maxDocFieldsDisplayed, + externalCustomRenderers ), - [dataView, displayedRows, useNewFieldsApi, shouldShowFieldHandler, services.uiSettings] + [ + dataView, + displayedRows, + useNewFieldsApi, + shouldShowFieldHandler, + maxDocFieldsDisplayed, + services.fieldFormats, + externalCustomRenderers, + ] ); /** @@ -459,7 +552,7 @@ export const DiscoverGrid = ({ ); const visibleColumns = useMemo( - () => getVisibleColumns(displayedColumns, dataView, showTimeCol) as string[], + () => getVisibleColumns(displayedColumns, dataView, showTimeCol), [dataView, displayedColumns, showTimeCol] ); @@ -511,6 +604,7 @@ export const DiscoverGrid = ({ valueToStringConverter, onFilter, editField, + visibleCellActions, }), [ onFilter, @@ -527,6 +621,7 @@ export const DiscoverGrid = ({ dataViewFieldEditor, valueToStringConverter, editField, + visibleCellActions, ] ); @@ -554,26 +649,33 @@ export const DiscoverGrid = ({ return { columns: sortingColumns, onSort: () => {} }; }, [isSortEnabled, sortingColumns, isPlainRecord, inmemorySortingColumns, onTableSort]); - const canSetExpandedDoc = Boolean(setExpandedDoc && DocumentView); + const canSetExpandedDoc = Boolean(setExpandedDoc && !!renderDocumentView); - const lead = useMemo( - () => - getLeadControlColumns(canSetExpandedDoc).filter(({ id }) => controlColumnIds.includes(id)), - [controlColumnIds, canSetExpandedDoc] - ); + const leadingControlColumns = useMemo(() => { + const internalControlColumns = getLeadControlColumns(canSetExpandedDoc).filter(({ id }) => + controlColumnIds.includes(id) + ); + return externalControlColumns + ? [...internalControlColumns, ...externalControlColumns] + : internalControlColumns; + }, [canSetExpandedDoc, externalControlColumns, controlColumnIds]); const additionalControls = useMemo( - () => - usedSelectedDocs.length ? ( - - ) : null, - [usedSelectedDocs, isFilterActive, rows, setIsFilterActive] + () => ( + <> + {usedSelectedDocs.length ? ( + + ) : null} + {externalAdditionalControls} + + ), + [usedSelectedDocs, isFilterActive, rows, externalAdditionalControls] ); const showDisplaySelector = useMemo( @@ -588,8 +690,10 @@ export const DiscoverGrid = ({ ); const inMemory = useMemo(() => { - return isPlainRecord ? ({ level: 'sorting' } as EuiDataGridInMemory) : undefined; - }, [isPlainRecord]); + return isPlainRecord && columns.length + ? ({ level: 'sorting' } as EuiDataGridInMemory) + : undefined; + }, [columns.length, isPlainRecord]); const toolbarVisibility = useMemo( () => @@ -615,17 +719,20 @@ export const DiscoverGrid = ({ const rowHeightsOptions = useRowHeightsOptions({ rowHeightState, onUpdateRowHeight, + storage, + configRowHeight, + consumer, }); const isRenderComplete = loadingState !== DataLoadingState.loading; if (!rowCount && loadingState === DataLoadingState.loading) { return ( -
+
- +
); @@ -644,32 +751,18 @@ export const DiscoverGrid = ({ - +
); } return ( - { - setSelectedDocs(newSelectedDocs); - if (isFilterActive && newSelectedDocs.length === 0) { - setIsFilterActive(false); - } - }, - valueToStringConverter, - }} - > - + +
{loadingState !== DataLoadingState.loading && isPaginationEnabled && ( // we hide the footer for Surrounding Documents page - )} {searchTitle && ( @@ -716,13 +813,13 @@ export const DiscoverGrid = ({

{searchDescription ? ( ) : ( @@ -730,24 +827,10 @@ export const DiscoverGrid = ({

)} - {setExpandedDoc && expandedDoc && DocumentView && ( - setExpandedDoc(undefined)} - setExpandedDoc={setExpandedDoc} - query={query} - /> - )} + {canSetExpandedDoc && + expandedDoc && + renderDocumentView!(expandedDoc, displayedRows, displayedColumns)}
-
+ ); }; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx b/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx similarity index 86% rename from src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx rename to packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx index cea7f6c3505c9..01246603643fd 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx @@ -7,10 +7,10 @@ */ import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import { getEuiGridColumns, getVisibleColumns } from './discover_grid_columns'; +import { getEuiGridColumns, getVisibleColumns } from './data_table_columns'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; -import { discoverGridContextMock } from '../../__mocks__/grid_context'; -import { discoverServiceMock } from '../../__mocks__/services'; +import { dataTableContextMock } from '../../__mocks__/table_context'; +import { servicesMock } from '../../__mocks__/services'; const columns = ['extension', 'message']; const columnsWithTimeCol = getVisibleColumns( @@ -19,7 +19,7 @@ const columnsWithTimeCol = getVisibleColumns( true ) as string[]; -describe('Discover grid columns', function () { +describe('Data table columns', function () { describe('getEuiGridColumns', () => { it('returns eui grid columns showing default columns', async () => { const actual = getEuiGridColumns({ @@ -29,14 +29,14 @@ describe('Discover grid columns', function () { defaultColumns: true, isSortEnabled: true, isPlainRecord: false, - valueToStringConverter: discoverGridContextMock.valueToStringConverter, + valueToStringConverter: dataTableContextMock.valueToStringConverter, rowsCount: 100, services: { - uiSettings: discoverServiceMock.uiSettings, - toastNotifications: discoverServiceMock.toastNotifications, + uiSettings: servicesMock.uiSettings, + toastNotifications: servicesMock.toastNotifications, }, hasEditDataViewPermission: () => - discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(), + servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(), onFilter: () => {}, }); expect(actual).toMatchInlineSnapshot(` @@ -52,7 +52,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -66,7 +66,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -86,6 +86,7 @@ describe('Discover grid columns', function () { "id": "extension", "isSortable": false, "schema": "string", + "visibleCellActions": undefined, }, Object { "actions": Object { @@ -98,7 +99,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -112,7 +113,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -130,6 +131,7 @@ describe('Discover grid columns', function () { "id": "message", "isSortable": false, "schema": "string", + "visibleCellActions": undefined, }, ] `); @@ -143,14 +145,14 @@ describe('Discover grid columns', function () { defaultColumns: false, isSortEnabled: true, isPlainRecord: false, - valueToStringConverter: discoverGridContextMock.valueToStringConverter, + valueToStringConverter: dataTableContextMock.valueToStringConverter, rowsCount: 100, services: { - uiSettings: discoverServiceMock.uiSettings, - toastNotifications: discoverServiceMock.toastNotifications, + uiSettings: servicesMock.uiSettings, + toastNotifications: servicesMock.toastNotifications, }, hasEditDataViewPermission: () => - discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(), + servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(), onFilter: () => {}, }); expect(actual).toMatchInlineSnapshot(` @@ -166,7 +168,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -180,7 +182,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -219,6 +221,7 @@ describe('Discover grid columns', function () { "initialWidth": 210, "isSortable": true, "schema": "datetime", + "visibleCellActions": undefined, }, Object { "actions": Object { @@ -231,7 +234,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -245,7 +248,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -268,6 +271,7 @@ describe('Discover grid columns', function () { "id": "extension", "isSortable": false, "schema": "string", + "visibleCellActions": undefined, }, Object { "actions": Object { @@ -280,7 +284,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -294,7 +298,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -315,6 +319,7 @@ describe('Discover grid columns', function () { "id": "message", "isSortable": false, "schema": "string", + "visibleCellActions": undefined, }, ] `); @@ -328,14 +333,14 @@ describe('Discover grid columns', function () { defaultColumns: false, isSortEnabled: true, isPlainRecord: true, - valueToStringConverter: discoverGridContextMock.valueToStringConverter, + valueToStringConverter: dataTableContextMock.valueToStringConverter, rowsCount: 100, services: { - uiSettings: discoverServiceMock.uiSettings, - toastNotifications: discoverServiceMock.toastNotifications, + uiSettings: servicesMock.uiSettings, + toastNotifications: servicesMock.toastNotifications, }, hasEditDataViewPermission: () => - discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(), + servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(), onFilter: () => {}, }); expect(actual).toMatchInlineSnapshot(` @@ -351,7 +356,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -365,7 +370,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -404,6 +409,7 @@ describe('Discover grid columns', function () { "initialWidth": 210, "isSortable": true, "schema": "datetime", + "visibleCellActions": undefined, }, Object { "actions": Object { @@ -416,7 +422,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -430,7 +436,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -453,6 +459,7 @@ describe('Discover grid columns', function () { "id": "extension", "isSortable": true, "schema": "string", + "visibleCellActions": undefined, }, Object { "actions": Object { @@ -465,7 +472,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -479,7 +486,7 @@ describe('Discover grid columns', function () { "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -500,6 +507,7 @@ describe('Discover grid columns', function () { "id": "message", "isSortable": true, "schema": "string", + "visibleCellActions": undefined, }, ] `); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx similarity index 82% rename from src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx rename to packages/kbn-unified-data-table/src/components/data_table_columns.tsx index a93b8a9ff1464..4b66f2a2bd6cf 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx @@ -17,14 +17,14 @@ import { } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; import { ToastsStart, IUiSettingsClient } from '@kbn/core/public'; -import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; -import { ExpandButton } from './discover_grid_expand_button'; -import { DiscoverGridSettings } from './types'; -import type { ValueToStringConverter } from '../../types'; -import { buildCellActions } from './discover_grid_cell_actions'; -import { getSchemaByKbnType } from './discover_grid_schema'; -import { SelectButton } from './discover_grid_document_selection'; -import { defaultTimeColumnWidth } from './constants'; +import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { ExpandButton } from './data_table_expand_button'; +import { UnifiedDataTableSettings } from '../types'; +import type { ValueToStringConverter } from '../types'; +import { buildCellActions } from './default_cell_actions'; +import { getSchemaByKbnType } from './data_table_schema'; +import { SelectButton } from './data_table_document_selection'; +import { defaultTimeColumnWidth } from '../constants'; import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button'; import { buildEditFieldButton } from './build_edit_field_button'; @@ -34,7 +34,7 @@ const openDetails = { headerCellRender: () => ( - {i18n.translate('discover.controlColumnHeader', { + {i18n.translate('unifiedDataTable.controlColumnHeader', { defaultMessage: 'Control column', })} @@ -50,7 +50,7 @@ const select = { headerCellRender: () => ( - {i18n.translate('discover.selectColumnHeader', { + {i18n.translate('unifiedDataTable.selectColumnHeader', { defaultMessage: 'Select column', })} @@ -79,6 +79,7 @@ function buildEuiGridColumn({ onFilter, editField, columnCellActions, + visibleCellActions, }: { columnName: string; columnWidth: number | undefined; @@ -93,6 +94,7 @@ function buildEuiGridColumn({ onFilter?: DocViewFilterFn; editField?: (fieldName: string) => void; columnCellActions?: EuiDataGridColumnCellAction[]; + visibleCellActions?: number; }) { const dataViewField = dataView.getFieldByName(columnName); const editFieldButton = @@ -101,16 +103,19 @@ function buildEuiGridColumn({ buildEditFieldButton({ hasEditDataViewPermission, dataView, field: dataViewField, editField }); const columnDisplayName = columnName === '_source' - ? i18n.translate('discover.grid.documentHeader', { + ? i18n.translate('unifiedDataTable.grid.documentHeader', { defaultMessage: 'Document', }) : dataViewField?.displayName || columnName; let cellActions: EuiDataGridColumnCellAction[]; + if (columnCellActions?.length) { cellActions = columnCellActions; } else { - cellActions = dataViewField ? buildCellActions(dataViewField, onFilter) : []; + cellActions = dataViewField + ? buildCellActions(dataViewField, toastNotifications, valueToStringConverter, onFilter) + : []; } const column: EuiDataGridColumn = { @@ -123,7 +128,7 @@ function buildEuiGridColumn({ defaultColumns || columnName === dataView.timeFieldName ? false : { - label: i18n.translate('discover.removeColumnLabel', { + label: i18n.translate('unifiedDataTable.removeColumnLabel', { defaultMessage: 'Remove column', }), iconType: 'cross', @@ -150,23 +155,21 @@ function buildEuiGridColumn({ ], }, cellActions, + visibleCellActions, }; if (column.id === dataView.timeFieldName) { const timeFieldName = dataViewField?.customLabel ?? dataView.timeFieldName; const primaryTimeAriaLabel = i18n.translate( - 'discover.docTable.tableHeader.timeFieldIconTooltipAriaLabel', + 'unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel', { defaultMessage: '{timeFieldName} - this field represents the time that events occurred.', values: { timeFieldName }, } ); - const primaryTimeTooltip = i18n.translate( - 'discover.docTable.tableHeader.timeFieldIconTooltip', - { - defaultMessage: 'This field represents the time that events occurred.', - } - ); + const primaryTimeTooltip = i18n.translate('unifiedDataTable.tableHeader.timeFieldIconTooltip', { + defaultMessage: 'This field represents the time that events occurred.', + }); column.display = (
@@ -199,11 +202,12 @@ export function getEuiGridColumns({ valueToStringConverter, onFilter, editField, + visibleCellActions, }: { columns: string[]; columnsCellActions?: EuiDataGridColumnCellAction[][]; rowsCount: number; - settings: DiscoverGridSettings | undefined; + settings: UnifiedDataTableSettings | undefined; dataView: DataView; defaultColumns: boolean; isSortEnabled: boolean; @@ -216,6 +220,7 @@ export function getEuiGridColumns({ valueToStringConverter: ValueToStringConverter; onFilter: DocViewFilterFn; editField?: (fieldName: string) => void; + visibleCellActions?: number; }) { const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; @@ -234,6 +239,7 @@ export function getEuiGridColumns({ rowsCount, onFilter, editField, + visibleCellActions, }) ); } diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx b/packages/kbn-unified-data-table/src/components/data_table_document_selection.test.tsx similarity index 79% rename from src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx rename to packages/kbn-unified-data-table/src/components/data_table_document_selection.test.tsx index 7fda5b74ce550..ca0d422948416 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_document_selection.test.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { DiscoverGridDocumentToolbarBtn, SelectButton } from './discover_grid_document_selection'; -import { discoverGridContextMock } from '../../__mocks__/grid_context'; -import { DiscoverGridContext } from './discover_grid_context'; +import { DataTableDocumentToolbarBtn, SelectButton } from './data_table_document_selection'; +import { dataTableContextMock } from '../../__mocks__/table_context'; +import { UnifiedDataTableContext } from '../table_context'; import { getDocId } from '@kbn/discover-utils'; describe('document selection', () => { @@ -35,11 +35,11 @@ describe('document selection', () => { describe('SelectButton', () => { test('is not checked', () => { const contextMock = { - ...discoverGridContextMock, + ...dataTableContextMock, }; const component = mountWithIntl( - + { isDetails={false} isExpandable={false} /> - + ); const checkBox = findTestSubject(component, 'dscGridSelectDoc-i::1::'); @@ -58,12 +58,12 @@ describe('document selection', () => { test('is checked', () => { const contextMock = { - ...discoverGridContextMock, + ...dataTableContextMock, selectedDocs: ['i::1::'], }; const component = mountWithIntl( - + { isDetails={false} isExpandable={false} /> - + ); const checkBox = findTestSubject(component, 'dscGridSelectDoc-i::1::'); @@ -82,11 +82,11 @@ describe('document selection', () => { test('adding a selection', () => { const contextMock = { - ...discoverGridContextMock, + ...dataTableContextMock, }; const component = mountWithIntl( - + { isDetails={false} isExpandable={false} /> - + ); const checkBox = findTestSubject(component, 'dscGridSelectDoc-i::1::'); @@ -105,12 +105,12 @@ describe('document selection', () => { }); test('removing a selection', () => { const contextMock = { - ...discoverGridContextMock, + ...dataTableContextMock, selectedDocs: ['i::1::'], }; const component = mountWithIntl( - + { isDetails={false} isExpandable={false} /> - + ); const checkBox = findTestSubject(component, 'dscGridSelectDoc-i::1::'); @@ -128,16 +128,16 @@ describe('document selection', () => { expect(contextMock.setSelectedDocs).toHaveBeenCalledWith([]); }); }); - describe('DiscoverGridDocumentToolbarBtn', () => { + describe('DataTableDocumentToolbarBtn', () => { test('it renders a button clickable button', () => { const props = { isFilterActive: false, - rows: discoverGridContextMock.rows, + rows: dataTableContextMock.rows, selectedDocs: ['i::1::'], setIsFilterActive: jest.fn(), setSelectedDocs: jest.fn(), }; - const component = mountWithIntl(); + const component = mountWithIntl(); const button = findTestSubject(component, 'dscGridSelectionBtn'); expect(button.length).toBe(1); }); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx b/packages/kbn-unified-data-table/src/components/data_table_document_selection.tsx similarity index 89% rename from src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx rename to packages/kbn-unified-data-table/src/components/data_table_document_selection.tsx index f14ec323f8ca2..213e24790e840 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_document_selection.tsx @@ -20,15 +20,15 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { euiDarkVars as themeDark, euiLightVars as themeLight } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { DiscoverGridContext } from './discover_grid_context'; +import { UnifiedDataTableContext } from '../table_context'; export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { const { selectedDocs, expanded, rows, isDarkMode, setSelectedDocs } = - useContext(DiscoverGridContext); + useContext(UnifiedDataTableContext); const doc = useMemo(() => rows[rowIndex], [rows, rowIndex]); const checked = useMemo(() => selectedDocs.includes(doc.id), [selectedDocs, doc.id]); - const toggleDocumentSelectionLabel = i18n.translate('discover.grid.selectDoc', { + const toggleDocumentSelectionLabel = i18n.translate('unifiedDataTable.grid.selectDoc', { defaultMessage: `Select document '{rowNumber}'`, values: { rowNumber: rowIndex + 1 }, }); @@ -63,7 +63,7 @@ export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle ); }; -export function DiscoverGridDocumentToolbarBtn({ +export function DataTableDocumentToolbarBtn({ isFilterActive, rows, selectedDocs, @@ -90,7 +90,10 @@ export function DiscoverGridDocumentToolbarBtn({ setIsFilterActive(false); }} > - + ) : ( @@ -122,7 +125,7 @@ export function DiscoverGridDocumentToolbarBtn({ {(copy) => ( @@ -138,7 +141,7 @@ export function DiscoverGridDocumentToolbarBtn({ setIsFilterActive(false); }} > - + , ]; }, [ @@ -176,7 +179,7 @@ export function DiscoverGridDocumentToolbarBtn({ })} > diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx b/packages/kbn-unified-data-table/src/components/data_table_expand_button.test.tsx similarity index 70% rename from src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx rename to packages/kbn-unified-data-table/src/components/data_table_expand_button.test.tsx index e95853b99b7b7..56d6d3ae3ce0e 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_expand_button.test.tsx @@ -9,18 +9,18 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { ExpandButton } from './discover_grid_expand_button'; -import { DiscoverGridContext } from './discover_grid_context'; -import { discoverGridContextMock } from '../../__mocks__/grid_context'; +import { ExpandButton } from './data_table_expand_button'; +import { UnifiedDataTableContext } from '../table_context'; +import { dataTableContextMock } from '../../__mocks__/table_context'; -describe('Discover grid view button ', function () { +describe('Data table view button ', function () { it('when no document is expanded, setExpanded is called with current document', async () => { const contextMock = { - ...discoverGridContextMock, + ...dataTableContextMock, }; const component = mountWithIntl( - + - + ); const button = findTestSubject(component, 'docTableExpandToggleColumn'); await button.simulate('click'); - expect(contextMock.setExpanded).toHaveBeenCalledWith(discoverGridContextMock.rows[0]); + expect(contextMock.setExpanded).toHaveBeenCalledWith(dataTableContextMock.rows[0]); }); it('when the current document is expanded, setExpanded is called with undefined', async () => { const contextMock = { - ...discoverGridContextMock, - expanded: discoverGridContextMock.rows[0], + ...dataTableContextMock, + expanded: dataTableContextMock.rows[0], }; const component = mountWithIntl( - + - + ); const button = findTestSubject(component, 'docTableExpandToggleColumn'); await button.simulate('click'); @@ -61,12 +61,12 @@ describe('Discover grid view button ', function () { }); it('when another document is expanded, setExpanded is called with the current document', async () => { const contextMock = { - ...discoverGridContextMock, - expanded: discoverGridContextMock.rows[0], + ...dataTableContextMock, + expanded: dataTableContextMock.rows[0], }; const component = mountWithIntl( - + - + ); const button = findTestSubject(component, 'docTableExpandToggleColumn'); await button.simulate('click'); - expect(contextMock.setExpanded).toHaveBeenCalledWith(discoverGridContextMock.rows[1]); + expect(contextMock.setExpanded).toHaveBeenCalledWith(dataTableContextMock.rows[1]); }); }); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx b/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx similarity index 86% rename from src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx rename to packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx index 1a127f3639432..108ffaa4ec5fe 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx @@ -10,8 +10,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; -import { DiscoverGridContext } from './discover_grid_context'; -import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../discover_tour'; +import { UnifiedDataTableContext } from '../table_context'; /** * Button to expand a given row @@ -19,9 +18,11 @@ import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../discover_tour'; export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { const toolTipRef = useRef(null); const [pressed, setPressed] = useState(false); - const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext); + const { expanded, setExpanded, rows, isDarkMode, componentsTourSteps } = + useContext(UnifiedDataTableContext); const current = rows[rowIndex]; + const tourStep = componentsTourSteps ? componentsTourSteps.expandButton : undefined; useEffect(() => { if (current.isAnchor) { setCellProps({ @@ -39,7 +40,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle }, [expanded, current, setCellProps, isDarkMode]); const isCurrentRowExpanded = current === expanded; - const buttonLabel = i18n.translate('discover.grid.viewDoc', { + const buttonLabel = i18n.translate('unifiedDataTable.grid.viewDoc', { defaultMessage: 'Toggle dialog with details', }); @@ -63,7 +64,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle return ( { const component = mountWithIntl( - - + ); @@ -31,32 +33,32 @@ describe('DiscoverGridFooter', function () { it('should not render anything yet when all rows shown', async () => { const component = mountWithIntl( - - - + ); expect(component.isEmptyRender()).toBe(true); }); it('should render a message for the last page', async () => { const component = mountWithIntl( - - - + ); - expect(findTestSubject(component, 'discoverTableFooter').text()).toBe( + expect(findTestSubject(component, 'unifiedDataTableFooter').text()).toBe( 'Search results are limited to 500 documents. Add more search terms to narrow your search.' ); expect(findTestSubject(component, 'dscGridSampleSizeFetchMoreLink').exists()).toBe(false); @@ -66,19 +68,19 @@ describe('DiscoverGridFooter', function () { const mockLoadMore = jest.fn(); const component = mountWithIntl( - - - + ); - expect(findTestSubject(component, 'discoverTableFooter').text()).toBe( + expect(findTestSubject(component, 'unifiedDataTableFooter').text()).toBe( 'Search results are limited to 500 documents.Load more' ); @@ -94,19 +96,19 @@ describe('DiscoverGridFooter', function () { const mockLoadMore = jest.fn(); const component = mountWithIntl( - - - + ); - expect(findTestSubject(component, 'discoverTableFooter').text()).toBe( + expect(findTestSubject(component, 'unifiedDataTableFooter').text()).toBe( 'Search results are limited to 500 documents.Load more' ); @@ -121,17 +123,17 @@ describe('DiscoverGridFooter', function () { it('should render a message when max total limit is reached', async () => { const component = mountWithIntl( - - - + ); - expect(findTestSubject(component, 'discoverTableFooter').text()).toBe( + expect(findTestSubject(component, 'unifiedDataTableFooter').text()).toBe( 'Search results are limited to 10000 documents. Add more search terms to narrow your search.' ); }); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_footer.tsx b/packages/kbn-unified-data-table/src/components/data_table_footer.tsx similarity index 75% rename from src/plugins/discover/public/components/discover_grid/discover_grid_footer.tsx rename to packages/kbn-unified-data-table/src/components/data_table_footer.tsx index 540c7102bd424..21819a023afed 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_footer.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_footer.tsx @@ -12,10 +12,11 @@ import { EuiButtonEmpty, EuiToolTip, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; -import { MAX_LOADED_GRID_ROWS } from '../../../common/constants'; -import { useDiscoverServices } from '../../hooks/use_discover_services'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { MAX_LOADED_GRID_ROWS } from '../constants'; -export interface DiscoverGridFooterProps { +export interface UnifiedDataTableFooterProps { isLoadingMore?: boolean; rowCount: number; sampleSize: number; @@ -23,9 +24,11 @@ export interface DiscoverGridFooterProps { pageCount: number; totalHits?: number; onFetchMoreRecords?: () => void; + data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; } -export const DiscoverGridFooter: React.FC = (props) => { +export const UnifiedDataTableFooter: React.FC = (props) => { const { isLoadingMore, rowCount, @@ -34,8 +37,8 @@ export const DiscoverGridFooter: React.FC = (props) => pageCount, totalHits = 0, onFetchMoreRecords, + data, } = props; - const { data } = useDiscoverServices(); const timefilter = data.query.timefilter.timefilter; const [refreshInterval, setRefreshInterval] = useState(timefilter.getRefreshInterval()); @@ -58,15 +61,15 @@ export const DiscoverGridFooter: React.FC = (props) => return null; } - // allow to fetch more records on Discover page + // allow to fetch more records for UnifiedDataTable if (onFetchMoreRecords && typeof isLoadingMore === 'boolean') { if (rowCount <= MAX_LOADED_GRID_ROWS - sampleSize) { return ( - + = (props) => `} > - + ); } - return ; + return ; } if (rowCount < totalHits) { // show only a message for embeddable - return ; + return ; } return null; }; -interface DiscoverGridFooterContainerProps extends DiscoverGridFooterProps { +interface UnifiedDataTableFooterContainerProps extends UnifiedDataTableFooterProps { hasButton: boolean; } -const DiscoverGridFooterContainer: React.FC = ({ +const UnifiedDataTableFooterContainer: React.FC = ({ hasButton, rowCount, children, + fieldFormats, }) => { const { euiTheme } = useEuiTheme(); - const { fieldFormats } = useDiscoverServices(); const formattedRowCount = fieldFormats .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) @@ -121,7 +124,7 @@ const DiscoverGridFooterContainer: React.FC = return (

= {hasButton ? ( = /> ) : ( true); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + copyToClipboard: (value: string) => mockCopyToClipboard(value), + }; +}); + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { + FilterInBtn, + FilterOutBtn, + buildCellActions, + buildCopyValueButton, +} from './default_cell_actions'; +import { servicesMock } from '../../__mocks__/services'; +import { UnifiedDataTableContext } from '../table_context'; +import { EuiButton, EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { dataTableContextMock } from '../../__mocks__/table_context'; +import { DataViewField } from '@kbn/data-views-plugin/public'; + +describe('Default cell actions ', function () { + const CopyBtn = buildCopyValueButton( + { + Component: () => <>, + colIndex: 0, + columnId: 'extension', + } as unknown as EuiDataGridColumnCellActionProps, + servicesMock.toastNotifications, + dataTableContextMock.valueToStringConverter + ); + + it('should not show cell actions for unfilterable fields', async () => { + const cellActions = buildCellActions( + { name: 'foo', filterable: false } as DataViewField, + servicesMock.toastNotifications, + dataTableContextMock.valueToStringConverter + ); + expect(cellActions.length).toEqual(1); + expect( + cellActions[0]({ + Component: () => <>, + colIndex: 1, + columnId: 'extension', + } as unknown as EuiDataGridColumnCellActionProps).props['aria-label'] + ).toEqual(CopyBtn.props['aria-label']); + }); + + it('should show filter actions for filterable fields', async () => { + const cellActions = buildCellActions( + { name: 'foo', filterable: true } as DataViewField, + servicesMock.toastNotifications, + dataTableContextMock.valueToStringConverter, + jest.fn() + ); + expect(cellActions).toContain(FilterInBtn); + expect(cellActions).toContain(FilterOutBtn); + }); + + it('should show Copy action for _source field', async () => { + const cellActions = buildCellActions( + { name: '_source', type: '_source', filterable: false } as DataViewField, + servicesMock.toastNotifications, + dataTableContextMock.valueToStringConverter + ); + expect( + cellActions[0]({ + Component: () => <>, + colIndex: 1, + columnId: 'extension', + } as unknown as EuiDataGridColumnCellActionProps).props['aria-label'] + ).toEqual(CopyBtn.props['aria-label']); + }); + + it('triggers filter function when FilterInBtn is clicked', async () => { + const component = mountWithIntl( + + } + rowIndex={1} + colIndex={1} + columnId="extension" + isExpanded={false} + /> + + ); + const button = findTestSubject(component, 'filterForButton'); + await button.simulate('click'); + expect(dataTableContextMock.onFilter).toHaveBeenCalledWith( + dataTableContextMock.dataView.fields.getByName('extension'), + 'jpg', + '+' + ); + }); + it('triggers filter function when FilterInBtn is clicked for a non-provided value', async () => { + const component = mountWithIntl( + + } + rowIndex={0} + colIndex={1} + columnId="extension" + isExpanded={false} + /> + + ); + const button = findTestSubject(component, 'filterForButton'); + await button.simulate('click'); + expect(dataTableContextMock.onFilter).toHaveBeenCalledWith( + dataTableContextMock.dataView.fields.getByName('extension'), + undefined, + '+' + ); + }); + it('triggers filter function when FilterInBtn is clicked for an empty string value', async () => { + const component = mountWithIntl( + + } + rowIndex={4} + colIndex={1} + columnId="message" + isExpanded={false} + /> + + ); + const button = findTestSubject(component, 'filterForButton'); + await button.simulate('click'); + expect(dataTableContextMock.onFilter).toHaveBeenCalledWith( + dataTableContextMock.dataView.fields.getByName('message'), + '', + '+' + ); + }); + it('triggers filter function when FilterOutBtn is clicked', async () => { + const component = mountWithIntl( + + } + rowIndex={1} + colIndex={1} + columnId="extension" + isExpanded={false} + /> + + ); + const button = findTestSubject(component, 'filterOutButton'); + await button.simulate('click'); + expect(dataTableContextMock.onFilter).toHaveBeenCalledWith( + dataTableContextMock.dataView.fields.getByName('extension'), + 'jpg', + '-' + ); + }); + it('triggers clipboard copy when CopyBtn is clicked', async () => { + const component = mountWithIntl( + + {buildCopyValueButton( + { + Component: (props: any) => , + colIndex: 1, + rowIndex: 1, + columnId: 'extension', + } as unknown as EuiDataGridColumnCellActionProps, + servicesMock.toastNotifications, + dataTableContextMock.valueToStringConverter + )} + + ); + const button = findTestSubject(component, 'copyClipboardButton'); + await button.simulate('click'); + expect(mockCopyToClipboard).toHaveBeenCalledWith('jpg'); + }); +}); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx b/packages/kbn-unified-data-table/src/components/default_cell_actions.tsx similarity index 59% rename from src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx rename to packages/kbn-unified-data-table/src/components/default_cell_actions.tsx index 59cd130277f90..6005d75ea6632 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx +++ b/packages/kbn-unified-data-table/src/components/default_cell_actions.tsx @@ -9,14 +9,15 @@ import React, { useContext } from 'react'; import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DataViewField } from '@kbn/data-views-plugin/public'; -import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; -import { DiscoverGridContext, GridContext } from './discover_grid_context'; -import { useDiscoverServices } from '../../hooks/use_discover_services'; -import { copyValueToClipboard } from '../../utils/copy_value_to_clipboard'; +import type { DataViewField } from '@kbn/data-views-plugin/public'; +import { ToastsStart } from '@kbn/core/public'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { UnifiedDataTableContext, DataTableContext } from '../table_context'; +import { copyValueToClipboard } from '../utils/copy_value_to_clipboard'; +import { ValueToStringConverter } from '../types'; function onFilterCell( - context: GridContext, + context: DataTableContext, rowIndex: EuiDataGridColumnCellActionProps['rowIndex'], columnId: EuiDataGridColumnCellActionProps['columnId'], mode: '+' | '-' @@ -35,8 +36,8 @@ export const FilterInBtn = ({ rowIndex, columnId, }: EuiDataGridColumnCellActionProps) => { - const context = useContext(DiscoverGridContext); - const buttonTitle = i18n.translate('discover.grid.filterForAria', { + const context = useContext(UnifiedDataTableContext); + const buttonTitle = i18n.translate('unifiedDataTable.grid.filterForAria', { defaultMessage: 'Filter for this {value}', values: { value: columnId }, }); @@ -51,7 +52,7 @@ export const FilterInBtn = ({ title={buttonTitle} data-test-subj="filterForButton" > - {i18n.translate('discover.grid.filterFor', { + {i18n.translate('unifiedDataTable.grid.filterFor', { defaultMessage: 'Filter for', })} @@ -63,8 +64,8 @@ export const FilterOutBtn = ({ rowIndex, columnId, }: EuiDataGridColumnCellActionProps) => { - const context = useContext(DiscoverGridContext); - const buttonTitle = i18n.translate('discover.grid.filterOutAria', { + const context = useContext(UnifiedDataTableContext); + const buttonTitle = i18n.translate('unifiedDataTable.grid.filterOutAria', { defaultMessage: 'Filter out this {value}', values: { value: columnId }, }); @@ -79,18 +80,19 @@ export const FilterOutBtn = ({ title={buttonTitle} data-test-subj="filterOutButton" > - {i18n.translate('discover.grid.filterOut', { + {i18n.translate('unifiedDataTable.grid.filterOut', { defaultMessage: 'Filter out', })} ); }; -export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) => { - const { valueToStringConverter } = useContext(DiscoverGridContext); - const { toastNotifications } = useDiscoverServices(); - - const buttonTitle = i18n.translate('discover.grid.copyClipboardButtonTitle', { +export function buildCopyValueButton( + { Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps, + toastNotifications: ToastsStart, + valueToStringConverter: ValueToStringConverter +) { + const buttonTitle = i18n.translate('unifiedDataTable.grid.copyClipboardButtonTitle', { defaultMessage: 'Copy value of {column}', values: { column: columnId }, }); @@ -101,8 +103,8 @@ export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCell copyValueToClipboard({ rowIndex, columnId, - toastNotifications, valueToStringConverter, + toastNotifications, }); }} iconType="copyClipboard" @@ -110,13 +112,26 @@ export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCell title={buttonTitle} data-test-subj="copyClipboardButton" > - {i18n.translate('discover.grid.copyCellValueButton', { + {i18n.translate('unifiedDataTable.grid.copyCellValueButton', { defaultMessage: 'Copy value', })} ); -}; +} -export function buildCellActions(field: DataViewField, onFilter?: DocViewFilterFn) { - return [...(onFilter && field.filterable ? [FilterInBtn, FilterOutBtn] : []), CopyBtn]; +export function buildCellActions( + field: DataViewField, + toastNotifications: ToastsStart, + valueToStringConverter: ValueToStringConverter, + onFilter?: DocViewFilterFn +) { + return [ + ...(onFilter && field.filterable ? [FilterInBtn, FilterOutBtn] : []), + ({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) => + buildCopyValueButton( + { Component, rowIndex, columnId } as EuiDataGridColumnCellActionProps, + toastNotifications, + valueToStringConverter + ), + ]; } diff --git a/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/packages/kbn-unified-data-table/src/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap rename to packages/kbn-unified-data-table/src/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap diff --git a/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.scss b/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.scss new file mode 100644 index 0000000000000..a07f7ccac408d --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.scss @@ -0,0 +1,3 @@ +.unifiedDataTableJsonEditor { + width: 100%; +} diff --git a/src/plugins/discover/public/components/json_code_editor/json_code_editor.test.tsx b/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.test.tsx similarity index 92% rename from src/plugins/discover/public/components/json_code_editor/json_code_editor.test.tsx rename to packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.test.tsx index 9ec60410b591e..e1ec1373f8657 100644 --- a/src/plugins/discover/public/components/json_code_editor/json_code_editor.test.tsx +++ b/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { JsonCodeEditor } from './json_code_editor'; +import JsonCodeEditor from './json_code_editor'; it('returns the `JsonCodeEditor` component', () => { const value = { diff --git a/src/plugins/discover/public/components/json_code_editor/json_code_editor.tsx b/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.tsx similarity index 78% rename from src/plugins/discover/public/components/json_code_editor/json_code_editor.tsx rename to packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.tsx index 426a180d37957..d08e35eb6d4bf 100644 --- a/src/plugins/discover/public/components/json_code_editor/json_code_editor.tsx +++ b/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor.tsx @@ -11,14 +11,21 @@ import './json_code_editor.scss'; import React from 'react'; import { JsonCodeEditorCommon } from './json_code_editor_common'; -interface JsonCodeEditorProps { +export interface JsonCodeEditorProps { json: Record; width?: string | number; height?: string | number; hasLineNumbers?: boolean; } -export const JsonCodeEditor = ({ json, width, height, hasLineNumbers }: JsonCodeEditorProps) => { +// Required for usage in React.lazy +// eslint-disable-next-line import/no-default-export +export default function JsonCodeEditor({ + json, + width, + height, + hasLineNumbers, +}: JsonCodeEditorProps) { const jsonValue = JSON.stringify(json, null, 2); return ( @@ -31,4 +38,4 @@ export const JsonCodeEditor = ({ json, width, height, hasLineNumbers }: JsonCode hideCopyButton={true} /> ); -}; +} diff --git a/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor_common.tsx b/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor_common.tsx new file mode 100644 index 0000000000000..079a98c305459 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/json_code_editor/json_code_editor_common.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './json_code_editor.scss'; + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { monaco, XJsonLang } from '@kbn/monaco'; +import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { CodeEditor } from '@kbn/code-editor'; +const codeEditorAriaLabel = i18n.translate('unifiedDataTable.json.codeEditorAriaLabel', { + defaultMessage: 'Read only JSON view of an elasticsearch document', +}); +const copyToClipboardLabel = i18n.translate('unifiedDataTable.json.copyToClipboardLabel', { + defaultMessage: 'Copy to clipboard', +}); + +interface JsonCodeEditorCommonProps { + jsonValue: string; + onEditorDidMount: (editor: monaco.editor.IStandaloneCodeEditor) => void; + width?: string | number; + height?: string | number; + hasLineNumbers?: boolean; + hideCopyButton?: boolean; +} + +export const JsonCodeEditorCommon = ({ + jsonValue, + width, + height, + hasLineNumbers, + onEditorDidMount, + hideCopyButton, +}: JsonCodeEditorCommonProps) => { + if (jsonValue === '') { + return null; + } + + const codeEditor = ( + + ); + if (hideCopyButton) { + return codeEditor; + } + return ( + + + +

+ + {(copy) => ( + + {copyToClipboardLabel} + + )} + +
+ + {codeEditor} + + ); +}; + +export const JSONCodeEditorCommonMemoized = React.memo((props: JsonCodeEditorCommonProps) => { + return ; +}); diff --git a/src/plugins/discover/public/components/discover_grid/constants.ts b/packages/kbn-unified-data-table/src/constants.ts similarity index 81% rename from src/plugins/discover/public/components/discover_grid/constants.ts rename to packages/kbn-unified-data-table/src/constants.ts index 8f7c40e33b957..1fb391ddc7f70 100644 --- a/src/plugins/discover/public/components/discover_grid/constants.ts +++ b/packages/kbn-unified-data-table/src/constants.ts @@ -5,11 +5,17 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { EuiDataGridStyle } from '@elastic/eui'; -// data types +export const DEFAULT_ROWS_PER_PAGE = 100; +export const MAX_LOADED_GRID_ROWS = 10000; + +export const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, DEFAULT_ROWS_PER_PAGE, 250, 500]; + +export const defaultMonacoEditorWidth = 370; +export const defaultTimeColumnWidth = 210; export const kibanaJSON = 'kibana-json'; + export const GRID_STYLE = { border: 'all', fontSize: 's', @@ -17,12 +23,9 @@ export const GRID_STYLE = { rowHover: 'none', } as EuiDataGridStyle; -export const defaultTimeColumnWidth = 210; export const toolbarVisibility = { showColumnSelector: { allowHide: false, allowReorder: true, }, }; - -export const defaultMonacoEditorWidth = 370; diff --git a/src/plugins/discover/public/hooks/use_data_grid_columns.test.tsx b/packages/kbn-unified-data-table/src/hooks/use_data_grid_columns.test.tsx similarity index 94% rename from src/plugins/discover/public/hooks/use_data_grid_columns.test.tsx rename to packages/kbn-unified-data-table/src/hooks/use_data_grid_columns.test.tsx index 4a1874ec8e940..e0afdab4ff043 100644 --- a/src/plugins/discover/public/hooks/use_data_grid_columns.test.tsx +++ b/packages/kbn-unified-data-table/src/hooks/use_data_grid_columns.test.tsx @@ -9,8 +9,8 @@ import { renderHook } from '@testing-library/react-hooks'; import { useColumns } from './use_data_grid_columns'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import { configMock } from '../__mocks__/config'; -import { dataViewsMock } from '../__mocks__/data_views'; +import { configMock } from '../../__mocks__/config'; +import { dataViewsMock } from '../../__mocks__/data_views'; import { Capabilities } from '@kbn/core/types'; describe('useColumns', () => { diff --git a/src/plugins/discover/public/hooks/use_data_grid_columns.ts b/packages/kbn-unified-data-table/src/hooks/use_data_grid_columns.ts similarity index 74% rename from src/plugins/discover/public/hooks/use_data_grid_columns.ts rename to packages/kbn-unified-data-table/src/hooks/use_data_grid_columns.ts index 22fc8e9836888..088f7b0491c69 100644 --- a/src/plugins/discover/public/hooks/use_data_grid_columns.ts +++ b/packages/kbn-unified-data-table/src/hooks/use_data_grid_columns.ts @@ -9,32 +9,30 @@ import { useEffect, useMemo, useState } from 'react'; import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; -import { Capabilities, IUiSettingsClient } from '@kbn/core/public'; +import { Capabilities } from '@kbn/core/public'; import { isEqual } from 'lodash'; -import { DiscoverAppStateContainer } from '../application/main/services/discover_app_state_container'; -import { GetStateReturn as ContextGetStateReturn } from '../application/context/services/context_state'; -import { getStateColumnActions } from '../components/doc_table/actions/columns'; +import { getStateColumnActions } from '../components/actions/columns'; interface UseColumnsProps { capabilities: Capabilities; - config: IUiSettingsClient; dataView: DataView; dataViews: DataViewsContract; useNewFieldsApi: boolean; - setAppState: DiscoverAppStateContainer['update'] | ContextGetStateReturn['setAppState']; + setAppState: (state: { columns: string[]; sort?: string[][] }) => void; columns?: string[]; sort?: string[][]; + defaultOrder?: string; } export const useColumns = ({ capabilities, - config, dataView, dataViews, setAppState, useNewFieldsApi, columns, sort, + defaultOrder = 'desc', }: UseColumnsProps) => { const [usedColumns, setUsedColumns] = useState(getColumns(columns, useNewFieldsApi)); useEffect(() => { @@ -48,15 +46,24 @@ export const useColumns = ({ () => getStateColumnActions({ capabilities, - config, dataView, dataViews, setAppState, useNewFieldsApi, columns: usedColumns, sort, + defaultOrder, }), - [capabilities, config, dataView, dataViews, setAppState, sort, useNewFieldsApi, usedColumns] + [ + capabilities, + dataView, + dataViews, + defaultOrder, + setAppState, + sort, + useNewFieldsApi, + usedColumns, + ] ); return { diff --git a/packages/kbn-unified-data-table/src/hooks/use_row_heights_options.test.tsx b/packages/kbn-unified-data-table/src/hooks/use_row_heights_options.test.tsx new file mode 100644 index 0000000000000..2da08c178720a --- /dev/null +++ b/packages/kbn-unified-data-table/src/hooks/use_row_heights_options.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { LocalStorageMock } from '../../__mocks__/local_storage_mock'; +import { useRowHeightsOptions } from './use_row_heights_options'; + +const CONFIG_ROW_HEIGHT = 3; + +describe('useRowHeightsOptions', () => { + test('should apply rowHeight from savedSearch', () => { + const { result } = renderHook(() => { + return useRowHeightsOptions({ + rowHeightState: 2, + storage: new LocalStorageMock({}) as unknown as Storage, + consumer: 'discover', + }); + }); + + expect(result.current.defaultHeight).toEqual({ lineCount: 2 }); + }); + + test('should apply rowHeight from local storage', () => { + const { result } = renderHook(() => { + return useRowHeightsOptions({ + storage: new LocalStorageMock({ + ['discover:dataGridRowHeight']: { + previousRowHeight: 5, + previousConfigRowHeight: 3, + }, + }) as unknown as Storage, + consumer: 'discover', + }); + }); + + expect(result.current.defaultHeight).toEqual({ lineCount: 5 }); + }); + + test('should apply rowHeight from configRowHeight', () => { + const { result } = renderHook(() => { + return useRowHeightsOptions({ + consumer: 'discover', + configRowHeight: 3, + storage: new LocalStorageMock({}) as unknown as Storage, + }); + }); + + expect(result.current.defaultHeight).toEqual({ + lineCount: CONFIG_ROW_HEIGHT, + }); + }); + + test('should apply rowHeight from uiSettings instead of local storage value, since uiSettings has been changed', () => { + const { result } = renderHook(() => { + return useRowHeightsOptions({ + storage: new LocalStorageMock({ + ['discover:dataGridRowHeight']: { + previousRowHeight: 4, + // different from uiSettings (config), now user changed it to 3, but prev was 4 + previousConfigRowHeight: 4, + }, + }) as unknown as Storage, + consumer: 'discover', + }); + }); + + expect(result.current.defaultHeight).toEqual({ + lineCount: CONFIG_ROW_HEIGHT, + }); + }); +}); diff --git a/src/plugins/discover/public/hooks/use_row_heights_options.ts b/packages/kbn-unified-data-table/src/hooks/use_row_heights_options.ts similarity index 84% rename from src/plugins/discover/public/hooks/use_row_heights_options.ts rename to packages/kbn-unified-data-table/src/hooks/use_row_heights_options.ts index a9ef67ace530b..9d460c8ea2ba9 100644 --- a/src/plugins/discover/public/hooks/use_row_heights_options.ts +++ b/packages/kbn-unified-data-table/src/hooks/use_row_heights_options.ts @@ -7,10 +7,9 @@ */ import type { EuiDataGridRowHeightOption, EuiDataGridRowHeightsOptions } from '@elastic/eui'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import { useMemo } from 'react'; -import { ROW_HEIGHT_OPTION } from '@kbn/discover-utils'; import { isValidRowHeight } from '../utils/validate_row_height'; -import { useDiscoverServices } from './use_discover_services'; import { DataGridOptionsRecord, getStoredRowHeight, @@ -20,6 +19,9 @@ import { interface UseRowHeightProps { rowHeightState?: number; onUpdateRowHeight?: (rowHeight: number) => void; + storage: Storage; + configRowHeight?: number; + consumer: string; } /** @@ -30,6 +32,7 @@ interface UseRowHeightProps { */ const SINGLE_ROW_HEIGHT_OPTION = 0; const AUTO_ROW_HEIGHT_OPTION = -1; +const DEFAULT_ROW_HEIGHT_OPTION = 3; /** * Converts rowHeight of EuiDataGrid to rowHeight number (-1 to 20) @@ -57,12 +60,15 @@ const deserializeRowHeight = (number: number): EuiDataGridRowHeightOption | unde return { lineCount: number }; // custom }; -export const useRowHeightsOptions = ({ rowHeightState, onUpdateRowHeight }: UseRowHeightProps) => { - const { storage, uiSettings } = useDiscoverServices(); - +export const useRowHeightsOptions = ({ + rowHeightState, + onUpdateRowHeight, + storage, + configRowHeight = DEFAULT_ROW_HEIGHT_OPTION, + consumer, +}: UseRowHeightProps) => { return useMemo((): EuiDataGridRowHeightsOptions => { - const rowHeightFromLS = getStoredRowHeight(storage); - const configRowHeight = uiSettings.get(ROW_HEIGHT_OPTION); + const rowHeightFromLS = getStoredRowHeight(storage, consumer); const configHasNotChanged = ( localStorageRecord: DataGridOptionsRecord | null @@ -83,9 +89,9 @@ export const useRowHeightsOptions = ({ rowHeightState, onUpdateRowHeight }: UseR lineHeight: '1.6em', onChange: ({ defaultHeight: newRowHeight }: EuiDataGridRowHeightsOptions) => { const newSerializedRowHeight = serializeRowHeight(newRowHeight); - updateStoredRowHeight(newSerializedRowHeight, configRowHeight, storage); + updateStoredRowHeight(newSerializedRowHeight, configRowHeight, storage, consumer); onUpdateRowHeight?.(newSerializedRowHeight); }, }; - }, [rowHeightState, uiSettings, storage, onUpdateRowHeight]); + }, [storage, consumer, rowHeightState, configRowHeight, onUpdateRowHeight]); }; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx b/packages/kbn-unified-data-table/src/table_context.tsx similarity index 70% rename from src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx rename to packages/kbn-unified-data-table/src/table_context.tsx index 8ec4689a00178..2a1d4656d4a65 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx +++ b/packages/kbn-unified-data-table/src/table_context.tsx @@ -9,10 +9,10 @@ import React from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import type { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; -import type { ValueToStringConverter } from '../../types'; +import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import type { ValueToStringConverter } from './types'; -export interface GridContext { +export interface DataTableContext { expanded?: DataTableRecord | undefined; setExpanded?: (hit?: DataTableRecord) => void; rows: DataTableRecord[]; @@ -22,8 +22,9 @@ export interface GridContext { selectedDocs: string[]; setSelectedDocs: (selected: string[]) => void; valueToStringConverter: ValueToStringConverter; + componentsTourSteps?: Record; } -const defaultContext = {} as unknown as GridContext; +const defaultContext = {} as unknown as DataTableContext; -export const DiscoverGridContext = React.createContext(defaultContext); +export const UnifiedDataTableContext = React.createContext(defaultContext); diff --git a/src/plugins/discover/public/types.ts b/packages/kbn-unified-data-table/src/types.ts similarity index 57% rename from src/plugins/discover/public/types.ts rename to packages/kbn-unified-data-table/src/types.ts index 051892902239d..79ca4e721e910 100644 --- a/src/plugins/discover/public/types.ts +++ b/packages/kbn-unified-data-table/src/types.ts @@ -6,18 +6,19 @@ * Side Public License, v 1. */ -import type { DatatableColumn } from '@kbn/expressions-plugin/common'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; -import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; +/** + * User configurable state of data grid, persisted in saved search + */ +export interface UnifiedDataTableSettings { + columns?: Record; +} + +export interface UnifiedDataTableSettingsColumn { + width?: number; +} export type ValueToStringConverter = ( rowIndex: number, columnId: string, options?: { compatibleWithCSV?: boolean } ) => { formattedString: string; withFormula: boolean }; - -export interface RecordsFetchResponse { - records: DataTableRecord[]; - textBasedQueryColumns?: DatatableColumn[]; - interceptedWarnings?: SearchResponseInterceptedWarning[]; -} diff --git a/src/plugins/discover/public/utils/columns.test.ts b/packages/kbn-unified-data-table/src/utils/columns.test.ts similarity index 95% rename from src/plugins/discover/public/utils/columns.test.ts rename to packages/kbn-unified-data-table/src/utils/columns.test.ts index 5ef7d8fea450f..36a8b60a6bc68 100644 --- a/src/plugins/discover/public/utils/columns.test.ts +++ b/packages/kbn-unified-data-table/src/utils/columns.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ +import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { getDisplayedColumns } from './columns'; -import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; describe('getDisplayedColumns', () => { diff --git a/src/plugins/discover/public/utils/columns.ts b/packages/kbn-unified-data-table/src/utils/columns.ts similarity index 94% rename from src/plugins/discover/public/utils/columns.ts rename to packages/kbn-unified-data-table/src/utils/columns.ts index 49e234b11decc..f2a72f0a8b650 100644 --- a/src/plugins/discover/public/utils/columns.ts +++ b/packages/kbn-unified-data-table/src/utils/columns.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; // We store this outside the function as a constant, so we're not creating a new array every time // the function is returning this. A changing array might cause the data grid to think it got diff --git a/src/plugins/discover/public/utils/convert_value_to_string.test.tsx b/packages/kbn-unified-data-table/src/utils/convert_value_to_string.test.tsx similarity index 67% rename from src/plugins/discover/public/utils/convert_value_to_string.test.tsx rename to packages/kbn-unified-data-table/src/utils/convert_value_to_string.test.tsx index dd81ad621f182..aa8ba719c5ba2 100644 --- a/src/plugins/discover/public/utils/convert_value_to_string.test.tsx +++ b/packages/kbn-unified-data-table/src/utils/convert_value_to_string.test.tsx @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -import { discoverGridContextComplexMock, discoverGridContextMock } from '../__mocks__/grid_context'; -import { discoverServiceMock } from '../__mocks__/services'; +import { dataTableContextComplexMock, dataTableContextMock } from '../../__mocks__/table_context'; +import { servicesMock } from '../../__mocks__/services'; import { convertValueToString, convertNameToString } from './convert_value_to_string'; describe('convertValueToString', () => { it('should convert a keyword value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'keyword_key', rowIndex: 0, options: { @@ -28,9 +28,9 @@ describe('convertValueToString', () => { it('should convert a text value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'text_message', rowIndex: 0, options: { @@ -43,9 +43,9 @@ describe('convertValueToString', () => { it('should convert a text value to text (not for CSV)', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'text_message', rowIndex: 0, options: { @@ -58,9 +58,9 @@ describe('convertValueToString', () => { it('should convert a multiline text value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'text_message', rowIndex: 1, options: { @@ -74,9 +74,9 @@ describe('convertValueToString', () => { it('should convert a number value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'number_price', rowIndex: 0, options: { @@ -89,9 +89,9 @@ describe('convertValueToString', () => { it('should convert a date value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'date', rowIndex: 0, options: { @@ -104,9 +104,9 @@ describe('convertValueToString', () => { it('should convert a date nanos value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'date_nanos', rowIndex: 0, options: { @@ -119,9 +119,9 @@ describe('convertValueToString', () => { it('should convert a date nanos value to text (not for CSV)', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'date_nanos', rowIndex: 0, options: { @@ -134,9 +134,9 @@ describe('convertValueToString', () => { it('should convert a boolean value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'bool_enabled', rowIndex: 0, options: { @@ -149,9 +149,9 @@ describe('convertValueToString', () => { it('should convert a binary value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'binary_blob', rowIndex: 0, options: { @@ -164,9 +164,9 @@ describe('convertValueToString', () => { it('should convert a binary value to text (not for CSV)', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'binary_blob', rowIndex: 0, options: { @@ -179,9 +179,9 @@ describe('convertValueToString', () => { it('should convert an object value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'object_user.first', rowIndex: 0, options: { @@ -194,9 +194,9 @@ describe('convertValueToString', () => { it('should convert a nested value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'nested_user', rowIndex: 0, options: { @@ -211,9 +211,9 @@ describe('convertValueToString', () => { it('should convert a flattened value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'flattened_labels', rowIndex: 0, options: { @@ -226,9 +226,9 @@ describe('convertValueToString', () => { it('should convert a range value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'range_time_frame', rowIndex: 0, options: { @@ -243,9 +243,9 @@ describe('convertValueToString', () => { it('should convert a rank features value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'rank_features', rowIndex: 0, options: { @@ -258,9 +258,9 @@ describe('convertValueToString', () => { it('should convert a histogram value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'histogram', rowIndex: 0, options: { @@ -273,9 +273,9 @@ describe('convertValueToString', () => { it('should convert a IP value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'ip_addr', rowIndex: 0, options: { @@ -288,9 +288,9 @@ describe('convertValueToString', () => { it('should convert a IP value to text (not for CSV)', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'ip_addr', rowIndex: 0, options: { @@ -303,9 +303,9 @@ describe('convertValueToString', () => { it('should convert a version value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'version', rowIndex: 0, options: { @@ -318,9 +318,9 @@ describe('convertValueToString', () => { it('should convert a version value to text (not for CSV)', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'version', rowIndex: 0, options: { @@ -333,9 +333,9 @@ describe('convertValueToString', () => { it('should convert a vector value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'vector', rowIndex: 0, options: { @@ -348,9 +348,9 @@ describe('convertValueToString', () => { it('should convert a geo point value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'geo_point', rowIndex: 0, options: { @@ -363,9 +363,9 @@ describe('convertValueToString', () => { it('should convert a geo point object value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'geo_point', rowIndex: 1, options: { @@ -378,9 +378,9 @@ describe('convertValueToString', () => { it('should convert an array value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'array_tags', rowIndex: 0, options: { @@ -393,9 +393,9 @@ describe('convertValueToString', () => { it('should convert a shape value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'geometry', rowIndex: 0, options: { @@ -410,9 +410,9 @@ describe('convertValueToString', () => { it('should convert a runtime value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'runtime_number', rowIndex: 0, options: { @@ -425,9 +425,9 @@ describe('convertValueToString', () => { it('should convert a scripted value to text', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'scripted_string', rowIndex: 0, options: { @@ -440,9 +440,9 @@ describe('convertValueToString', () => { it('should convert a scripted value to text (not for CSV)', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'scripted_string', rowIndex: 0, options: { @@ -455,9 +455,9 @@ describe('convertValueToString', () => { it('should return an empty string and not fail', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'unknown', rowIndex: 0, options: { @@ -470,9 +470,9 @@ describe('convertValueToString', () => { it('should return an empty string when rowIndex is out of range', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'unknown', rowIndex: -1, options: { @@ -485,9 +485,9 @@ describe('convertValueToString', () => { it('should return _source value', () => { const result = convertValueToString({ - rows: discoverGridContextMock.rows, - dataView: discoverGridContextMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextMock.rows, + dataView: dataTableContextMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: '_source', rowIndex: 0, options: { @@ -508,9 +508,9 @@ describe('convertValueToString', () => { it('should return a formatted _source value', () => { const result = convertValueToString({ - rows: discoverGridContextMock.rows, - dataView: discoverGridContextMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextMock.rows, + dataView: dataTableContextMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: '_source', rowIndex: 0, options: { @@ -525,9 +525,9 @@ describe('convertValueToString', () => { it('should escape formula', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'array_tags', rowIndex: 1, options: { @@ -539,9 +539,9 @@ describe('convertValueToString', () => { expect(result.withFormula).toBe(true); const result2 = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'scripted_string', rowIndex: 1, options: { @@ -555,9 +555,9 @@ describe('convertValueToString', () => { it('should not escape formulas when not for CSV', () => { const result = convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, columnId: 'array_tags', rowIndex: 1, options: { diff --git a/src/plugins/discover/public/utils/convert_value_to_string.ts b/packages/kbn-unified-data-table/src/utils/convert_value_to_string.ts similarity index 95% rename from src/plugins/discover/public/utils/convert_value_to_string.ts rename to packages/kbn-unified-data-table/src/utils/convert_value_to_string.ts index 605c7912b17f1..486ed5574dbf2 100644 --- a/src/plugins/discover/public/utils/convert_value_to_string.ts +++ b/packages/kbn-unified-data-table/src/utils/convert_value_to_string.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; import { cellHasFormulas, createEscapeValue } from '@kbn/data-plugin/common'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import { formatFieldValue } from '@kbn/discover-utils'; diff --git a/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx b/packages/kbn-unified-data-table/src/utils/copy_value_to_clipboard.test.tsx similarity index 77% rename from src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx rename to packages/kbn-unified-data-table/src/utils/copy_value_to_clipboard.test.tsx index 595b7601b6f65..7ff5c9b3f19b6 100644 --- a/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx +++ b/packages/kbn-unified-data-table/src/utils/copy_value_to_clipboard.test.tsx @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { discoverGridContextComplexMock } from '../__mocks__/grid_context'; -import { discoverServiceMock } from '../__mocks__/services'; +import { dataTableContextComplexMock } from '../../__mocks__/table_context'; +import { servicesMock } from '../../__mocks__/services'; import { copyValueToClipboard, copyColumnNameToClipboard, @@ -22,9 +22,9 @@ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); describe('copyValueToClipboard', () => { const valueToStringConverter: ValueToStringConverter = (rowIndex, columnId, options) => convertValueToString({ - rows: discoverGridContextComplexMock.rows, - dataView: discoverGridContextComplexMock.dataView, - fieldFormats: discoverServiceMock.fieldFormats, + rows: dataTableContextComplexMock.rows, + dataView: dataTableContextComplexMock.dataView, + fieldFormats: servicesMock.fieldFormats, rowIndex, columnId, options, @@ -39,6 +39,10 @@ describe('copyValueToClipboard', () => { }, writable: true, }); + Object.defineProperty(window, 'sessionStorage', { + value: { clear: jest.fn() }, + writable: true, + }); }); afterAll(() => { @@ -50,7 +54,7 @@ describe('copyValueToClipboard', () => { it('should copy a value to clipboard', () => { execCommandMock.mockImplementationOnce(() => true); const result = copyValueToClipboard({ - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, columnId: 'keyword_key', rowIndex: 0, valueToStringConverter, @@ -59,7 +63,7 @@ describe('copyValueToClipboard', () => { expect(result).toBe('abcd1'); expect(execCommandMock).toHaveBeenCalledWith('copy'); expect(warn).not.toHaveBeenCalled(); - expect(discoverServiceMock.toastNotifications.addInfo).toHaveBeenCalledWith({ + expect(servicesMock.toastNotifications.addInfo).toHaveBeenCalledWith({ title: 'Copied to clipboard', }); }); @@ -68,7 +72,7 @@ describe('copyValueToClipboard', () => { execCommandMock.mockImplementationOnce(() => false); const result = copyValueToClipboard({ - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, columnId: 'keyword_key', rowIndex: 0, valueToStringConverter, @@ -77,7 +81,7 @@ describe('copyValueToClipboard', () => { expect(result).toBe(null); expect(execCommandMock).toHaveBeenCalledWith('copy'); expect(warn).toHaveBeenCalledWith('Unable to copy to clipboard.'); - expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith({ + expect(servicesMock.toastNotifications.addWarning).toHaveBeenCalledWith({ title: 'Unable to copy to clipboard in this browser', }); }); @@ -85,13 +89,13 @@ describe('copyValueToClipboard', () => { it('should copy a column name to clipboard', () => { execCommandMock.mockImplementationOnce(() => true); const result = copyColumnNameToClipboard({ - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, columnDisplayName: 'text_message', }); expect(result).toBe('"text_message"'); expect(execCommandMock).toHaveBeenCalledWith('copy'); - expect(discoverServiceMock.toastNotifications.addInfo).toHaveBeenCalledWith({ + expect(servicesMock.toastNotifications.addInfo).toHaveBeenCalledWith({ title: 'Copied to clipboard', }); }); @@ -99,14 +103,14 @@ describe('copyValueToClipboard', () => { it('should inform when copy a column name to clipboard failed', () => { execCommandMock.mockImplementationOnce(() => false); const result = copyColumnNameToClipboard({ - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, columnDisplayName: 'text_message', }); expect(result).toBe(null); expect(execCommandMock).toHaveBeenCalledWith('copy'); expect(warn).toHaveBeenCalledWith('Unable to copy to clipboard.'); - expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith({ + expect(servicesMock.toastNotifications.addWarning).toHaveBeenCalledWith({ title: 'Unable to copy to clipboard in this browser', }); }); @@ -115,7 +119,7 @@ describe('copyValueToClipboard', () => { execCommandMock.mockImplementationOnce(() => true); const result = await copyColumnValuesToClipboard({ - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, columnId: 'bool_enabled', columnDisplayName: 'custom_bool_enabled', rowsCount: 2, @@ -126,7 +130,7 @@ describe('copyValueToClipboard', () => { expect(global.window.navigator.clipboard.writeText).toHaveBeenCalledWith( '"custom_bool_enabled"\nfalse\ntrue' ); - expect(discoverServiceMock.toastNotifications.addInfo).toHaveBeenCalledWith({ + expect(servicesMock.toastNotifications.addInfo).toHaveBeenCalledWith({ title: 'Values of "custom_bool_enabled" column copied to clipboard', }); }); @@ -134,7 +138,7 @@ describe('copyValueToClipboard', () => { it('should copy column values to clipboard with a warning', async () => { execCommandMock.mockImplementationOnce(() => true); const result = await copyColumnValuesToClipboard({ - toastNotifications: discoverServiceMock.toastNotifications, + toastNotifications: servicesMock.toastNotifications, columnId: 'scripted_string', columnDisplayName: 'custom_scripted_string', rowsCount: 2, @@ -142,7 +146,7 @@ describe('copyValueToClipboard', () => { }); expect(result).toBe('"custom_scripted_string"\n"hi there"\n"\'=1+2"";=1+2"'); - expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith({ + expect(servicesMock.toastNotifications.addWarning).toHaveBeenCalledWith({ title: 'Values of "custom_scripted_string" column copied to clipboard', text: 'Values may contain formulas that are escaped.', }); diff --git a/src/plugins/discover/public/utils/copy_value_to_clipboard.ts b/packages/kbn-unified-data-table/src/utils/copy_value_to_clipboard.ts similarity index 89% rename from src/plugins/discover/public/utils/copy_value_to_clipboard.ts rename to packages/kbn-unified-data-table/src/utils/copy_value_to_clipboard.ts index c700fa748f335..2e9620b42728b 100644 --- a/src/plugins/discover/public/utils/copy_value_to_clipboard.ts +++ b/packages/kbn-unified-data-table/src/utils/copy_value_to_clipboard.ts @@ -13,12 +13,12 @@ import type { ValueToStringConverter } from '../types'; import { convertNameToString } from './convert_value_to_string'; const WARNING_FOR_FORMULAS = i18n.translate( - 'discover.grid.copyEscapedValueWithFormulasToClipboardWarningText', + 'unifiedDataTable.copyEscapedValueWithFormulasToClipboardWarningText', { defaultMessage: 'Values may contain formulas that are escaped.', } ); -const COPY_FAILED_ERROR_MESSAGE = i18n.translate('discover.grid.copyFailedErrorText', { +const COPY_FAILED_ERROR_MESSAGE = i18n.translate('unifiedDataTable.copyFailedErrorText', { defaultMessage: 'Unable to copy to clipboard in this browser', }); @@ -46,7 +46,7 @@ export const copyValueToClipboard = ({ return null; } - const toastTitle = i18n.translate('discover.grid.copyValueToClipboard.toastTitle', { + const toastTitle = i18n.translate('unifiedDataTable.copyValueToClipboard.toastTitle', { defaultMessage: 'Copied to clipboard', }); @@ -105,7 +105,7 @@ export const copyColumnValuesToClipboard = async ({ return null; } - const toastTitle = i18n.translate('discover.grid.copyColumnValuesToClipboard.toastTitle', { + const toastTitle = i18n.translate('unifiedDataTable.copyColumnValuesToClipboard.toastTitle', { defaultMessage: 'Values of "{column}" column copied to clipboard', values: { column: columnDisplayName }, }); @@ -143,7 +143,7 @@ export const copyColumnNameToClipboard = ({ return null; } - const toastTitle = i18n.translate('discover.grid.copyColumnNameToClipboard.toastTitle', { + const toastTitle = i18n.translate('unifiedDataTable.copyColumnNameToClipboard.toastTitle', { defaultMessage: 'Copied to clipboard', }); diff --git a/src/plugins/discover/public/utils/get_field_capabilities.test.ts b/packages/kbn-unified-data-table/src/utils/get_field_capabilities.test.ts similarity index 100% rename from src/plugins/discover/public/utils/get_field_capabilities.test.ts rename to packages/kbn-unified-data-table/src/utils/get_field_capabilities.test.ts diff --git a/src/plugins/discover/public/utils/get_field_capabilities.ts b/packages/kbn-unified-data-table/src/utils/get_field_capabilities.ts similarity index 90% rename from src/plugins/discover/public/utils/get_field_capabilities.ts rename to packages/kbn-unified-data-table/src/utils/get_field_capabilities.ts index cd63c1c189e73..8374801ec311e 100644 --- a/src/plugins/discover/public/utils/get_field_capabilities.ts +++ b/packages/kbn-unified-data-table/src/utils/get_field_capabilities.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; export const getFieldCapabilities = (dataView: DataView, field: DataViewField) => { const isRuntimeField = Boolean(dataView.getFieldByName(field.name)?.runtimeField); diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx b/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx similarity index 76% rename from src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx rename to packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx index 229c730ea9dbd..941dccabf2474 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx +++ b/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx @@ -13,9 +13,38 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { getRenderCellValueFn } from './get_render_cell_value'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { CodeEditorProps, KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '@kbn/discover-utils'; import type { EsHitRecord } from '@kbn/discover-utils/types'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; + +jest.mock('@kbn/code-editor', () => { + const original = jest.requireActual('@kbn/code-editor'); + + const CodeEditorMock = (props: CodeEditorProps) => ( + + ); + + return { + ...original, + CodeEditor: CodeEditorMock, + }; +}); + +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + }; +}); const mockServices = { settings: { @@ -34,14 +63,6 @@ const mockServices = { }, }; -jest.mock('../../hooks/use_discover_services', () => { - const originalModule = jest.requireActual('../../hooks/use_discover_services'); - return { - ...originalModule, - useDiscoverServices: () => mockServices, - }; -}); - const rowsSource: EsHitRecord[] = [ { _id: '1', @@ -82,18 +103,19 @@ const rowsFieldsWithTopLevelObject: EsHitRecord[] = [ const build = (hit: EsHitRecord) => buildDataTableRecord(hit, dataViewMock); -describe('Discover grid cell rendering', function () { +describe('Unified data table cell rendering', function () { it('renders bytes column correctly', () => { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsSource.map(build), false, () => false, - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - ); expect(component.html()).toMatchInlineSnapshot( - `"100"` + `"100"` ); }); it('renders bytes column correctly using _source when details is true', () => { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsSource.map(build), false, () => false, - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - ); expect(component.html()).toMatchInlineSnapshot( - `"
100
"` + `"
100
"` ); }); it('renders bytes column correctly using fields when details is true', () => { const closePopoverMockFn = jest.fn(); - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFields.map(build), false, () => false, - 100, - closePopoverMockFn + closePopoverMockFn, + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = mountWithIntl( - ); expect(component.html()).toMatchInlineSnapshot( - `"
100
"` + `"
100
"` ); findTestSubject(component, 'docTableClosePopover').simulate('click'); expect(closePopoverMockFn).toHaveBeenCalledTimes(1); }); it('renders _source column correctly', () => { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsSource.map(build), false, (fieldName) => ['extension', 'bytes'].includes(fieldName), - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - @@ -191,7 +216,7 @@ describe('Discover grid cell rendering', function () { extension { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsSource.map(build), false, () => false, - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFields.map(build), true, (fieldName) => ['extension', 'bytes'].includes(fieldName), - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - @@ -340,7 +367,7 @@ describe('Discover grid cell rendering', function () { extension { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFields.map(build), true, (fieldName) => ['extension', 'bytes'].includes(fieldName), + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, // this is the number of rendered items - 1, - jest.fn() + 1 ); const component = shallow( - @@ -419,7 +447,7 @@ describe('Discover grid cell rendering', function () { extension { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFields.map(build), true, (fieldName) => false, - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFieldsWithTopLevelObject.map(build), true, (fieldName) => ['object.value', 'extension', 'bytes'].includes(fieldName), - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - @@ -577,7 +607,7 @@ describe('Discover grid cell rendering', function () { object.value { (dataViewMock.getFieldByName as jest.Mock).mockReturnValueOnce(undefined); - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFieldsWithTopLevelObject.map(build), true, (fieldName) => ['extension', 'bytes', 'object.value'].includes(fieldName), - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - @@ -619,7 +650,7 @@ describe('Discover grid cell rendering', function () { object.value { const closePopoverMockFn = jest.fn(); - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFieldsWithTopLevelObject.map(build), true, () => false, - 100, - closePopoverMockFn + closePopoverMockFn, + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - { const closePopoverMockFn = jest.fn(); - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFieldsWithTopLevelObject.map(build), true, () => false, - 100, - closePopoverMockFn + closePopoverMockFn, + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = mountWithIntl( - { (dataViewMock.getFieldByName as jest.Mock).mockReturnValueOnce(undefined); - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFieldsWithTopLevelObject.map(build), true, () => false, - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsSource.map(build), false, () => false, - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - ); expect(component.html()).toMatchInlineSnapshot( - `"-"` + `"-"` ); }); it('renders correctly when invalid column is given', () => { - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsSource.map(build), false, () => false, - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - ); expect(component.html()).toMatchInlineSnapshot( - `"-"` + `"-"` ); }); @@ -824,16 +860,17 @@ describe('Discover grid cell rendering', function () { }, }, ]; - const DiscoverGridCellValue = getRenderCellValueFn( + const DataTableCellValue = getRenderCellValueFn( dataViewMock, rowsFieldsUnmapped.map(build), true, (fieldName) => ['unmapped'].includes(fieldName), - 100, - jest.fn() + jest.fn(), + mockServices.fieldFormats as unknown as FieldFormatsStart, + 100 ); const component = shallow( - void + closePopover: () => void, + fieldFormats: FieldFormatsStart, + maxEntries: number, + externalCustomRenderers?: Record< + string, + (props: EuiDataGridCellValueElementProps) => React.ReactNode + > ) => - ({ rowIndex, columnId, isDetails, setCellProps }: EuiDataGridCellValueElementProps) => { - const { uiSettings, fieldFormats } = useDiscoverServices(); - - const maxEntries = useMemo(() => uiSettings.get(MAX_DOC_FIELDS_DISPLAYED), [uiSettings]); - + ({ + rowIndex, + columnId, + isDetails, + setCellProps, + colIndex, + isExpandable, + isExpanded, + }: EuiDataGridCellValueElementProps) => { + if (!!externalCustomRenderers && !!externalCustomRenderers[columnId]) { + return ( + <> + {externalCustomRenderers[columnId]({ + rowIndex, + columnId, + isDetails, + setCellProps, + isExpandable, + isExpanded, + colIndex, + })} + + ); + } const row = rows ? rows[rowIndex] : undefined; const field = dataView.fields.getByName(columnId); - const ctx = useContext(DiscoverGridContext); + const ctx = useContext(UnifiedDataTableContext); useEffect(() => { if (row?.isAnchor) { @@ -102,7 +125,7 @@ export const getRenderCellValueFn = const pairs = useTopLevelObjectColumns ? getTopLevelObjectPairs(row.raw, columnId, dataView, shouldShowFieldHandler).slice( 0, - maxDocFieldsDisplayed + maxEntries ) : formatHit(row, dataView, shouldShowFieldHandler, maxEntries, fieldFormats); @@ -110,13 +133,13 @@ export const getRenderCellValueFn = {pairs.map(([key, value]) => ( {key} @@ -144,7 +167,7 @@ export const getRenderCellValueFn = function getInnerColumns(fields: Record, columnId: string) { return Object.fromEntries( Object.entries(fields).filter(([key]) => { - return key.indexOf(`${columnId}.`) === 0; + return key.startsWith(`${columnId}.`); }) ); } @@ -178,7 +201,7 @@ function renderPopoverContent({ }) { const closeButton = ( @@ -216,7 +239,7 @@ function renderPopoverContent({ String(v) }; - const formatted = (values as unknown[]) + const formatted = values .map((val: unknown) => formatter.convert(val, 'html', { field: subField, diff --git a/src/plugins/discover/public/utils/popularize_field.test.ts b/packages/kbn-unified-data-table/src/utils/popularize_field.test.ts similarity index 100% rename from src/plugins/discover/public/utils/popularize_field.test.ts rename to packages/kbn-unified-data-table/src/utils/popularize_field.test.ts diff --git a/src/plugins/discover/public/utils/popularize_field.ts b/packages/kbn-unified-data-table/src/utils/popularize_field.ts similarity index 89% rename from src/plugins/discover/public/utils/popularize_field.ts rename to packages/kbn-unified-data-table/src/utils/popularize_field.ts index 9ab711be49266..3feca3fd3d4e7 100644 --- a/src/plugins/discover/public/utils/popularize_field.ts +++ b/packages/kbn-unified-data-table/src/utils/popularize_field.ts @@ -7,8 +7,8 @@ */ import type { Capabilities } from '@kbn/core/public'; -import { DataViewsContract } from '@kbn/data-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/public'; +import type { DataViewsContract } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; async function popularizeField( dataView: DataView, diff --git a/src/plugins/discover/public/utils/row_heights.ts b/packages/kbn-unified-data-table/src/utils/row_heights.ts similarity index 70% rename from src/plugins/discover/public/utils/row_heights.ts rename to packages/kbn-unified-data-table/src/utils/row_heights.ts index f1e096b76b9f9..45f472286d030 100644 --- a/src/plugins/discover/public/utils/row_heights.ts +++ b/packages/kbn-unified-data-table/src/utils/row_heights.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Storage } from '@kbn/kibana-utils-plugin/public'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import { isValidRowHeight } from './validate_row_height'; export interface DataGridOptionsRecord { @@ -14,10 +14,13 @@ export interface DataGridOptionsRecord { previousConfigRowHeight: number; } -const ROW_HEIGHT_KEY = 'discover:dataGridRowHeight'; +const getRowHeightKey = (consumer: string) => `${consumer}:dataGridRowHeight`; -export const getStoredRowHeight = (storage: Storage): DataGridOptionsRecord | null => { - const entry = storage.get(ROW_HEIGHT_KEY); +export const getStoredRowHeight = ( + storage: Storage, + consumer: string +): DataGridOptionsRecord | null => { + const entry = storage.get(getRowHeightKey(consumer)); if ( typeof entry === 'object' && entry !== null && @@ -32,9 +35,10 @@ export const getStoredRowHeight = (storage: Storage): DataGridOptionsRecord | nu export const updateStoredRowHeight = ( newRowHeight: number, configRowHeight: number, - storage: Storage + storage: Storage, + consumer: string ) => { - storage.set(ROW_HEIGHT_KEY, { + storage.set(getRowHeightKey(consumer), { previousRowHeight: newRowHeight, previousConfigRowHeight: configRowHeight, }); diff --git a/src/plugins/discover/public/utils/rows_per_page.test.ts b/packages/kbn-unified-data-table/src/utils/rows_per_page.test.ts similarity index 58% rename from src/plugins/discover/public/utils/rows_per_page.test.ts rename to packages/kbn-unified-data-table/src/utils/rows_per_page.test.ts index 25eddf9a44de2..8da8ea099734b 100644 --- a/src/plugins/discover/public/utils/rows_per_page.test.ts +++ b/packages/kbn-unified-data-table/src/utils/rows_per_page.test.ts @@ -6,9 +6,7 @@ * Side Public License, v 1. */ -import { discoverServiceMock } from '../__mocks__/services'; -import { SAMPLE_ROWS_PER_PAGE_SETTING } from '@kbn/discover-utils'; -import { getRowsPerPageOptions, getDefaultRowsPerPage } from './rows_per_page'; +import { getRowsPerPageOptions } from './rows_per_page'; const SORTED_OPTIONS = [10, 25, 50, 100, 250, 500]; @@ -26,17 +24,4 @@ describe('rows per page', () => { expect(getRowsPerPageOptions(350)).toEqual([10, 25, 50, 100, 250, 350, 500]); }); }); - - describe('getDefaultRowsPerPage', () => { - it('should return a value from settings', () => { - expect(getDefaultRowsPerPage(discoverServiceMock.uiSettings)).toEqual(150); - expect(discoverServiceMock.uiSettings.get).toHaveBeenCalledWith(SAMPLE_ROWS_PER_PAGE_SETTING); - }); - - it('should return a default value', () => { - expect(getDefaultRowsPerPage({ ...discoverServiceMock.uiSettings, get: jest.fn() })).toEqual( - 100 - ); - }); - }); }); diff --git a/src/plugins/discover/public/utils/rows_per_page.ts b/packages/kbn-unified-data-table/src/utils/rows_per_page.ts similarity index 62% rename from src/plugins/discover/public/utils/rows_per_page.ts rename to packages/kbn-unified-data-table/src/utils/rows_per_page.ts index bc5f07f6253d3..2eb547df1a36f 100644 --- a/src/plugins/discover/public/utils/rows_per_page.ts +++ b/packages/kbn-unified-data-table/src/utils/rows_per_page.ts @@ -7,9 +7,8 @@ */ import { sortBy, uniq } from 'lodash'; -import { SAMPLE_ROWS_PER_PAGE_SETTING } from '@kbn/discover-utils'; -import { DEFAULT_ROWS_PER_PAGE, ROWS_PER_PAGE_OPTIONS } from '../../common/constants'; -import { DiscoverServices } from '../build_services'; + +import { ROWS_PER_PAGE_OPTIONS } from '../constants'; export const getRowsPerPageOptions = (currentRowsPerPage?: number): number[] => { return sortBy( @@ -20,7 +19,3 @@ export const getRowsPerPageOptions = (currentRowsPerPage?: number): number[] => ) ); }; - -export const getDefaultRowsPerPage = (uiSettings: DiscoverServices['uiSettings']): number => { - return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE; -}; diff --git a/src/plugins/discover/public/utils/validate_row_height.ts b/packages/kbn-unified-data-table/src/utils/validate_row_height.ts similarity index 100% rename from src/plugins/discover/public/utils/validate_row_height.ts rename to packages/kbn-unified-data-table/src/utils/validate_row_height.ts diff --git a/packages/kbn-unified-data-table/tsconfig.json b/packages/kbn-unified-data-table/tsconfig.json new file mode 100644 index 0000000000000..bed8d16a279b1 --- /dev/null +++ b/packages/kbn-unified-data-table/tsconfig.json @@ -0,0 +1,37 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": ["*.ts", "**/*.tsx", "src/**/*", "__mocks__/**/*.ts"], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/i18n", + "@kbn/data-views-plugin", + "@kbn/unified-doc-viewer", + "@kbn/discover-utils", + "@kbn/kibana-utils-plugin", + "@kbn/expressions-plugin", + "@kbn/test-jest-helpers", + "@kbn/i18n-react", + "@kbn/ui-theme", + "@kbn/field-types", + "@kbn/kibana-utils-plugin", + "@kbn/cell-actions", + "@kbn/utility-types", + "@kbn/data-view-field-editor-plugin", + "@kbn/field-formats-plugin", + "@kbn/react-kibana-context-common", + "@kbn/data-plugin", + "@kbn/core", + "@kbn/ui-actions-plugin", + "@kbn/charts-plugin", + "@kbn/kibana-react-plugin", + "@kbn/monaco", + "@kbn/code-editor", + "@kbn/config", + "@kbn/monaco", + ] +} diff --git a/packages/kbn-unified-doc-viewer/README.md b/packages/kbn-unified-doc-viewer/README.md new file mode 100644 index 0000000000000..825f6eee9f910 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/README.md @@ -0,0 +1,15 @@ +# @kbn/unified-doc-viewer + +This package contains components and services for the unified doc viewer component. + +Discover (Classic view → Expanded document) + +![image](https://github.com/elastic/kibana/assets/1178348/a0a360bf-2697-4427-a32e-c728f06f5a7e) + +Discover (Document explorer → Toggle dialog with details) + +![image](https://github.com/elastic/kibana/assets/1178348/c9c11587-c53f-4bcd-8d48-aaceb64981ea) + +Discover (View single document) + +![image](https://github.com/elastic/kibana/assets/1178348/4a8ea694-d4f5-4c9c-9259-1212f0d50a18) diff --git a/packages/kbn-unified-doc-viewer/index.ts b/packages/kbn-unified-doc-viewer/index.ts new file mode 100644 index 0000000000000..47aae8b209ac3 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { DocViewer, DocViewsRegistry, ElasticRequestState, FieldName } from './src'; diff --git a/packages/kbn-unified-doc-viewer/jest.config.js b/packages/kbn-unified-doc-viewer/jest.config.js new file mode 100644 index 0000000000000..1abe8ad8e52e9 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-unified-doc-viewer'], +}; diff --git a/packages/kbn-unified-doc-viewer/kibana.jsonc b/packages/kbn-unified-doc-viewer/kibana.jsonc new file mode 100644 index 0000000000000..272c2ec69ce82 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/unified-doc-viewer", + "owner": "@elastic/kibana-data-discovery" +} diff --git a/packages/kbn-unified-doc-viewer/package.json b/packages/kbn-unified-doc-viewer/package.json new file mode 100644 index 0000000000000..c301255f057bc --- /dev/null +++ b/packages/kbn-unified-doc-viewer/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/unified-doc-viewer", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "sideEffects": false +} \ No newline at end of file diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap similarity index 95% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap rename to packages/kbn-unified-doc-viewer/src/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap index ad0ee0fa4dc95..83dde56a57228 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap +++ b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Render with 3 different tabs 1`] = ` +exports[` Render with 3 different tabs 1`] = `
', () => { + test('Render with 3 different tabs', () => { + const registry = new DocViewsRegistry(); + registry.addDocView({ order: 10, title: 'Render function', render: jest.fn() }); + registry.addDocView({ order: 20, title: 'React component', component: () =>
test
}); + // @ts-expect-error This should be invalid and will throw an error when rendering + registry.addDocView({ order: 30, title: 'Invalid doc view' }); + + const renderProps = { hit: {} } as DocViewRenderProps; + + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('Render with 1 tab displaying error message', () => { + function SomeComponent() { + // this is just a placeholder + return null; + } + + const registry = new DocViewsRegistry(); + registry.addDocView({ + order: 10, + title: 'React component', + component: SomeComponent, + }); + + const renderProps = { + hit: buildDataTableRecord({ _index: 't', _id: '1' }), + } as DocViewRenderProps; + const errorMsg = 'Catch me if you can!'; + + const wrapper = mount( + + ); + const error = new Error(errorMsg); + wrapper.find(SomeComponent).simulateError(error); + const errorMsgComponent = findTestSubject(wrapper, 'docViewerError'); + expect(errorMsgComponent.text()).toMatch(new RegExp(`${errorMsg}`)); + }); +}); diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.tsx b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx similarity index 57% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.tsx rename to packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx index c2532735641f9..901208f8b3988 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.tsx +++ b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx @@ -8,9 +8,12 @@ import React from 'react'; import { EuiTabbedContent } from '@elastic/eui'; -import { getDocViewsRegistry } from '../../../../kibana_services'; import { DocViewerTab } from './doc_viewer_tab'; -import { DocView, DocViewRenderProps } from '../../doc_views_types'; +import type { DocView, DocViewRenderProps } from '../../types'; + +export interface DocViewerProps extends DocViewRenderProps { + docViews: DocView[]; +} /** * Rendering tabs with different views of 1 Elasticsearch hit in Discover. @@ -18,26 +21,23 @@ import { DocView, DocViewRenderProps } from '../../doc_views_types'; * A view can contain a React `component`, or any JS framework by using * a `render` function. */ -export function DocViewer(renderProps: DocViewRenderProps) { - const docViewsRegistry = getDocViewsRegistry(); - const tabs = docViewsRegistry - .getDocViewsSorted(renderProps.hit) - .map(({ title, render, component }: DocView, idx: number) => { - return { - id: `kbn_doc_viewer_tab_${idx}`, - name: title, - content: ( - - ), - ['data-test-subj']: `docViewerTab-${idx}`, - }; - }); +export function DocViewer({ docViews, ...renderProps }: DocViewerProps) { + const tabs = docViews.map(({ title, render, component }: DocView, idx: number) => { + return { + id: `kbn_doc_viewer_tab_${idx}`, + name: title, + content: ( + + ), + ['data-test-subj']: `docViewerTab-${idx}`, + }; + }); if (!tabs.length) { // There there's a minimum of 2 tabs active in Discover. diff --git a/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_error.test.tsx b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_error.test.tsx new file mode 100644 index 0000000000000..e21d0d772ecb5 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_error.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { DocViewerError } from './doc_viewer_error'; + +test('DocViewerError should wrap error in boundary', () => { + const props = { + error: new Error('my error'), + }; + + expect(() => { + const wrapper = mount(); + const html = wrapper.html(); + expect(html).toContain('euiErrorBoundary'); + expect(html).toContain('my error'); + }).not.toThrowError(); +}); diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_error.tsx b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_error.tsx similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_error.tsx rename to packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_error.tsx diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.test.tsx b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_render_tab.test.tsx similarity index 93% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.test.tsx rename to packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_render_tab.test.tsx index 5c61938a9e830..1aa7372c65fb5 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.test.tsx +++ b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_render_tab.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { DocViewRenderTab } from './doc_viewer_render_tab'; -import { DocViewRenderProps } from '../../doc_views_types'; +import type { DocViewRenderProps } from '../../types'; test('Mounting and unmounting DocViewerRenderTab', () => { const unmountFn = jest.fn(); diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.tsx b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_render_tab.tsx similarity index 92% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.tsx rename to packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_render_tab.tsx index 257f40a9850a1..5471bb52104bc 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.tsx +++ b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_render_tab.tsx @@ -7,7 +7,7 @@ */ import React, { useRef, useEffect } from 'react'; -import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views_types'; +import type { DocViewRenderFn, DocViewRenderProps } from '../../types'; interface Props { render: DocViewRenderFn; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.test.tsx b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_tab.test.tsx similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.test.tsx rename to packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_tab.test.tsx diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.tsx b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_tab.tsx similarity index 94% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.tsx rename to packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_tab.tsx index 837ee910176b9..ce88120c1d196 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.tsx +++ b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer_tab.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { isEqual } from 'lodash'; import { DocViewRenderTab } from './doc_viewer_render_tab'; -import { DocViewerError } from './doc_viewer_render_error'; -import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views_types'; +import { DocViewerError } from './doc_viewer_error'; +import type { DocViewRenderFn, DocViewRenderProps } from '../../types'; interface Props { id: number; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/index.ts b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/index.ts similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer/index.ts rename to packages/kbn-unified-doc-viewer/src/components/doc_viewer/index.ts diff --git a/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap b/packages/kbn-unified-doc-viewer/src/components/field_name/__snapshots__/field_name.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap rename to packages/kbn-unified-doc-viewer/src/components/field_name/__snapshots__/field_name.test.tsx.snap diff --git a/src/plugins/discover/public/components/field_name/__stories__/field_name.stories.tsx b/packages/kbn-unified-doc-viewer/src/components/field_name/__stories__/field_name.stories.tsx similarity index 100% rename from src/plugins/discover/public/components/field_name/__stories__/field_name.stories.tsx rename to packages/kbn-unified-doc-viewer/src/components/field_name/__stories__/field_name.stories.tsx diff --git a/src/plugins/discover/public/components/field_name/field_name.scss b/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.scss similarity index 100% rename from src/plugins/discover/public/components/field_name/field_name.scss rename to packages/kbn-unified-doc-viewer/src/components/field_name/field_name.scss diff --git a/src/plugins/discover/public/components/field_name/field_name.test.tsx b/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.test.tsx similarity index 100% rename from src/plugins/discover/public/components/field_name/field_name.test.tsx rename to packages/kbn-unified-doc-viewer/src/components/field_name/field_name.test.tsx diff --git a/src/plugins/discover/public/components/field_name/field_name.tsx b/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx similarity index 85% rename from src/plugins/discover/public/components/field_name/field_name.tsx rename to packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx index b651cca0b7592..824de1da96dbb 100644 --- a/src/plugins/discover/public/components/field_name/field_name.tsx +++ b/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx @@ -12,8 +12,9 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiHighlight } from '@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { FieldIcon, FieldIconProps } from '@kbn/react-field'; -import { type DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public'; -import { getFieldTypeName } from '@kbn/unified-field-list/src/utils/field_types/get_field_type_name'; +import type { DataViewField } from '@kbn/data-views-plugin/public'; +import { getDataViewFieldSubtypeMulti } from '@kbn/es-query'; +import { getFieldTypeName } from '@kbn/discover-utils'; interface Props { fieldName: string; @@ -36,7 +37,7 @@ export function FieldName({ const displayName = fieldMapping && fieldMapping.displayName ? fieldMapping.displayName : fieldName; const tooltip = displayName !== fieldName ? `${fieldName} (${displayName})` : fieldName; - const subTypeMulti = fieldMapping && getFieldSubtypeMulti(fieldMapping.spec); + const subTypeMulti = fieldMapping && getDataViewFieldSubtypeMulti(fieldMapping.spec); const isMultiField = !!subTypeMulti?.multi; return ( @@ -62,7 +63,7 @@ export function FieldName({ position="top" delay="long" content={i18n.translate( - 'discover.fieldChooser.discoverField.multiFieldTooltipContent', + 'unifiedDocViewer.fieldChooser.discoverField.multiFieldTooltipContent', { defaultMessage: 'Multi-fields can have multiple values per field', } @@ -75,7 +76,7 @@ export function FieldName({ data-test-subj={`tableDocViewRow-${fieldName}-multifieldBadge`} > diff --git a/packages/kbn-unified-doc-viewer/src/components/field_name/index.ts b/packages/kbn-unified-doc-viewer/src/components/field_name/index.ts new file mode 100644 index 0000000000000..67a2edbe1b440 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/src/components/field_name/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './field_name'; diff --git a/packages/kbn-unified-doc-viewer/src/components/index.ts b/packages/kbn-unified-doc-viewer/src/components/index.ts new file mode 100644 index 0000000000000..0ab434a88fd44 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/src/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './doc_viewer'; +export * from './field_name'; diff --git a/packages/kbn-unified-doc-viewer/src/index.ts b/packages/kbn-unified-doc-viewer/src/index.ts new file mode 100644 index 0000000000000..ae2e0bc299be7 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './components'; +export * from './services'; diff --git a/src/plugins/discover/public/services/doc_views/doc_views_registry.ts b/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts similarity index 78% rename from src/plugins/discover/public/services/doc_views/doc_views_registry.ts rename to packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts index 9b6f9d2490d80..620c8bc3058de 100644 --- a/src/plugins/discover/public/services/doc_views/doc_views_registry.ts +++ b/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts @@ -7,7 +7,15 @@ */ import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { DocView, DocViewInput, DocViewInputFn } from './doc_views_types'; +import { DocView, DocViewInput, DocViewInputFn } from './types'; + +export enum ElasticRequestState { + Loading, + NotFound, + Found, + Error, + NotFoundDataView, +} export class DocViewsRegistry { private docViews: DocView[] = []; @@ -17,10 +25,10 @@ export class DocViewsRegistry { */ addDocView(docViewRaw: DocViewInput | DocViewInputFn) { const docView = typeof docViewRaw === 'function' ? docViewRaw() : docViewRaw; - if (typeof docView.shouldShow !== 'function') { - docView.shouldShow = () => true; - } - this.docViews.push(docView as DocView); + this.docViews.push({ + ...docView, + shouldShow: docView.shouldShow ?? (() => true), + }); } /** * Returns a sorted array of doc_views for rendering tabs diff --git a/packages/kbn-unified-doc-viewer/src/services/index.ts b/packages/kbn-unified-doc-viewer/src/services/index.ts new file mode 100644 index 0000000000000..c1094d0d7a348 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/src/services/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { DocViewsRegistry, ElasticRequestState } from './doc_views_registry'; diff --git a/src/plugins/discover/public/services/doc_views/doc_views_types.ts b/packages/kbn-unified-doc-viewer/src/services/types.ts similarity index 96% rename from src/plugins/discover/public/services/doc_views/doc_views_types.ts rename to packages/kbn-unified-doc-viewer/src/services/types.ts index 4fc94580ba29a..0cce5ea3ff813 100644 --- a/src/plugins/discover/public/services/doc_views/doc_views_types.ts +++ b/packages/kbn-unified-doc-viewer/src/services/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import type { AggregateQuery, Query } from '@kbn/es-query'; import type { DataTableRecord, IgnoredReason } from '@kbn/discover-utils/types'; diff --git a/packages/kbn-unified-doc-viewer/src/types.ts b/packages/kbn-unified-doc-viewer/src/types.ts new file mode 100644 index 0000000000000..569665fcc6d87 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/src/types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { + DocView, + DocViewFilterFn, + DocViewRenderFn, + DocViewRenderProps, + FieldRecordLegacy, +} from './services/types'; diff --git a/packages/kbn-unified-doc-viewer/tsconfig.json b/packages/kbn-unified-doc-viewer/tsconfig.json new file mode 100644 index 0000000000000..856fbe93d6b0f --- /dev/null +++ b/packages/kbn-unified-doc-viewer/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/discover-utils", + "@kbn/data-views-plugin", + "@kbn/es-query", + "@kbn/data-plugin", + "@kbn/i18n-react", + "@kbn/i18n", + "@kbn/react-field", + ] +} diff --git a/packages/kbn-unified-doc-viewer/types.ts b/packages/kbn-unified-doc-viewer/types.ts new file mode 100644 index 0000000000000..d4543a112d9e1 --- /dev/null +++ b/packages/kbn-unified-doc-viewer/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { ElasticRequestState } from '.'; +export type { DocViewFilterFn, DocViewRenderProps, FieldRecordLegacy } from './src/types'; diff --git a/packages/kbn-unified-field-list/index.ts b/packages/kbn-unified-field-list/index.ts index fed0bc2e736e2..ffee31c4642fc 100755 --- a/packages/kbn-unified-field-list/index.ts +++ b/packages/kbn-unified-field-list/index.ts @@ -48,7 +48,6 @@ export type { AddFieldFilterHandler, FieldListGroups, FieldsGroupDetails, - FieldTypeKnown, FieldListItem, GetCustomFieldType, RenderFieldItemParams, @@ -86,13 +85,7 @@ export { type QuerySubscriberParams, } from './src/hooks/use_query_subscriber'; -export { - getFieldTypeName, - getFieldTypeDescription, - KNOWN_FIELD_TYPES, - getFieldType, - getFieldIconType, -} from './src/utils/field_types'; +export { getFieldTypeDescription, getFieldType, getFieldIconType } from './src/utils/field_types'; export { UnifiedFieldListSidebarContainer, diff --git a/packages/kbn-unified-field-list/src/components/field_icon/field_icon.tsx b/packages/kbn-unified-field-list/src/components/field_icon/field_icon.tsx index 070716911706c..cd6fcb7588fdd 100644 --- a/packages/kbn-unified-field-list/src/components/field_icon/field_icon.tsx +++ b/packages/kbn-unified-field-list/src/components/field_icon/field_icon.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { FieldIcon as KbnFieldIcon, FieldIconProps as KbnFieldIconProps } from '@kbn/react-field'; -import { getFieldTypeName } from '../../utils/field_types'; +import { getFieldTypeName } from '@kbn/discover-utils'; export type FieldIconProps = KbnFieldIconProps; diff --git a/packages/kbn-unified-field-list/src/components/field_list_filters/field_type_filter.tsx b/packages/kbn-unified-field-list/src/components/field_list_filters/field_type_filter.tsx index 1afe4bac5f0ba..b1a6387143edd 100644 --- a/packages/kbn-unified-field-list/src/components/field_list_filters/field_type_filter.tsx +++ b/packages/kbn-unified-field-list/src/components/field_list_filters/field_type_filter.tsx @@ -32,15 +32,11 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { type DataViewField } from '@kbn/data-views-plugin/common'; +import type { FieldTypeKnown } from '@kbn/discover-utils/types'; +import { getFieldTypeName, isKnownFieldType, KNOWN_FIELD_TYPE_LIST } from '@kbn/discover-utils'; import { FieldIcon } from '../field_icon'; -import { - getFieldIconType, - getFieldTypeName, - getFieldTypeDescription, - isKnownFieldType, - KNOWN_FIELD_TYPE_LIST, -} from '../../utils/field_types'; -import type { FieldListItem, FieldTypeKnown, GetCustomFieldType } from '../../types'; +import { getFieldIconType, getFieldTypeDescription } from '../../utils/field_types'; +import type { FieldListItem, GetCustomFieldType } from '../../types'; const EQUAL_HEIGHT_OFFSET = 2; // to avoid changes in the header's height after "Clear all" button appears const popoverTitleStyle = css` diff --git a/packages/kbn-unified-field-list/src/hooks/use_field_filters.ts b/packages/kbn-unified-field-list/src/hooks/use_field_filters.ts index c3e08ff335602..0c04ace0911f6 100644 --- a/packages/kbn-unified-field-list/src/hooks/use_field_filters.ts +++ b/packages/kbn-unified-field-list/src/hooks/use_field_filters.ts @@ -10,8 +10,9 @@ import { useMemo, useState } from 'react'; import { htmlIdGenerator } from '@elastic/eui'; import { type DataViewField } from '@kbn/data-views-plugin/common'; import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { FieldTypeKnown } from '@kbn/discover-utils/types'; import { type FieldListFiltersProps } from '../components/field_list_filters'; -import { type FieldListItem, type FieldTypeKnown, GetCustomFieldType } from '../types'; +import { type FieldListItem, GetCustomFieldType } from '../types'; import { getFieldIconType } from '../utils/field_types'; import { fieldNameWildcardMatcher } from '../utils/field_name_wildcard_matcher'; diff --git a/packages/kbn-unified-field-list/src/types.ts b/packages/kbn-unified-field-list/src/types.ts index ab9c2af61171a..76997c73176b3 100755 --- a/packages/kbn-unified-field-list/src/types.ts +++ b/packages/kbn-unified-field-list/src/types.ts @@ -8,6 +8,7 @@ import type { DataViewField } from '@kbn/data-views-plugin/common'; import type { EuiButtonIconProps, EuiButtonProps } from '@elastic/eui'; +import type { FieldTypeKnown } from '@kbn/discover-utils/types'; export interface BucketedAggregation { buckets: Array<{ @@ -89,11 +90,6 @@ export type FieldListGroups = { [key in FieldsGroupNames]?: FieldsGroup; }; -export type FieldTypeKnown = Exclude< - DataViewField['timeSeriesMetric'] | DataViewField['type'], - undefined ->; - export type GetCustomFieldType = (field: T) => FieldTypeKnown; export interface RenderFieldItemParams { diff --git a/packages/kbn-unified-field-list/src/utils/field_types/get_field_icon_type.ts b/packages/kbn-unified-field-list/src/utils/field_types/get_field_icon_type.ts index 13ba84121085f..b03ad8cb1389e 100644 --- a/packages/kbn-unified-field-list/src/utils/field_types/get_field_icon_type.ts +++ b/packages/kbn-unified-field-list/src/utils/field_types/get_field_icon_type.ts @@ -7,9 +7,9 @@ */ import { type DataViewField } from '@kbn/data-views-plugin/common'; +import { isKnownFieldType } from '@kbn/discover-utils'; import type { FieldListItem, GetCustomFieldType } from '../../types'; import { getFieldType } from './get_field_type'; -import { isKnownFieldType } from './field_types'; /** * Returns an icon type for a field diff --git a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type.ts b/packages/kbn-unified-field-list/src/utils/field_types/get_field_type.ts index 3ff787188dd06..fc92301b8f8e3 100644 --- a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type.ts +++ b/packages/kbn-unified-field-list/src/utils/field_types/get_field_type.ts @@ -7,7 +7,8 @@ */ import { type DataViewField } from '@kbn/data-views-plugin/common'; -import type { FieldListItem, FieldTypeKnown } from '../../types'; +import type { FieldTypeKnown } from '@kbn/discover-utils/types'; +import type { FieldListItem } from '../../types'; /** * Returns a field type. Time series metric type will override the original field type. diff --git a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_description.test.ts b/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_description.test.ts index 26434008e34ea..8c9ed41e05287 100644 --- a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_description.test.ts +++ b/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_description.test.ts @@ -7,7 +7,7 @@ */ import { getFieldTypeDescription, UNKNOWN_FIELD_TYPE_DESC } from './get_field_type_description'; -import { KNOWN_FIELD_TYPES } from './field_types'; +import { KNOWN_FIELD_TYPES } from '@kbn/discover-utils'; describe('UnifiedFieldList getFieldTypeDescription()', () => { describe('known field types should be recognized', () => { diff --git a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_description.ts b/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_description.ts index cadb07e59eeb4..7f6f9b6e1d765 100644 --- a/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_description.ts +++ b/packages/kbn-unified-field-list/src/utils/field_types/get_field_type_description.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; -import { KNOWN_FIELD_TYPES } from './field_types'; +import { KNOWN_FIELD_TYPES } from '@kbn/discover-utils'; /** * A user-friendly description of an unknown field type diff --git a/packages/kbn-unified-field-list/src/utils/field_types/index.ts b/packages/kbn-unified-field-list/src/utils/field_types/index.ts index 732d98e63f8f9..59d09efdd19d6 100644 --- a/packages/kbn-unified-field-list/src/utils/field_types/index.ts +++ b/packages/kbn-unified-field-list/src/utils/field_types/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -export { KNOWN_FIELD_TYPES, KNOWN_FIELD_TYPE_LIST, isKnownFieldType } from './field_types'; -export { getFieldTypeName } from './get_field_type_name'; export { getFieldTypeDescription } from './get_field_type_description'; export { getFieldType } from './get_field_type'; export { getFieldIconType } from './get_field_icon_type'; diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 3aeaf04083c66..21ed1425a1cb2 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -151,3 +151,23 @@ export type ArrayElement = A extends ReadonlyArray ? T : never; export type WithRequiredProperty = Omit & { [Property in Key]-?: Type[Property]; }; + +// Recursive partial object type. inspired by EUI RecursivePartial +export type RecursivePartial = { + [P in keyof T]?: T[P] extends NonAny[] + ? T[P] + : T[P] extends readonly NonAny[] + ? T[P] + : T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : T[P] extends Set + ? Set> + : T[P] extends Map + ? Map> + : T[P] extends NonAny + ? T[P] + : RecursivePartial; +}; +type NonAny = number | boolean | string | symbol | null; diff --git a/packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap b/packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap index f86ba86999236..b8a983ce582f2 100644 --- a/packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap +++ b/packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap @@ -351,7 +351,7 @@ exports[` is rendered 1`] = ` = ({ value, onChange, width, + height = '100px', options, overrideEditorWillMount, editorDidMount, @@ -478,7 +479,7 @@ export const CodeEditor: React.FC = ({ onChange={onChange} width={isFullScreen ? '100vw' : width} // previously defaulted to height which defaulted to 100% but this makes it unviewable - height={isFullScreen ? '100vh' : '100px'} + height={isFullScreen ? '100vh' : height} editorWillMount={_editorWillMount} editorDidMount={_editorDidMount} options={{ diff --git a/renovate.json b/renovate.json index cab03197e4c42..7d6ccf6a22c88 100644 --- a/renovate.json +++ b/renovate.json @@ -278,6 +278,7 @@ { "groupName": "platform security modules", "matchPackageNames": [ + "css.escape", "node-forge", "formik", "@types/node-forge", @@ -610,4 +611,4 @@ "enabled": true } ] -} +} \ No newline at end of file diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 5027d0484fd16..911eecd45a9fb 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -44,6 +44,45 @@ const getBootstrapScript = (isDev) => { } }; +const setServerlessKibanaDevServiceAccountIfPossible = (get, set, opts) => { + const esHosts = [].concat( + get('elasticsearch.hosts', []), + opts.elasticsearch ? opts.elasticsearch.split(',') : [] + ); + + /* + * We only handle the service token if serverless ES is running locally. + * Example would be if the user is running SES in the cloud and KBN serverless + * locally, they would be expected to handle auth on their own and this token + * is likely invalid anyways. + */ + const isESlocalhost = esHosts.length + ? esHosts.some((hostUrl) => { + const parsedUrl = url.parse(hostUrl); + return ( + parsedUrl.hostname === 'localhost' || + parsedUrl.hostname === '127.0.0.1' || + parsedUrl.hostname === 'host.docker.internal' + ); + }) + : true; // default is localhost:9200 + + if (!opts.dev || !opts.serverless || !isESlocalhost) { + return; + } + + const DEV_UTILS_PATH = '@kbn/dev-utils'; + + if (!canRequire(DEV_UTILS_PATH)) { + return; + } + + // need dynamic require to exclude it from production build + // eslint-disable-next-line import/no-dynamic-require + const { kibanaDevServiceAccount } = require(DEV_UTILS_PATH); + set('elasticsearch.serviceAccountToken', kibanaDevServiceAccount.token); +}; + function pathCollector() { const paths = []; return function (path) { @@ -68,6 +107,10 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) { delete extraCliOptions.env; if (opts.dev) { + if (opts.serverless) { + setServerlessKibanaDevServiceAccountIfPossible(get, set, opts); + } + if (!has('elasticsearch.serviceAccountToken') && opts.devCredentials !== false) { if (!has('elasticsearch.username')) { set('elasticsearch.username', 'kibana_system'); @@ -98,7 +141,6 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) { ensureNotDefined('server.ssl.truststore.path'); ensureNotDefined('server.ssl.certificateAuthorities'); ensureNotDefined('elasticsearch.ssl.certificateAuthorities'); - const elasticsearchHosts = ( (customElasticsearchHosts.length > 0 && customElasticsearchHosts) || [ 'https://localhost:9200', diff --git a/src/core/server/integration_tests/config/check_dynamic_config.test.ts b/src/core/server/integration_tests/config/check_dynamic_config.test.ts new file mode 100644 index 0000000000000..7239f051f41e7 --- /dev/null +++ b/src/core/server/integration_tests/config/check_dynamic_config.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { set } from '@kbn/safer-lodash-set'; +import { Root } from '@kbn/core-root-server-internal'; +import { createRootWithCorePlugins } from '@kbn/core-test-helpers-kbn-server'; +import { PLUGIN_SYSTEM_ENABLE_ALL_PLUGINS_CONFIG_PATH } from '@kbn/core-plugins-server-internal/src/constants'; + +describe('checking migration metadata changes on all registered SO types', () => { + let root: Root; + + beforeAll(async () => { + const settings = { + logging: { + loggers: [{ name: 'root', level: 'info', appenders: ['console'] }], + }, + }; + + set(settings, PLUGIN_SYSTEM_ENABLE_ALL_PLUGINS_CONFIG_PATH, true); + + root = createRootWithCorePlugins(settings, { + basePath: false, + cache: false, + dev: true, + disableOptimizer: true, + silent: false, + dist: false, + oss: false, + runExamples: false, + watch: false, + }); + + await root.preboot(); + await root.setup(); + }); + + afterAll(async () => { + if (root) { + await root.shutdown(); + } + }); + + function getListOfDynamicConfigPaths(): string[] { + // eslint-disable-next-line dot-notation + return [...root['server']['configService']['dynamicPaths'].entries()] + .flatMap(([configPath, dynamicConfigKeys]) => { + return dynamicConfigKeys.map( + (dynamicConfigKey: string) => `${configPath}.${dynamicConfigKey}` + ); + }) + .sort(); + } + + /** + * This test is meant to fail when any setting is flagged as capable + * of dynamic configuration {@link PluginConfigDescriptor.dynamicConfig}. + * + * Please, add your settings to the list with a comment of why it's required to be dynamic. + * + * The intent is to trigger a code review from the Core and Security teams to discuss potential issues. + */ + test('detecting all the settings that have opted-in for dynamic in-memory updates', () => { + expect(getListOfDynamicConfigPaths()).toStrictEqual([ + // We need this for enriching our Perf tests with more valuable data regarding the steps of the test + // Also helpful in Cloud & Serverless testing because we can't control the labels in those offerings + 'telemetry.labels', + ]); + }); +}); diff --git a/src/core/server/integration_tests/http/router.test.ts b/src/core/server/integration_tests/http/router.test.ts index 743624f2f46b3..de5e10d1ee599 100644 --- a/src/core/server/integration_tests/http/router.test.ts +++ b/src/core/server/integration_tests/http/router.test.ts @@ -576,11 +576,19 @@ describe('Handler', () => { 'An internal server error occurred. Check Kibana server logs for details.' ); expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ Array [ - Array [ - [Error: unexpected error], - ], - ] + "500 Server Error - /", + Object { + "error": [Error: unexpected error], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, + ], + ] `); }); @@ -617,7 +625,15 @@ describe('Handler', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: Unauthorized], + "500 Server Error - /", + Object { + "error": [Error: Unauthorized], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); @@ -639,7 +655,15 @@ describe('Handler', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: Unexpected result from Route Handler. Expected KibanaResponse, but given: string.], + "500 Server Error - /", + Object { + "error": [Error: Unexpected result from Route Handler. Expected KibanaResponse, but given: string.], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); @@ -672,6 +696,22 @@ describe('Handler', () => { message: '[request query.page]: expected value of type [number] but got [string]', statusCode: 400, }); + + expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + "400 Bad Request - /", + Object { + "error": [Error: [request query.page]: expected value of type [number] but got [string]], + "http": Object { + "response": Object { + "status_code": 400, + }, + }, + }, + ], + ] + `); }); it('accept to receive an array payload', async () => { @@ -1145,7 +1185,15 @@ describe('Response factory', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: expected 'location' header to be set], + "500 Server Error - /", + Object { + "error": [Error: expected 'location' header to be set], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); @@ -1551,7 +1599,15 @@ describe('Response factory', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: Unexpected Http status code. Expected from 400 to 599, but given: 200], + "500 Server Error - /", + Object { + "error": [Error: Unexpected Http status code. Expected from 400 to 599, but given: 200], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); @@ -1620,7 +1676,15 @@ describe('Response factory', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: expected 'location' header to be set], + "500 Server Error - /", + Object { + "error": [Error: expected 'location' header to be set], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); @@ -1760,7 +1824,15 @@ describe('Response factory', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: expected error message to be provided], + "500 Server Error - /", + Object { + "error": [Error: expected error message to be provided], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); @@ -1786,7 +1858,15 @@ describe('Response factory', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: expected error message to be provided], + "500 Server Error - /", + Object { + "error": [Error: expected error message to be provided], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); @@ -1811,7 +1891,15 @@ describe('Response factory', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: options.statusCode is expected to be set. given options: undefined], + "500 Server Error - /", + Object { + "error": [Error: options.statusCode is expected to be set. given options: undefined], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); @@ -1836,7 +1924,15 @@ describe('Response factory', () => { expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: Unexpected Http status code. Expected from 100 to 599, but given: 20.], + "500 Server Error - /", + Object { + "error": [Error: Unexpected Http status code. Expected from 100 to 599, but given: 20.], + "http": Object { + "response": Object { + "status_code": 500, + }, + }, + }, ], ] `); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index 171d45a2c7537..6bea2a8d7f2e2 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -120,7 +120,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ml-module": "2225cbb4bd508ea5f69db4b848be9d8a74b60198", "ml-trained-model": "482195cefd6b04920e539d34d7356d22cb68e4f3", "monitoring-telemetry": "5d91bf75787d9d4dd2fae954d0b3f76d33d2e559", - "observability-onboarding-state": "c18631f47a0da568f12f859c9ab9d4ca73bdff7c", + "observability-onboarding-state": "b16064c516aac64ae699c737d7d10b6e199bfded", "osquery-manager-usage-metric": "983bcbc3b7dda0aad29b20907db233abba709bcc", "osquery-pack": "6ab4358ca4304a12dcfc1777c8135b75cffb4397", "osquery-pack-asset": "b14101d3172c4b60eb5404696881ce5275c84152", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/incompatible_cluster_routing_allocation.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/incompatible_cluster_routing_allocation.test.ts index b723ca1b62608..d663dc15e9a1c 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/incompatible_cluster_routing_allocation.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/incompatible_cluster_routing_allocation.test.ts @@ -125,7 +125,9 @@ describe('incompatible_cluster_routing_allocation', () => { await root.preboot(); await root.setup(); - root.start(); + root.start().catch(() => { + // Silent catch because the test might be done and call shutdown before starting is completed, causing unwanted thrown errors. + }); // Wait for the INIT -> INIT action retry await retryAsync( diff --git a/src/core/server/integration_tests/saved_objects/routes/update.test.ts b/src/core/server/integration_tests/saved_objects/routes/update.test.ts index c261584217a37..e121fa7a43e0d 100644 --- a/src/core/server/integration_tests/saved_objects/routes/update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/update.test.ts @@ -24,8 +24,8 @@ import { setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; const testTypes = [ - { name: 'index-pattern', hide: false }, - { name: 'hidden-type', hide: true }, + { name: 'index-pattern', hide: false }, // multi-namespace type + { name: 'hidden-type', hide: true }, // hidden { name: 'hidden-from-http', hide: false, hideFromHttpApis: true }, ]; @@ -117,7 +117,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { 'index-pattern', 'logstash-*', { title: 'Testing' }, - { version: 'foo' } + { version: 'foo', migrationVersionCompatibility: 'raw' } ); }); diff --git a/src/core/server/integration_tests/saved_objects/serverless/migrations/smoke.test.ts b/src/core/server/integration_tests/saved_objects/serverless/migrations/smoke.test.ts index 940ea4a160cb6..1d884706fc8ba 100644 --- a/src/core/server/integration_tests/saved_objects/serverless/migrations/smoke.test.ts +++ b/src/core/server/integration_tests/saved_objects/serverless/migrations/smoke.test.ts @@ -13,10 +13,7 @@ import { createTestServerlessInstances, } from '@kbn/core-test-helpers-kbn-server'; -/** - * Until we merge https://github.com/elastic/kibana/pull/162673 this test should remain skipped. - */ -describe.skip('smoke', () => { +describe('smoke', () => { let serverlessES: TestServerlessESUtils; let serverlessKibana: TestServerlessKibanaUtils; let root: TestServerlessKibanaUtils['root']; diff --git a/src/core/server/integration_tests/saved_objects/service/lib/repository_with_proxy.test.ts b/src/core/server/integration_tests/saved_objects/service/lib/repository_with_proxy.test.ts index db24bf5a10765..035da58c60fa1 100644 --- a/src/core/server/integration_tests/saved_objects/service/lib/repository_with_proxy.test.ts +++ b/src/core/server/integration_tests/saved_objects/service/lib/repository_with_proxy.test.ts @@ -30,6 +30,7 @@ import { declarePostPitRoute, declarePostUpdateByQueryRoute, declarePassthroughRoute, + declareIndexRoute, setProxyInterrupt, allCombinationsPermutations, } from './repository_with_proxy_utils'; @@ -113,6 +114,7 @@ describe('404s from proxies', () => { declarePostSearchRoute(hapiServer, esHostname, esPort, kbnIndexPath); declarePostPitRoute(hapiServer, esHostname, esPort, kbnIndexPath); declarePostUpdateByQueryRoute(hapiServer, esHostname, esPort, kbnIndexPath); + declareIndexRoute(hapiServer, esHostname, esPort, kbnIndexPath); }); // register index-agnostic routes @@ -396,7 +398,9 @@ describe('404s from proxies', () => { expect(genericNotFoundEsUnavailableError(myError, 'my_type', 'myTypeId1')); }); - it('returns an EsUnavailable error on `update` requests that are interrupted', async () => { + it('returns an EsUnavailable error on `update` requests that are interrupted during index', async () => { + setProxyInterrupt('update'); + let updateError; try { await repository.update('my_type', 'myTypeToUpdate', { @@ -406,9 +410,26 @@ describe('404s from proxies', () => { } catch (err) { updateError = err; } + expect(genericNotFoundEsUnavailableError(updateError)); }); + it('returns an EsUnavailable error on `update` requests that are interrupted during preflight', async () => { + setProxyInterrupt('updatePreflight'); + + let updateError; + try { + await repository.update('my_type', 'myTypeToUpdate', { + title: 'updated title', + }); + expect(false).toBe(true); // Should not get here (we expect the call to throw) + } catch (err) { + updateError = err; + } + + expect(genericNotFoundEsUnavailableError(updateError, 'my_type', 'myTypeToUpdate')); + }); + it('returns an EsUnavailable error on `bulkCreate` requests with a 404 proxy response and wrong product header', async () => { setProxyInterrupt('bulkCreate'); let bulkCreateError: any; diff --git a/src/core/server/integration_tests/saved_objects/service/lib/repository_with_proxy_utils.ts b/src/core/server/integration_tests/saved_objects/service/lib/repository_with_proxy_utils.ts index 35b6b37b9c413..6f40d19f609d9 100644 --- a/src/core/server/integration_tests/saved_objects/service/lib/repository_with_proxy_utils.ts +++ b/src/core/server/integration_tests/saved_objects/service/lib/repository_with_proxy_utils.ts @@ -27,6 +27,8 @@ export const setProxyInterrupt = ( | 'openPit' | 'deleteByNamespace' | 'internalBulkResolve' + | 'update' + | 'updatePreflight' | null ) => (proxyInterrupt = testArg); @@ -63,7 +65,11 @@ export const declareGetRoute = ( path: `/${kbnIndex}/_doc/{type*}`, options: { handler: (req, h) => { - if (req.params.type === 'my_type:myTypeId1' || req.params.type === 'my_type:myType_123') { + if ( + req.params.type === 'my_type:myTypeId1' || + req.params.type === 'my_type:myType_123' || + proxyInterrupt === 'updatePreflight' + ) { return proxyResponseHandler(h, hostname, port); } else { return relayHandler(h, hostname, port); @@ -257,6 +263,31 @@ export const declarePostUpdateByQueryRoute = ( }, }); +// PUT _doc +export const declareIndexRoute = ( + hapiServer: Hapi.Server, + hostname: string, + port: string, + kbnIndex: string +) => + hapiServer.route({ + method: ['PUT', 'POST'], + path: `/${kbnIndex}/_doc/{_id?}`, + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + if (proxyInterrupt === 'update') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); + // catch-all passthrough route export const declarePassthroughRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => hapiServer.route({ diff --git a/src/core/server/integration_tests/saved_objects/service/lib/update.test.ts b/src/core/server/integration_tests/saved_objects/service/lib/update.test.ts new file mode 100644 index 0000000000000..e2b7d91355d81 --- /dev/null +++ b/src/core/server/integration_tests/saved_objects/service/lib/update.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import fs from 'fs/promises'; +import { pick } from 'lodash'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { SavedObjectsType, SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server'; +import { type TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server'; +import '../../migrations/jest_matchers'; +import { + getKibanaMigratorTestKit, + startElasticsearch, +} from '../../migrations/kibana_migrator_test_kit'; +import { delay } from '../../migrations/test_utils'; +import { getBaseMigratorParams } from '../../migrations/fixtures/zdt_base.fixtures'; + +export const logFilePath = Path.join(__dirname, 'update.test.log'); + +describe('SOR - update API', () => { + let esServer: TestElasticsearchUtils['es']; + + beforeAll(async () => { + await fs.unlink(logFilePath).catch(() => {}); + esServer = await startElasticsearch(); + }); + + const getType = (version: 'v1' | 'v2'): SavedObjectsType => { + const versionMap: SavedObjectsModelVersionMap = { + 1: { + changes: [], + schemas: { + forwardCompatibility: (attributes) => { + return pick(attributes, 'count'); + }, + }, + }, + }; + + if (version === 'v2') { + versionMap[2] = { + changes: [ + { + type: 'data_backfill', + backfillFn: (document) => { + return { attributes: { even: document.attributes.count % 2 === 0 } }; + }, + }, + ], + }; + } + + return { + name: 'my-test-type', + hidden: false, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: { + count: { type: 'integer' }, + ...(version === 'v2' ? { even: { type: 'boolean' } } : {}), + }, + }, + management: { + importableAndExportable: true, + }, + switchToModelVersionAt: '8.10.0', + modelVersions: versionMap, + }; + }; + + afterAll(async () => { + await esServer?.stop(); + await delay(10); + }); + + const setup = async () => { + const { runMigrations: runMigrationV1, savedObjectsRepository: repositoryV1 } = + await getKibanaMigratorTestKit({ + ...getBaseMigratorParams(), + types: [getType('v1')], + }); + await runMigrationV1(); + + const { + runMigrations: runMigrationV2, + savedObjectsRepository: repositoryV2, + client: esClient, + } = await getKibanaMigratorTestKit({ + ...getBaseMigratorParams(), + types: [getType('v2')], + }); + await runMigrationV2(); + + return { repositoryV1, repositoryV2, esClient }; + }; + + it('supports updates between older and newer versions', async () => { + const { repositoryV1, repositoryV2, esClient } = await setup(); + + await repositoryV1.create('my-test-type', { count: 12 }, { id: 'my-id' }); + + let document = await repositoryV2.get('my-test-type', 'my-id'); + + expect(document.attributes).toEqual({ + count: 12, + even: true, + }); + + await repositoryV2.update('my-test-type', 'my-id', { + count: 11, + even: false, + }); + + document = await repositoryV1.get('my-test-type', 'my-id'); + + expect(document.attributes).toEqual({ + count: 11, + }); + + await repositoryV1.update('my-test-type', 'my-id', { + count: 14, + }); + + document = await repositoryV2.get('my-test-type', 'my-id'); + + expect(document.attributes).toEqual({ + count: 14, + even: true, + }); + + const rawDoc = await fetchDoc(esClient, 'my-test-type', 'my-id'); + expect(rawDoc._source).toEqual( + expect.objectContaining({ + typeMigrationVersion: '10.1.0', + 'my-test-type': { + count: 14, + }, + }) + ); + }); + + const fetchDoc = async (client: ElasticsearchClient, type: string, id: string) => { + return await client.get({ + index: '.kibana', + id: `${type}:${id}`, + }); + }; +}); diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 20b7911e4f782..8232e102ddab2 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -151,6 +151,7 @@ "@kbn/core-elasticsearch-client-server-internal", "@kbn/tooling-log", "@kbn/stdio-dev-helpers", + "@kbn/safer-lodash-set", ], "exclude": [ "target/**/*", diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile index a44fae27c4265..8bf470f489cb7 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile @@ -127,6 +127,9 @@ COPY --chown=1000:0 config/serverless.es.yml /usr/share/kibana/config/serverless COPY --chown=1000:0 config/serverless.oblt.yml /usr/share/kibana/config/serverless.oblt.yml COPY --chown=1000:0 config/serverless.security.yml /usr/share/kibana/config/serverless.security.yml {{/serverless}} +{{^opensslLegacyProvider}} +RUN sed 's/\(--openssl-legacy-provider\)/#\1/' -i config/node.options +{{/opensslLegacyProvider}} # Add the launcher/wrapper script. It knows how to interpret environment # variables and translate them to Kibana CLI options. diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts index ca597e5c38941..456a09ccc3db3 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts @@ -19,6 +19,7 @@ function generator(options: TemplateContext) { packageManager: options.baseImage.includes('ubi') ? 'microdnf' : 'apt-get', ubi: options.baseImage.includes('ubi'), ubuntu: options.baseImage === 'ubuntu', + opensslLegacyProvider: !(options.cloud || options.serverless), ...options, }); } diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index b02009cd155d2..fea9d8629f382 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -27,7 +27,7 @@ export const storybookAliases = { dashboard: 'src/plugins/dashboard/.storybook', data: 'src/plugins/data/.storybook', discover: 'src/plugins/discover/.storybook', - discover_log_explorer: 'x-pack/plugins/discover_log_explorer/.storybook', + log_explorer: 'x-pack/plugins/log_explorer/.storybook', embeddable: 'src/plugins/embeddable/.storybook', es_ui_shared: 'src/plugins/es_ui_shared/.storybook', expression_error: 'src/plugins/expression_error/.storybook', diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index 97e5979766089..894ea706cccc0 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -762,24 +762,20 @@ export const HeatmapComponent: FC = memo( xAxisTitle={args.gridConfig.isXAxisTitleVisible ? xAxisTitle : undefined} yAxisTitle={args.gridConfig.isYAxisTitleVisible ? yAxisTitle : undefined} xAxisLabelFormatter={(v) => - args.gridConfig.isXAxisLabelVisible - ? `${ - xAccessor && formattedTable.formattedColumns[xAccessor] - ? v - : xValuesFormatter.convert(v) - }` - : '' + `${ + xAccessor && formattedTable.formattedColumns[xAccessor] + ? v + : xValuesFormatter.convert(v) + }` } yAxisLabelFormatter={ yAxisColumn ? (v) => - args.gridConfig.isYAxisLabelVisible - ? `${ - yAccessor && formattedTable.formattedColumns[yAccessor] - ? v - : yValuesFormatter.convert(v) ?? '' - }` - : '' + `${ + yAccessor && formattedTable.formattedColumns[yAccessor] + ? v + : yValuesFormatter.convert(v) ?? '' + }` : undefined } /> diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index 970847ac6bedc..04634e635ac12 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -1077,11 +1077,7 @@ export default function ({ tracer('has started evaluating current token', currentToken); if (!currentToken) { - if (pos.lineNumber === 1) { - lastEvaluatedToken = null; - tracer('not starting autocomplete due to invalid current token at line 1'); - return; - } + lastEvaluatedToken = null; currentToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' }; // empty row } diff --git a/src/plugins/content_management/public/content_client/content_client.tsx b/src/plugins/content_management/public/content_client/content_client.tsx index e1d148b74760a..caf7181472f03 100644 --- a/src/plugins/content_management/public/content_client/content_client.tsx +++ b/src/plugins/content_management/public/content_client/content_client.tsx @@ -96,7 +96,7 @@ export class ContentClient { private readonly crudClientProvider: (contentType?: string) => CrudClient, private readonly contentTypeRegistry: ContentTypeRegistry ) { - this.queryClient = new QueryClient(); + this.queryClient = new QueryClient({ defaultOptions: { queries: { networkMode: 'always' } } }); this.queryOptionBuilder = createQueryOptionBuilder({ crudClientProvider: this.crudClientProvider, contentTypeRegistry: this.contentTypeRegistry, diff --git a/src/plugins/data/common/query/text_based_query_state_to_ast.test.ts b/src/plugins/data/common/query/text_based_query_state_to_ast.test.ts index 64f9c5ca59111..ee1e072e8a50f 100644 --- a/src/plugins/data/common/query/text_based_query_state_to_ast.test.ts +++ b/src/plugins/data/common/query/text_based_query_state_to_ast.test.ts @@ -54,4 +54,30 @@ describe('textBasedQueryStateToExpressionAst', () => { }) ); }); + + it('returns an object with the correct structure for an ES|QL query', async () => { + const actual = await textBasedQueryStateToExpressionAst({ + filters: [], + query: { sql: 'FROM foo' }, + time: { + from: 'now', + to: 'now+7d', + }, + }); + + expect(actual).toHaveProperty( + 'chain.1.arguments.timeRange.0.chain.0.arguments', + expect.objectContaining({ + from: ['now'], + to: ['now+7d'], + }) + ); + + expect(actual).toHaveProperty( + 'chain.2.arguments', + expect.objectContaining({ + query: ['FROM foo'], + }) + ); + }); }); diff --git a/src/plugins/data/common/query/text_based_query_state_to_ast.ts b/src/plugins/data/common/query/text_based_query_state_to_ast.ts index cb34d9c9c405c..e24cbbd0a7dab 100644 --- a/src/plugins/data/common/query/text_based_query_state_to_ast.ts +++ b/src/plugins/data/common/query/text_based_query_state_to_ast.ts @@ -49,12 +49,13 @@ export function textBasedQueryStateToExpressionAst({ if (query && isOfAggregateQueryType(query)) { const mode = getAggregateQueryMode(query); - // sql query - if (mode === 'sql' && 'sql' in query) { - const essql = aggregateQueryToAst(query, timeFieldName); + for (const esMode of ['sql', 'esql']) { + if (mode === esMode && esMode in query) { + const essql = aggregateQueryToAst(query, timeFieldName); - if (essql) { - ast.chain.push(essql); + if (essql) { + ast.chain.push(essql); + } } } } diff --git a/src/plugins/data/common/search/expressions/aggregate_query_to_ast.test.ts b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.test.ts index f292954feea82..0ded432eb0508 100644 --- a/src/plugins/data/common/search/expressions/aggregate_query_to_ast.test.ts +++ b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.test.ts @@ -10,14 +10,14 @@ import { aggregateQueryToAst } from './aggregate_query_to_ast'; describe('aggregateQueryToAst', () => { it('should return a function', () => { - expect(aggregateQueryToAst({ sql: 'SELECT * from foo' })).toHaveProperty('type', 'function'); + expect(aggregateQueryToAst({ esql: 'from foo' })).toHaveProperty('type', 'function'); }); it('should forward arguments', () => { - expect(aggregateQueryToAst({ sql: 'SELECT * from foo' }, 'baz')).toHaveProperty( + expect(aggregateQueryToAst({ esql: 'from foo' }, 'baz')).toHaveProperty( 'arguments', expect.objectContaining({ - query: ['SELECT * from foo'], + query: ['from foo'], timeField: ['baz'], }) ); diff --git a/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts index 84e1e4e5f2262..6b69af873585b 100644 --- a/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts +++ b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts @@ -5,10 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { i18n } from '@kbn/i18n'; import { buildExpressionFunction, ExpressionAstFunction } from '@kbn/expressions-plugin/common'; import { AggregateQuery } from '../../query'; import { EssqlExpressionFunctionDefinition } from './essql'; +import { EsqlExpressionFunctionDefinition } from './esql'; export const aggregateQueryToAst = ( query: AggregateQuery, @@ -20,4 +21,11 @@ export const aggregateQueryToAst = ( timeField, }).toAst(); } + if ('esql' in query) { + return buildExpressionFunction('esql', { + query: query.esql, + timeField, + locale: i18n.getLocale(), + }).toAst(); + } }; diff --git a/src/plugins/data/common/search/expressions/esql.ts b/src/plugins/data/common/search/expressions/esql.ts new file mode 100644 index 0000000000000..8ef0f49588303 --- /dev/null +++ b/src/plugins/data/common/search/expressions/esql.ts @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { KibanaRequest } from '@kbn/core/server'; +import { castEsToKbnFieldTypeName, ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; +import { i18n } from '@kbn/i18n'; +import type { + Datatable, + DatatableColumnType, + ExpressionFunctionDefinition, +} from '@kbn/expressions-plugin/common'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; + +import { zipObject } from 'lodash'; +import { Observable, defer, throwError } from 'rxjs'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; +import { buildEsQuery } from '@kbn/es-query'; +import { getEsQueryConfig } from '../../es_query'; +import { getTime } from '../../query'; +import { ESQL_SEARCH_STRATEGY, IKibanaSearchRequest, ISearchGeneric, KibanaContext } from '..'; +import { IKibanaSearchResponse } from '../types'; +import { UiSettingsCommon } from '../..'; + +type Input = KibanaContext | null; +type Output = Observable; + +interface Arguments { + query: string; + timezone?: string; + timeField?: string; + locale?: string; +} + +export type EsqlExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'esql', + Input, + Arguments, + Output +>; + +interface EsqlFnArguments { + getStartDependencies(getKibanaRequest: () => KibanaRequest): Promise; +} + +interface EsqlStartDependencies { + search: ISearchGeneric; + uiSettings: UiSettingsCommon; +} + +function normalizeType(type: string): DatatableColumnType { + switch (type) { + case ES_FIELD_TYPES._INDEX: + case ES_FIELD_TYPES.GEO_POINT: + case ES_FIELD_TYPES.IP: + return KBN_FIELD_TYPES.STRING; + case '_version': + return KBN_FIELD_TYPES.NUMBER; + case 'datetime': + return KBN_FIELD_TYPES.DATE; + default: + return castEsToKbnFieldTypeName(type) as DatatableColumnType; + } +} + +function sanitize(value: string) { + return value.replace(/[\(\)]/g, '_'); +} + +interface ESQLSearchParams { + time_zone?: string; + query: string; + filter?: unknown; + locale?: string; +} + +interface ESQLSearchReponse { + columns?: Array<{ + name: string; + type: string; + }>; + values: unknown[][]; +} + +export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { + const essql: EsqlExpressionFunctionDefinition = { + name: 'esql', + type: 'datatable', + inputTypes: ['kibana_context', 'null'], + help: i18n.translate('data.search.esql.help', { + defaultMessage: 'Queries Elasticsearch using ES|QL.', + }), + args: { + query: { + aliases: ['_', 'q'], + types: ['string'], + help: i18n.translate('data.search.esql.query.help', { + defaultMessage: 'An ES|QL query.', + }), + }, + timezone: { + aliases: ['tz'], + types: ['string'], + default: 'UTC', + help: i18n.translate('data.search.esql.timezone.help', { + defaultMessage: + 'The timezone to use for date operations. Valid ISO8601 formats and UTC offsets both work.', + }), + }, + timeField: { + aliases: ['timeField'], + types: ['string'], + help: i18n.translate('data.search.essql.timeField.help', { + defaultMessage: 'The time field to use in the time range filter set in the context.', + }), + }, + locale: { + aliases: ['locale'], + types: ['string'], + help: i18n.translate('data.search.essql.locale.help', { + defaultMessage: 'The locale to use.', + }), + }, + }, + fn( + input, + { query, timezone, timeField, locale }, + { abortSignal, inspectorAdapters, getKibanaRequest } + ) { + return defer(() => + getStartDependencies(() => { + const request = getKibanaRequest?.(); + if (!request) { + throw new Error( + 'A KibanaRequest is required to run queries on the server. ' + + 'Please provide a request object to the expression execution params.' + ); + } + + return request; + }) + ).pipe( + switchMap(({ search, uiSettings }) => { + const params: ESQLSearchParams = { + query, + time_zone: timezone, + locale, + }; + if (input) { + const esQueryConfigs = getEsQueryConfig( + uiSettings as Parameters[0] + ); + const timeFilter = + input.timeRange && + getTime(undefined, input.timeRange, { + fieldName: timeField, + }); + + params.filter = buildEsQuery( + undefined, + input.query || [], + [...(input.filters ?? []), ...(timeFilter ? [timeFilter] : [])], + esQueryConfigs + ); + } + + let startTime = Date.now(); + const logInspectorRequest = () => { + if (!inspectorAdapters.requests) { + inspectorAdapters.requests = new RequestAdapter(); + } + + const request = inspectorAdapters.requests.start( + i18n.translate('data.search.dataRequest.title', { + defaultMessage: 'Data', + }), + { + description: i18n.translate('data.search.es_search.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + }, + startTime + ); + startTime = Date.now(); + + return request; + }; + + return search< + IKibanaSearchRequest, + IKibanaSearchResponse + >({ params }, { abortSignal, strategy: ESQL_SEARCH_STRATEGY }).pipe( + catchError((error) => { + if (!error.err) { + error.message = `Unexpected error from Elasticsearch: ${error.message}`; + } else { + const { type, reason } = error.err.attributes; + if (type === 'parsing_exception') { + error.message = `Couldn't parse Elasticsearch ES|QL query. You may need to add backticks to names containing special characters. Check your query and try again. Error: ${reason}`; + } else { + error.message = `Unexpected error from Elasticsearch: ${type} - ${reason}`; + } + } + + return throwError(() => error); + }), + tap({ + next({ rawResponse }) { + logInspectorRequest() + .stats({ + hits: { + label: i18n.translate('data.search.es_search.hitsLabel', { + defaultMessage: 'Hits', + }), + value: `${rawResponse.values.length}`, + description: i18n.translate('data.search.es_search.hitsDescription', { + defaultMessage: 'The number of documents returned by the query.', + }), + }, + }) + .json(params) + .ok({ json: rawResponse }); + }, + error(error) { + logInspectorRequest().error({ json: error }); + }, + }) + ); + }), + map(({ rawResponse: body, warning }) => { + const columns = + body.columns?.map(({ name, type }) => ({ + id: sanitize(name), + name: sanitize(name), + meta: { type: normalizeType(type) }, + })) ?? []; + const columnNames = columns.map(({ name }) => name); + const rows = body.values.map((row) => zipObject(columnNames, row)); + + return { + type: 'datatable', + meta: { + type: 'es_ql', + }, + columns, + rows, + warning, + } as Datatable; + }) + ); + }, + }; + + return essql; +}; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index d0d103abe1ea2..50356da1a4655 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -18,3 +18,4 @@ export * from './strategies/es_search'; export * from './strategies/eql_search'; export * from './strategies/ese_search'; export * from './strategies/sql_search'; +export * from './strategies/esql_search'; diff --git a/src/plugins/data/common/search/poll_search.test.ts b/src/plugins/data/common/search/poll_search.test.ts index dbb8e5137de21..db39b5e187036 100644 --- a/src/plugins/data/common/search/poll_search.test.ts +++ b/src/plugins/data/common/search/poll_search.test.ts @@ -83,7 +83,7 @@ describe('pollSearch', () => { abortSignal: abortController.signal, }).toPromise(); - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 300)); abortController.abort(); await expect(poll).rejects.toThrow(AbortError); @@ -99,7 +99,7 @@ describe('pollSearch', () => { const cancelFn = jest.fn(); const subscription = pollSearch(searchFn, cancelFn).subscribe(() => {}); - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 300)); subscription.unsubscribe(); await new Promise((resolve) => setTimeout(resolve, 1000)); diff --git a/src/plugins/data/common/search/poll_search.ts b/src/plugins/data/common/search/poll_search.ts index a58fad3880c69..1a6819257ab67 100644 --- a/src/plugins/data/common/search/poll_search.ts +++ b/src/plugins/data/common/search/poll_search.ts @@ -22,6 +22,8 @@ export const pollSearch = ( else { // if static pollInterval is not provided, then use default back-off logic switch (true) { + case elapsedTime < 1500: + return 300; case elapsedTime < 5000: return 1000; case elapsedTime < 20000: diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 91eea3e50f881..b74b7e111b5ce 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -918,8 +918,12 @@ export class SearchSource { }; body.query = buildEsQuery(index, query, filters, esQueryConfigs); - // For testing shard failure messages in UI, uncomment the next block and switch to `kibana*` data view - // body.query = { + // For testing shard failure messages in the UI, follow these steps: + // 1. Add all three sample data sets (flights, ecommerce, logs) to Kibana. + // 2. Create a data view using the index pattern `kibana*` and don't use a timestamp field. + // 3. Uncomment the lines below, navigate to Discover, + // and switch to the data view created in step 2. + // body.query.bool.must.push({ // error_query: { // indices: [ // { @@ -930,7 +934,8 @@ export class SearchSource { // }, // ], // }, - // }; + // }); + // Alternatively you could also add this query via "Edit as Query DSL", then it needs no code to be changed if (highlightAll && body.query) { body.highlight = getHighlightRequest(getConfig(UI_SETTINGS.DOC_HIGHLIGHT)); diff --git a/src/plugins/data/common/search/strategies/esql_search/index.ts b/src/plugins/data/common/search/strategies/esql_search/index.ts new file mode 100644 index 0000000000000..12594660136d8 --- /dev/null +++ b/src/plugins/data/common/search/strategies/esql_search/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './types'; diff --git a/src/plugins/discover/public/application/doc/types.ts b/src/plugins/data/common/search/strategies/esql_search/types.ts similarity index 78% rename from src/plugins/discover/public/application/doc/types.ts rename to src/plugins/data/common/search/strategies/esql_search/types.ts index dd82e86cdb4b6..d71da852e55de 100644 --- a/src/plugins/discover/public/application/doc/types.ts +++ b/src/plugins/data/common/search/strategies/esql_search/types.ts @@ -6,10 +6,4 @@ * Side Public License, v 1. */ -export enum ElasticRequestState { - Loading, - NotFound, - Found, - Error, - NotFoundDataView, -} +export const ESQL_SEARCH_STRATEGY = 'esql'; diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index 688f65038aa11..03cd77653becb 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -55,7 +55,7 @@ export const searchConfigSchema = schema.object({ * Block and wait until the search is completed up to the timeout (see es async_search's `wait_for_completion_timeout`) * TODO: we should optimize this as 100ms is likely not optimal (https://github.com/elastic/kibana/issues/143277) */ - waitForCompletion: schema.duration({ defaultValue: '100ms' }), + waitForCompletion: schema.duration({ defaultValue: '200ms' }), /** * How long the async search needs to be available after each search poll. Ongoing async searches and any saved search results are deleted after this period. * (see es async_search's `keep_alive`) @@ -71,7 +71,7 @@ export const searchConfigSchema = schema.object({ * How long to wait before polling the async_search after the previous poll response. * If not provided, then default dynamic interval with backoff is used. */ - pollInterval: schema.maybe(schema.number({ min: 1000 })), + pollInterval: schema.maybe(schema.number({ min: 200 })), }), aggs: schema.object({ shardDelay: schema.object({ diff --git a/src/plugins/data/public/search/expressions/esql.ts b/src/plugins/data/public/search/expressions/esql.ts new file mode 100644 index 0000000000000..443d4c595c742 --- /dev/null +++ b/src/plugins/data/public/search/expressions/esql.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { StartServicesAccessor } from '@kbn/core/public'; +import { UiSettingsCommon } from '../../../common'; +import { DataPublicPluginStart, DataStartDependencies } from '../../types'; +import { getEsqlFn } from '../../../common/search/expressions/esql'; + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * @internal + */ +export function getEsql({ + getStartServices, +}: { + getStartServices: StartServicesAccessor; +}) { + return getEsqlFn({ + async getStartDependencies() { + const [ + { uiSettings }, + , + { + nowProvider, + search: { search }, + }, + ] = await getStartServices(); + + return { nowProvider, search, uiSettings: uiSettings as unknown as UiSettingsCommon }; + }, + }); +} diff --git a/src/plugins/data/public/search/expressions/index.ts b/src/plugins/data/public/search/expressions/index.ts index cb07be49e8a62..74e947e2942bb 100644 --- a/src/plugins/data/public/search/expressions/index.ts +++ b/src/plugins/data/public/search/expressions/index.ts @@ -9,5 +9,6 @@ export * from './esaggs'; export * from './esdsl'; export * from './essql'; +export * from './esql'; export * from '../../../common/search/expressions'; export * from './eql'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 4a16d9487d2ea..79229eaff91bf 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -63,7 +63,7 @@ import { NowProviderInternalContract } from '../now_provider'; import { DataPublicPluginStart, DataStartDependencies } from '../types'; import { AggsService } from './aggs'; import { createUsageCollector, SearchUsageCollector } from './collectors'; -import { getEql, getEsaggs, getEsdsl, getEssql } from './expressions'; +import { getEql, getEsaggs, getEsdsl, getEssql, getEsql } from './expressions'; import { handleWarnings } from './fetch/handle_warnings'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; @@ -173,6 +173,11 @@ export class SearchService implements Plugin { getStartServices: StartServicesAccessor; }) ); + expressions.registerFunction( + getEsql({ getStartServices } as { + getStartServices: StartServicesAccessor; + }) + ); expressions.registerFunction( getEql({ getStartServices } as { getStartServices: StartServicesAccessor; diff --git a/src/plugins/data/server/search/expressions/esql.ts b/src/plugins/data/server/search/expressions/esql.ts new file mode 100644 index 0000000000000..93a4e136d88df --- /dev/null +++ b/src/plugins/data/server/search/expressions/esql.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { StartServicesAccessor } from '@kbn/core/server'; +import { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { getEsqlFn } from '../../../common/search/expressions/esql'; + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * @internal + */ +export function getEsql({ + getStartServices, +}: { + getStartServices: StartServicesAccessor; +}) { + return getEsqlFn({ + getStartDependencies: async (getKibanaRequest) => { + const [{ savedObjects, uiSettings }, , { search }] = await getStartServices(); + const request = getKibanaRequest(); + const savedObjectsClient = savedObjects.getScopedClient(request); + + return { + search: search.asScoped(request).search, + uiSettings: uiSettings.asScopedToClient(savedObjectsClient), + }; + }, + }); +} diff --git a/src/plugins/data/server/search/expressions/index.ts b/src/plugins/data/server/search/expressions/index.ts index 98a08e6383f34..e9a9ff316d137 100644 --- a/src/plugins/data/server/search/expressions/index.ts +++ b/src/plugins/data/server/search/expressions/index.ts @@ -9,4 +9,5 @@ export * from './esaggs'; export * from './esdsl'; export * from './essql'; +export * from './esql'; export * from './eql'; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 825b7eee3302f..f2cdc40594487 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -10,6 +10,7 @@ export * from './types'; export * from './strategies/es_search'; export * from './strategies/ese_search'; export * from './strategies/eql_search'; +export * from './strategies/esql_search'; export type { SearchUsage } from './collectors/search'; export { usageProvider, searchUsageObserver } from './collectors/search'; export * from './aggs'; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index dd199403c53af..71a335ce51592 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -79,8 +79,9 @@ import { SearchSourceService, eqlRawResponse, SQL_SEARCH_STRATEGY, + ESQL_SEARCH_STRATEGY, } from '../../common/search'; -import { getEsaggs, getEsdsl, getEssql, getEql } from './expressions'; +import { getEsaggs, getEsdsl, getEssql, getEql, getEsql } from './expressions'; import { getShardDelayBucketAgg, SHARD_DELAY_AGG_NAME, @@ -95,6 +96,7 @@ import { NoSearchIdInSessionError } from './errors/no_search_id_in_session'; import { CachedUiSettingsClient } from './services'; import { sqlSearchStrategyProvider } from './strategies/sql_search'; import { searchSessionSavedObjectType } from './saved_objects'; +import { esqlSearchStrategyProvider } from './strategies/esql_search'; type StrategyMap = Record>; @@ -176,6 +178,7 @@ export class SearchService implements Plugin { usage ) ); + this.registerSearchStrategy(ESQL_SEARCH_STRATEGY, esqlSearchStrategyProvider(this.logger)); // We don't want to register this because we don't want the client to be able to access this // strategy, but we do want to expose it to other server-side plugins @@ -216,6 +219,7 @@ export class SearchService implements Plugin { expressions.registerFunction(getEsdsl({ getStartServices: core.getStartServices })); expressions.registerFunction(getEssql({ getStartServices: core.getStartServices })); expressions.registerFunction(getEql({ getStartServices: core.getStartServices })); + expressions.registerFunction(getEsql({ getStartServices: core.getStartServices })); expressions.registerFunction(cidrFunction); expressions.registerFunction(dateRangeFunction); expressions.registerFunction(extendedBoundsFunction); diff --git a/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts b/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts new file mode 100644 index 0000000000000..7f3f6f521853d --- /dev/null +++ b/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { from } from 'rxjs'; +import type { Logger } from '@kbn/core/server'; +import { getKbnServerError, KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import type { ISearchStrategy } from '../../types'; + +export const esqlSearchStrategyProvider = ( + logger: Logger, + useInternalUser: boolean = false +): ISearchStrategy => ({ + /** + * @param request + * @param options + * @param deps + * @throws `KbnServerError` + * @returns `Observable>` + */ + search: (request, { abortSignal, ...options }, { esClient, uiSettingsClient }) => { + // Only default index pattern type is supported here. + // See ese for other type support. + if (request.indexType) { + throw new KbnServerError(`Unsupported index pattern type ${request.indexType}`, 400); + } + + const search = async () => { + try { + const { terminateAfter, ...requestParams } = request.params ?? {}; + const { headers, body } = await esClient.asCurrentUser.transport.request( + { + method: 'POST', + path: '/_query', + body: { + ...requestParams, + }, + }, + { + signal: abortSignal, + meta: true, + } + ); + return { + rawResponse: body, + isPartial: false, + isRunning: false, + warning: headers?.warning, + }; + } catch (e) { + throw getKbnServerError(e); + } + }; + + return from(search()); + }, +}); diff --git a/src/plugins/data/server/search/strategies/esql_search/index.ts b/src/plugins/data/server/search/strategies/esql_search/index.ts new file mode 100644 index 0000000000000..14fd011acc3d8 --- /dev/null +++ b/src/plugins/data/server/search/strategies/esql_search/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { esqlSearchStrategyProvider } from './esql_search_strategy'; diff --git a/src/plugins/discover/common/constants.ts b/src/plugins/discover/common/constants.ts index 3cb696766b65f..e80f9d2449ebc 100644 --- a/src/plugins/discover/common/constants.ts +++ b/src/plugins/discover/common/constants.ts @@ -6,12 +6,18 @@ * Side Public License, v 1. */ -export const MAX_LOADED_GRID_ROWS = 10000; +import { SAMPLE_ROWS_PER_PAGE_SETTING } from '@kbn/discover-utils'; +import { IUiSettingsClient } from '@kbn/core/public'; + export const DEFAULT_ROWS_PER_PAGE = 100; export const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, DEFAULT_ROWS_PER_PAGE, 250, 500]; + export enum VIEW_MODE { DOCUMENT_LEVEL = 'documents', AGGREGATED_LEVEL = 'aggregated', } export const DISABLE_SHARD_FAILURE_WARNING = true; +export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => { + return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE; +}; diff --git a/src/plugins/discover/common/field_types.ts b/src/plugins/discover/common/field_types.ts deleted file mode 100644 index bd24797ab5323..0000000000000 --- a/src/plugins/discover/common/field_types.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export enum KNOWN_FIELD_TYPES { - BOOLEAN = 'boolean', - CONFLICT = 'conflict', - DATE = 'date', - DATE_RANGE = 'date_range', - GEO_POINT = 'geo_point', - GEO_SHAPE = 'geo_shape', - HISTOGRAM = 'histogram', - IP = 'ip', - IP_RANGE = 'ip_range', - KEYWORD = 'keyword', - MURMUR3 = 'murmur3', - NUMBER = 'number', - NESTED = 'nested', - STRING = 'string', - TEXT = 'text', - VERSION = 'version', -} diff --git a/src/plugins/discover/kibana.jsonc b/src/plugins/discover/kibana.jsonc index 3da4912dd5661..0778ebf666851 100644 --- a/src/plugins/discover/kibana.jsonc +++ b/src/plugins/discover/kibana.jsonc @@ -24,6 +24,7 @@ "dataViewFieldEditor", "dataViewEditor", "expressions", + "unifiedDocViewer", "unifiedSearch", "unifiedHistogram", "contentManagement" diff --git a/src/plugins/discover/public/__mocks__/saved_search.ts b/src/plugins/discover/public/__mocks__/saved_search.ts index c6751b2990bc4..90a2a9c680825 100644 --- a/src/plugins/discover/public/__mocks__/saved_search.ts +++ b/src/plugins/discover/public/__mocks__/saved_search.ts @@ -27,11 +27,11 @@ export const savedSearchMockWithTimeFieldNew = { searchSource: createSearchSourceMock({ index: dataViewWithTimefieldMock }), } as unknown as SavedSearch; -export const savedSearchMockWithSQL = { - id: 'the-saved-search-id-sql', +export const savedSearchMockWithESQL = { + id: 'the-saved-search-id-esql', searchSource: createSearchSourceMock({ index: dataViewWithTimefieldMock, - query: { sql: 'SELECT * FROM "the-saved-search-id-sql"' }, + query: { esql: 'FROM "the-saved-search-id-esql"' }, }), } as unknown as SavedSearch; diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index 6355d6de47e80..19a5058638392 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -18,17 +18,20 @@ import { generateFilters } from '@kbn/data-plugin/public'; import { i18n } from '@kbn/i18n'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import { removeInterceptedWarningDuplicates } from '@kbn/search-response-warnings'; -import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils'; +import { + DOC_TABLE_LEGACY, + SEARCH_FIELDS_FROM_SOURCE, + SORT_DEFAULT_ORDER_SETTING, +} from '@kbn/discover-utils'; +import { popularizeField, useColumns } from '@kbn/unified-data-table'; +import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { ContextErrorMessage } from './components/context_error_message'; import { LoadingStatus } from './services/context_query_state'; import { AppState, GlobalState, isEqualFilters } from './services/context_state'; -import { useColumns } from '../../hooks/use_data_grid_columns'; import { useContextAppState } from './hooks/use_context_app_state'; import { useContextAppFetch } from './hooks/use_context_app_fetch'; -import { popularizeField } from '../../utils/popularize_field'; import { ContextAppContent } from './context_app_content'; import { SurrDocType } from './services/context'; -import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; import { useDiscoverServices } from '../../hooks/use_discover_services'; import { setBreadcrumbs } from '../../utils/breadcrumbs'; @@ -68,7 +71,7 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) => const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useColumns({ capabilities, - config: uiSettings, + defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING), dataView, dataViews, useNewFieldsApi, diff --git a/src/plugins/discover/public/application/context/context_app_content.test.tsx b/src/plugins/discover/public/application/context/context_app_content.test.tsx index f7ce2235333d4..f6809d63c035d 100644 --- a/src/plugins/discover/public/application/context/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.test.tsx @@ -12,11 +12,11 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from './components/action_bar/action_bar'; import { GetStateReturn } from './services/context_state'; import { SortDirection } from '@kbn/data-plugin/public'; +import { UnifiedDataTable } from '@kbn/unified-data-table'; import { ContextAppContent, ContextAppContentProps } from './context_app_content'; import { LoadingStatus } from './services/context_query_state'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { discoverServiceMock } from '../../__mocks__/services'; -import { DiscoverGrid } from '../../components/discover_grid/discover_grid'; import { DocTableWrapper } from '../../components/doc_table/doc_table_wrapper'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '@kbn/discover-utils'; @@ -103,6 +103,6 @@ describe('ContextAppContent test', () => { it('should render discover grid correctly', async () => { const component = await mountComponent({ isLegacy: false }); - expect(component.find(DiscoverGrid).length).toBe(1); + expect(component.find(UnifiedDataTable).length).toBe(1); }); }); diff --git a/src/plugins/discover/public/application/context/context_app_content.tsx b/src/plugins/discover/public/application/context/context_app_content.tsx index c72b7f366e5ce..0443718be6e2b 100644 --- a/src/plugins/discover/public/application/context/context_app_content.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.tsx @@ -18,18 +18,25 @@ import { type SearchResponseInterceptedWarning, SearchResponseWarnings, } from '@kbn/search-response-warnings'; -import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '@kbn/discover-utils'; +import { + CONTEXT_STEP_SETTING, + DOC_HIDE_TIME_COLUMN_SETTING, + MAX_DOC_FIELDS_DISPLAYED, + ROW_HEIGHT_OPTION, + SHOW_MULTIFIELDS, +} from '@kbn/discover-utils'; +import { DataLoadingState, UnifiedDataTable } from '@kbn/unified-data-table'; +import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { getDefaultRowsPerPage } from '../../../common/constants'; import { LoadingStatus } from './services/context_query_state'; import { ActionBar } from './components/action_bar/action_bar'; -import { DataLoadingState, DiscoverGrid } from '../../components/discover_grid/discover_grid'; -import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; import { AppState } from './services/context_state'; import { SurrDocType } from './services/context'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './services/constants'; import { DocTableContext } from '../../components/doc_table/doc_table_context'; import { useDiscoverServices } from '../../hooks/use_discover_services'; -import { DiscoverGridFlyout } from '../../components/discover_grid/discover_grid_flyout'; -import { DocViewer } from '../../services/doc_views/components/doc_viewer'; +import { DiscoverGridFlyout } from '../../components/discover_grid_flyout'; +import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../components/discover_tour'; export interface ContextAppContentProps { columns: string[]; @@ -58,7 +65,7 @@ export function clamp(value: number) { return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE); } -const DiscoverGridMemoized = React.memo(DiscoverGrid); +const DiscoverGridMemoized = React.memo(UnifiedDataTable); const DocTableContextMemoized = React.memo(DocTableContext); const ActionBarMemoized = React.memo(ActionBar); @@ -122,6 +129,24 @@ export function ContextAppContent({ return [[dataView.timeFieldName!, SortDirection.desc]]; }, [dataView]); + const renderDocumentView = useCallback( + (hit: DataTableRecord, displayedRows: DataTableRecord[], displayedColumns: string[]) => ( + setExpandedDoc(undefined)} + setExpandedDoc={setExpandedDoc} + /> + ), + [addFilter, dataView, onAddColumn, onRemoveColumn] + ); + return ( {!!interceptedWarnings?.length && ( @@ -157,7 +182,6 @@ export function ContextAppContent({ sort={sort} useNewFieldsApi={useNewFieldsApi} dataTestSubj="contextDocTable" - DocViewer={DocViewer} /> )} {!isLegacy && ( @@ -176,14 +200,17 @@ export function ContextAppContent({ showTimeCol={showTimeCol} useNewFieldsApi={useNewFieldsApi} isPaginationEnabled={false} + rowsPerPageState={getDefaultRowsPerPage(services.uiSettings)} controlColumnIds={controlColumnIds} setExpandedDoc={setExpandedDoc} onFilter={addFilter} - onAddColumn={onAddColumn} - onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} - DocumentView={DiscoverGridFlyout} + configRowHeight={services.uiSettings.get(ROW_HEIGHT_OPTION)} + showMultiFields={services.uiSettings.get(SHOW_MULTIFIELDS)} + maxDocFieldsDisplayed={services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)} + renderDocumentView={renderDocumentView} services={services} + componentsTourSteps={{ expandButton: DISCOVER_TOUR_STEP_ANCHOR_IDS.expandDocument }} />
diff --git a/src/plugins/discover/public/application/doc/components/doc.test.tsx b/src/plugins/discover/public/application/doc/components/doc.test.tsx index dc93a5718e1e0..56eac62b0318c 100644 --- a/src/plugins/discover/public/application/doc/components/doc.test.tsx +++ b/src/plugins/discover/public/application/doc/components/doc.test.tsx @@ -16,29 +16,11 @@ import { Doc, DocProps } from './doc'; import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '@kbn/discover-utils'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { setUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/plugin'; +import { mockUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/__mocks__'; const mockSearchApi = jest.fn(); -jest.mock('../../../kibana_services', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let registry: any[] = []; - - return { - getDocViewsRegistry: () => ({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - addDocView(view: any) { - registry.push(view); - }, - getDocViewsSorted() { - return registry; - }, - resetRegistry: () => { - registry = []; - }, - }), - }; -}); - beforeEach(() => { jest.clearAllMocks(); }); @@ -86,6 +68,7 @@ async function mountDoc(update = false) { locator: { getUrl: jest.fn(() => Promise.resolve('mock-url')) }, chrome: { setBreadcrumbs: jest.fn() }, }; + setUnifiedDocViewerServices(mockUnifiedDocViewerServices); await act(async () => { comp = mountWithIntl( diff --git a/src/plugins/discover/public/application/doc/components/doc.tsx b/src/plugins/discover/public/application/doc/components/doc.tsx index 384d0d7a4ffac..83c2c08eafa2e 100644 --- a/src/plugins/discover/public/application/doc/components/doc.tsx +++ b/src/plugins/discover/public/application/doc/components/doc.tsx @@ -9,40 +9,18 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPage, EuiPageBody } from '@elastic/eui'; -import type { DataView } from '@kbn/data-views-plugin/public'; import { i18n } from '@kbn/i18n'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { ElasticRequestState } from '@kbn/unified-doc-viewer'; +import { UnifiedDocViewer, useEsDocSearch } from '@kbn/unified-doc-viewer-plugin/public'; +import type { EsDocSearchProps } from '@kbn/unified-doc-viewer-plugin/public/types'; import { setBreadcrumbs } from '../../../utils/breadcrumbs'; -import { DocViewer } from '../../../services/doc_views/components/doc_viewer'; -import { ElasticRequestState } from '../types'; -import { useEsDocSearch } from '../../../hooks/use_es_doc_search'; import { useDiscoverServices } from '../../../hooks/use_discover_services'; -export interface DocProps { - /** - * Id of the doc in ES - */ - id: string; - /** - * Index in ES to query - */ - index: string; - /** - * DataView entity - */ - dataView: DataView; - /** - * If set, will always request source, regardless of the global `fieldsFromSource` setting - */ - requestSource?: boolean; +export interface DocProps extends EsDocSearchProps { /** * Discover main view url */ referrer?: string; - /** - * Records fetched from text based query - */ - textBasedHits?: DataTableRecord[]; } export function Doc(props: DocProps) { @@ -141,7 +119,7 @@ export function Doc(props: DocProps) { {reqState === ElasticRequestState.Found && hit !== null && dataView && (
- +
)} diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/discover_layout.stories.tsx b/src/plugins/discover/public/application/main/components/layout/__stories__/discover_layout.stories.tsx index 80db81bb4228b..4e143c80d96db 100644 --- a/src/plugins/discover/public/application/main/components/layout/__stories__/discover_layout.stories.tsx +++ b/src/plugins/discover/public/application/main/components/layout/__stories__/discover_layout.stories.tsx @@ -68,7 +68,7 @@ storiesOf('components/layout/DiscoverLayout', module).add( ); storiesOf('components/layout/DiscoverLayout', module).add( - 'SQL view', + 'ES|QL view', withDiscoverServices(() => { const props = getPlainRecordLayoutProps(getDataViewMock(false)); return ( diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts index 8d95db014fdc8..d681365767ce8 100644 --- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts +++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts @@ -159,7 +159,7 @@ export const getPlainRecordLayoutProps = (dataView: DataView) => { columns: ['name', 'message', 'bytes'], sort: [['date', 'desc']], query: { - sql: 'SELECT * FROM "kibana_sample_data_ecommerce"', + esql: 'FROM "kibana_sample_data_ecommerce"', }, filters: [], }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 10c0ca87ce504..ec809e2c2f760 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -21,30 +21,36 @@ import { SortOrder } from '@kbn/saved-search-plugin/public'; import { CellActionsProvider } from '@kbn/cell-actions'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import { SearchResponseWarnings } from '@kbn/search-response-warnings'; +import { DataLoadingState, UnifiedDataTable, useColumns } from '@kbn/unified-data-table'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, HIDE_ANNOUNCEMENTS, + MAX_DOC_FIELDS_DISPLAYED, + ROW_HEIGHT_OPTION, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, + SHOW_MULTIFIELDS, + SORT_DEFAULT_ORDER_SETTING, } from '@kbn/discover-utils'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { getDefaultRowsPerPage } from '../../../../../common/constants'; import { useInternalStateSelector } from '../../services/discover_internal_state_container'; import { useAppStateSelector } from '../../services/discover_app_state_container'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; -import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; -import { DataLoadingState, DiscoverGrid } from '../../../../components/discover_grid/discover_grid'; import { FetchStatus } from '../../../types'; -import { useColumns } from '../../../../hooks/use_data_grid_columns'; import { RecordRawType } from '../../services/discover_data_state_container'; import { DiscoverStateContainer } from '../../services/discover_state'; import { useDataState } from '../../hooks/use_data_state'; import { DocTableInfinite } from '../../../../components/doc_table/doc_table_infinite'; import { DocumentExplorerCallout } from '../document_explorer_callout'; import { DocumentExplorerUpdateCallout } from '../document_explorer_callout/document_explorer_update_callout'; -import { DiscoverTourProvider } from '../../../../components/discover_tour'; +import { + DISCOVER_TOUR_STEP_ANCHOR_IDS, + DiscoverTourProvider, +} from '../../../../components/discover_tour'; import { getRawRecordType } from '../../utils/get_raw_record_type'; -import { DiscoverGridFlyout } from '../../../../components/discover_grid/discover_grid_flyout'; -import { DocViewer } from '../../../../services/doc_views/components/doc_viewer'; +import { DiscoverGridFlyout } from '../../../../components/discover_grid_flyout'; import { useSavedSearchInitial } from '../../services/discover_state_provider'; import { useFetchMoreRecords } from './use_fetch_more_records'; @@ -57,7 +63,7 @@ const progressStyle = css` `; const DocTableInfiniteMemoized = React.memo(DocTableInfinite); -const DiscoverGridMemoized = React.memo(DiscoverGrid); +const DataGridMemoized = React.memo(UnifiedDataTable); // export needs for testing export const onResize = ( @@ -148,7 +154,7 @@ function DiscoverDocumentsComponent({ onSetColumns, } = useColumns({ capabilities, - config: uiSettings, + defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING), dataView, dataViews, setAppState: stateContainer.appState.update, @@ -185,10 +191,31 @@ function DiscoverDocumentsComponent({ const showTimeCol = useMemo( () => - !isTextBasedQuery && + // for ES|QL we want to show the time column only when is on Document view + (!isTextBasedQuery || !columns?.length) && !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!dataView.timeFieldName, - [isTextBasedQuery, uiSettings, dataView.timeFieldName] + [isTextBasedQuery, columns, uiSettings, dataView.timeFieldName] + ); + + const renderDocumentView = useCallback( + (hit: DataTableRecord, displayedRows: DataTableRecord[], displayedColumns: string[]) => ( + setExpandedDoc(undefined)} + setExpandedDoc={setExpandedDoc} + query={query} + /> + ), + [dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc] ); if (isDataViewLoading || (isEmptyDataResult && isDataLoading)) { @@ -217,7 +244,7 @@ function DiscoverDocumentsComponent({ data-test-subj="dscInterceptedWarningsCallout" /> )} - {isLegacy && rows && rows.length && ( + {isLegacy && rows && rows.length > 0 && ( <> {!hideAnnouncements && } )} @@ -247,11 +273,11 @@ function DiscoverDocumentsComponent({ )} -
+
-
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 328153b6eeec5..58c23aa561e12 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -23,7 +23,13 @@ import classNames from 'classnames'; import { generateFilters } from '@kbn/data-plugin/public'; import { useDragDropContext } from '@kbn/dom-drag-drop'; import { DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; -import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS } from '@kbn/discover-utils'; +import { + SEARCH_FIELDS_FROM_SOURCE, + SHOW_FIELD_STATISTICS, + SORT_DEFAULT_ORDER_SETTING, +} from '@kbn/discover-utils'; +import { popularizeField, useColumns } from '@kbn/unified-data-table'; +import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { useSavedSearchInitial } from '../../services/discover_state_provider'; import { DiscoverStateContainer } from '../../services/discover_state'; import { VIEW_MODE } from '../../../../../common/constants'; @@ -34,13 +40,10 @@ import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverNoResults } from '../no_results'; import { LoadingSpinner } from '../loading_spinner/loading_spinner'; import { DiscoverSidebarResponsive } from '../sidebar'; -import { popularizeField } from '../../../../utils/popularize_field'; import { DiscoverTopNav } from '../top_nav/discover_topnav'; -import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; import { getResultState } from '../../utils/get_result_state'; import { DiscoverUninitialized } from '../uninitialized/uninitialized'; import { DataMainMsg, RecordRawType } from '../../services/discover_data_state_container'; -import { useColumns } from '../../../../hooks/use_data_grid_columns'; import { FetchStatus } from '../../../types'; import { useDataState } from '../../hooks/use_data_state'; import { getRawRecordType } from '../../utils/get_raw_record_type'; @@ -127,7 +130,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { onRemoveColumn, } = useColumns({ capabilities, - config: uiSettings, + defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING), dataView, dataViews, setAppState: stateContainer.appState.update, @@ -175,6 +178,13 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { }, [isSidebarClosed, storage]); const contentCentered = resultState === 'uninitialized' || resultState === 'none'; + const documentState = useDataState(stateContainer.dataState.data$.documents$); + + const textBasedLanguageModeWarning = useMemo(() => { + if (isPlainRecord) { + return documentState.textBasedHeaderWarning; + } + }, [documentState.textBasedHeaderWarning, isPlainRecord]); const textBasedLanguageModeErrors = useMemo(() => { if (isPlainRecord) { @@ -255,6 +265,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { updateQuery={stateContainer.actions.onUpdateQuery} isPlainRecord={isPlainRecord} textBasedLanguageModeErrors={textBasedLanguageModeErrors} + textBasedLanguageModeWarning={textBasedLanguageModeWarning} onFieldEdited={onFieldEdited} /> diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 655ff5cbbb04f..91fe00263c1c4 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -12,10 +12,10 @@ import React, { useCallback } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { VIEW_MODE } from '../../../../../common/constants'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; -import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; import { DiscoverStateContainer } from '../../services/discover_state'; import { FieldStatisticsTab } from '../field_stats_table'; import { DiscoverDocuments } from './discover_documents'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 6be8266691089..1f8ffa8b655ce 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -497,7 +497,7 @@ describe('discover responsive sidebar', function () { expect(findTestSubject(comp, 'dataView-add-field_btn').length).toBe(1); }); - it('should render correctly in the sql mode', async () => { + it('should render correctly in the ES|QL mode', async () => { const propsWithTextBasedMode = { ...props, columns: ['extension', 'bytes'], @@ -514,7 +514,7 @@ describe('discover responsive sidebar', function () { }) as DataDocuments$, }; const compInTextBasedMode = await mountComponent(propsWithTextBasedMode, { - query: { sql: 'SELECT * FROM `index`' }, + query: { esql: 'FROM `index`' }, }); await act(async () => { diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx index 66f566cff1673..a743d4c1abebd 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query'; import { DataViewType, type DataView } from '@kbn/data-views-plugin/public'; import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public'; -import { ENABLE_SQL } from '@kbn/discover-utils'; +import { ENABLE_ESQL } from '@kbn/discover-utils'; import { useSavedSearchInitial } from '../../services/discover_state_provider'; import { useInternalStateSelector } from '../../services/discover_internal_state_container'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; @@ -31,6 +31,7 @@ export interface DiscoverTopNavProps { stateContainer: DiscoverStateContainer; isPlainRecord: boolean; textBasedLanguageModeErrors?: Error; + textBasedLanguageModeWarning?: string; onFieldEdited: () => Promise; } @@ -42,6 +43,7 @@ export const DiscoverTopNav = ({ updateQuery, isPlainRecord, textBasedLanguageModeErrors, + textBasedLanguageModeWarning, onFieldEdited, }: DiscoverTopNavProps) => { const adHocDataViews = useInternalStateSelector((state) => state.adHocDataViews); @@ -164,10 +166,10 @@ export const DiscoverTopNav = ({ const setMenuMountPoint = useMemo(() => { return getHeaderActionMenuMounter(); }, []); - const isSQLModeEnabled = uiSettings.get(ENABLE_SQL); + const isESQLModeEnabled = uiSettings.get(ENABLE_ESQL); const supportedTextBasedLanguages = []; - if (isSQLModeEnabled) { - supportedTextBasedLanguages.push('SQL'); + if (isESQLModeEnabled) { + supportedTextBasedLanguages.push('ESQL'); } const dataViewPickerProps: DataViewPickerProps = { trigger: { @@ -233,6 +235,7 @@ export const DiscoverTopNav = ({ textBasedLanguageModeErrors={ textBasedLanguageModeErrors ? [textBasedLanguageModeErrors] : undefined } + textBasedLanguageModeWarning={textBasedLanguageModeWarning} onTextBasedSavedAndExit={onTextBasedSavedAndExit} prependFilterBar={ searchBarCustomization?.PrependFilterBar ? ( diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts index 44cf4c503be30..4e3e7068f883d 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts @@ -74,7 +74,7 @@ test('getTopNavLinks result', () => { `); }); -test('getTopNavLinks result for sql mode', () => { +test('getTopNavLinks result for ES|QL mode', () => { const topNavLinks = getTopNavLinks({ dataView: dataViewMock, onOpenInspector: jest.fn(), diff --git a/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.tsx b/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.tsx index 125d47c087640..edacd4ba3976e 100644 --- a/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.tsx +++ b/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.tsx @@ -50,7 +50,7 @@ function getHookProps( replaceUrlState, }; } -const query = { sql: 'SELECT * from the-data-view-title' }; +const query = { esql: 'from the-data-view-title' }; const msgComplete = { recordRawType: RecordRawType.PLAIN, fetchStatus: FetchStatus.PARTIAL, @@ -103,19 +103,16 @@ describe('useTextBasedQueryLanguage', () => { const { replaceUrlState, stateContainer } = renderHookWithContext(true); await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); - expect(replaceUrlState).toHaveBeenCalledWith({ index: 'the-data-view-id' }); - - replaceUrlState.mockReset(); - - stateContainer.dataState.data$.documents$.next(msgComplete); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); - await waitFor(() => { expect(replaceUrlState).toHaveBeenCalledWith({ index: 'the-data-view-id', - columns: ['field1', 'field2'], }); }); + + replaceUrlState.mockReset(); + + stateContainer.dataState.data$.documents$.next(msgComplete); + expect(replaceUrlState).toHaveBeenCalledTimes(0); }); test('should change viewMode to DOCUMENT_LEVEL if it was AGGREGATED_LEVEL', async () => { const { replaceUrlState } = renderHookWithContext(false, { @@ -132,7 +129,7 @@ describe('useTextBasedQueryLanguage', () => { const { replaceUrlState, stateContainer } = renderHookWithContext(false); const documents$ = stateContainer.dataState.data$.documents$; stateContainer.dataState.data$.documents$.next(msgComplete); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); replaceUrlState.mockReset(); documents$.next({ @@ -145,7 +142,7 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT field1 from the-data-view-title' }, + query: { esql: 'from the-data-view-title | keep field1' }, }); await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); @@ -162,7 +159,7 @@ describe('useTextBasedQueryLanguage', () => { const documents$ = stateContainer.dataState.data$.documents$; documents$.next(msgComplete); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); replaceUrlState.mockReset(); documents$.next({ @@ -175,7 +172,7 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT field1 from the-data-view-title' }, + query: { esql: 'from the-data-view-title | keep field1' }, }); await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); replaceUrlState.mockReset(); @@ -190,17 +187,17 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT field1 from the-data-view-title WHERE field1=1' }, + query: { esql: 'from the-data-view-title | keep field 1 | WHERE field1=1' }, }); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(0)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); }); test('if its not a text based query coming along, it should be ignored', async () => { const { replaceUrlState, stateContainer } = renderHookWithContext(false); const documents$ = stateContainer.dataState.data$.documents$; documents$.next(msgComplete); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); replaceUrlState.mockReset(); documents$.next({ @@ -225,7 +222,7 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT field1 from the-data-view-title WHERE field1=1' }, + query: { esql: 'from the-data-view-title | keep field 1 | WHERE field1=1' }, }); await waitFor(() => { @@ -253,7 +250,7 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT field1 from the-data-view-title WHERE field1=1' }, + query: { esql: 'from the-data-view-title | keep field 1 | WHERE field1=1' }, }); documents$.next({ @@ -266,9 +263,9 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT field1 from the-data-view-title' }, + query: { esql: 'from the-data-view-title | keep field1' }, }); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); expect(replaceUrlState).toHaveBeenCalledWith({ columns: ['field1'], }); @@ -284,9 +281,9 @@ describe('useTextBasedQueryLanguage', () => { documents$.next({ recordRawType: RecordRawType.PLAIN, fetchStatus: FetchStatus.LOADING, - query: { sql: 'SELECT * from the-data-view-title WHERE field1=2' }, + query: { esql: 'from the-data-view-title | WHERE field1=2' }, }); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); + expect(replaceUrlState).toHaveBeenCalledTimes(0); documents$.next({ recordRawType: RecordRawType.PLAIN, fetchStatus: FetchStatus.PARTIAL, @@ -297,9 +294,9 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT * from the-data-view-title WHERE field1=2' }, + query: { esql: 'from the-data-view-title | WHERE field1=2' }, }); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(2)); + expect(replaceUrlState).toHaveBeenCalledTimes(0); stateContainer.appState.getState = jest.fn(() => { return { columns: ['field1', 'field2'], index: 'the-data-view-id' }; }); @@ -308,7 +305,7 @@ describe('useTextBasedQueryLanguage', () => { documents$.next({ recordRawType: RecordRawType.PLAIN, fetchStatus: FetchStatus.LOADING, - query: { sql: 'SELECT field1; from the-data-view-title WHERE field1=2' }, + query: { esql: 'from the-data-view-title | keep field 1; | WHERE field1=2' }, }); documents$.next({ @@ -319,7 +316,7 @@ describe('useTextBasedQueryLanguage', () => { documents$.next({ recordRawType: RecordRawType.PLAIN, fetchStatus: FetchStatus.LOADING, - query: { sql: 'SELECT field1 from the-data-view-title' }, + query: { esql: 'from the-data-view-title | keep field1' }, }); documents$.next({ @@ -332,7 +329,7 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT field1 from the-data-view-title' }, + query: { esql: 'from the-data-view-title | keep field1' }, }); await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); @@ -350,7 +347,7 @@ describe('useTextBasedQueryLanguage', () => { renderHook(() => useTextBasedQueryLanguage(props), { wrapper: getHookContext(stateContainer) }); documents$.next(msgComplete); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); replaceUrlState.mockReset(); documents$.next({ @@ -363,7 +360,7 @@ describe('useTextBasedQueryLanguage', () => { flattened: { field1: 1 }, } as unknown as DataTableRecord, ], - query: { sql: 'SELECT field1 from the-data-view-*' }, + query: { esql: 'from the-data-view-* | keep field1' }, }); props.stateContainer.actions.setDataView(dataViewAdHoc); await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); diff --git a/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts b/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts index 4939049ceedda..fb3725d12b1e1 100644 --- a/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts +++ b/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts @@ -6,12 +6,7 @@ * Side Public License, v 1. */ import { isEqual } from 'lodash'; -import { - isOfAggregateQueryType, - getIndexPatternFromSQLQuery, - AggregateQuery, - Query, -} from '@kbn/es-query'; +import { isOfAggregateQueryType, getAggregateQueryMode } from '@kbn/es-query'; import { useCallback, useEffect, useRef } from 'react'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; @@ -21,6 +16,8 @@ import { getValidViewMode } from '../utils/get_valid_view_mode'; import { FetchStatus } from '../../types'; const MAX_NUM_OF_COLUMNS = 50; +// For ES|QL we want in case of the following commands to display a table view, otherwise display a document view +const TRANSFORMATIONAL_COMMANDS = ['stats', 'project', 'keep']; /** * Hook to take care of text based query language state transformations when a new result is returned @@ -33,11 +30,14 @@ export function useTextBasedQueryLanguage({ stateContainer: DiscoverStateContainer; dataViews: DataViewsContract; }) { - const prev = useRef<{ query: AggregateQuery | Query | undefined; columns: string[] }>({ + const prev = useRef<{ + query: string; + columns: string[]; + }>({ columns: [], - query: undefined, + query: '', }); - const indexTitle = useRef(''); + const initialFetch = useRef(true); const savedSearch = useSavedSearchInitial(); const cleanup = useCallback(() => { @@ -45,9 +45,8 @@ export function useTextBasedQueryLanguage({ // cleanup when it's not a text based query lang prev.current = { columns: [], - query: undefined, + query: '', }; - indexTitle.current = ''; } }, []); @@ -63,51 +62,60 @@ export function useTextBasedQueryLanguage({ fetchStatus: FetchStatus.COMPLETE, }); }; - const { columns: stateColumns, index, viewMode } = stateContainer.appState.getState(); + const { index, viewMode } = stateContainer.appState.getState(); let nextColumns: string[] = []; const isTextBasedQueryLang = - recordRawType === 'plain' && isOfAggregateQueryType(query) && 'sql' in query; + recordRawType === 'plain' && + isOfAggregateQueryType(query) && + ('sql' in query || 'esql' in query); const hasResults = Boolean(next.result?.length); - const initialFetch = !prev.current.columns.length; + let queryHasTransformationalCommands = 'sql' in query; + if ('esql' in query) { + TRANSFORMATIONAL_COMMANDS.forEach((command: string) => { + if (query.esql.toLowerCase().includes(command)) { + queryHasTransformationalCommands = true; + return; + } + }); + } if (isTextBasedQueryLang) { + const language = getAggregateQueryMode(query); if (next.fetchStatus !== FetchStatus.PARTIAL) { return; } + const dataViewObj = stateContainer.internalState.getState().dataView!; + if (hasResults) { // check if state needs to contain column transformation due to a different columns in the resultset const firstRow = next.result![0]; const firstRowColumns = Object.keys(firstRow.raw).slice(0, MAX_NUM_OF_COLUMNS); - if ( - !isEqual(firstRowColumns, prev.current.columns) && - !isEqual(query, prev.current.query) - ) { - prev.current = { columns: firstRowColumns, query }; + if (!queryHasTransformationalCommands) { + nextColumns = []; + initialFetch.current = false; + } else { nextColumns = firstRowColumns; - } - - if (firstRowColumns && initialFetch) { - prev.current = { columns: firstRowColumns, query }; + if ( + initialFetch.current && + !prev.current.columns.length && + Boolean(dataViewObj?.id === index) + ) { + prev.current.columns = firstRowColumns; + } } } - const indexPatternFromQuery = getIndexPatternFromSQLQuery(query.sql); - - const dataViewObj = stateContainer.internalState.getState().dataView!; - - // don't set the columns on initial fetch, to prevent overwriting existing state - const addColumnsToState = Boolean( - nextColumns.length && (!initialFetch || !stateColumns?.length) - ); + const addColumnsToState = !isEqual(nextColumns, prev.current.columns); + const queryChanged = query[language] !== prev.current.query; // no need to reset index to state if it hasn't changed - const addDataViewToState = Boolean(dataViewObj?.id !== index) || initialFetch; - const queryChanged = indexPatternFromQuery !== indexTitle.current; - if (!addColumnsToState && !queryChanged) { + const addDataViewToState = Boolean(dataViewObj?.id !== index); + if (!queryChanged || (!addDataViewToState && !addColumnsToState)) { sendComplete(); return; } if (queryChanged) { - indexTitle.current = indexPatternFromQuery; + prev.current.query = query[language]; + prev.current.columns = nextColumns; } const nextState = { ...(addDataViewToState && { index: dataViewObj.id }), diff --git a/src/plugins/discover/public/application/main/services/discover_app_state_container.ts b/src/plugins/discover/public/application/main/services/discover_app_state_container.ts index ea2d1d1f2324a..47cd216b1547e 100644 --- a/src/plugins/discover/public/application/main/services/discover_app_state_container.ts +++ b/src/plugins/discover/public/application/main/services/discover_app_state_container.ts @@ -23,12 +23,12 @@ import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { IKbnUrlStateStorage, ISyncStateRef, syncState } from '@kbn/kibana-utils-plugin/public'; import { isEqual } from 'lodash'; import { connectToQueryState, syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public'; +import type { UnifiedDataTableSettings } from '@kbn/unified-data-table'; import type { DiscoverServices } from '../../../build_services'; import { addLog } from '../../../utils/add_log'; import { cleanupUrlState } from '../utils/cleanup_url_state'; import { getStateDefaults } from '../utils/get_state_defaults'; import { handleSourceColumnState } from '../../../utils/state_helpers'; -import type { DiscoverGridSettings } from '../../../components/discover_grid/types'; export const APP_STATE_URL_KEY = '_a'; export interface DiscoverAppStateContainer extends ReduxLikeStateContainer { @@ -87,7 +87,7 @@ export interface DiscoverAppState { /** * Data Grid related state */ - grid?: DiscoverGridSettings; + grid?: UnifiedDataTableSettings; /** * Hide chart */ diff --git a/src/plugins/discover/public/application/main/services/discover_data_state_container.test.ts b/src/plugins/discover/public/application/main/services/discover_data_state_container.test.ts index d3f600dcfaff8..516d81cc9c3f0 100644 --- a/src/plugins/discover/public/application/main/services/discover_data_state_container.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_data_state_container.test.ts @@ -10,7 +10,7 @@ import { waitFor } from '@testing-library/react'; import { buildDataTableRecord } from '@kbn/discover-utils'; import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__'; import { discoverServiceMock } from '../../../__mocks__/services'; -import { savedSearchMockWithSQL } from '../../../__mocks__/saved_search'; +import { savedSearchMockWithESQL } from '../../../__mocks__/saved_search'; import { FetchStatus } from '../../types'; import { setUrlTracker } from '../../../kibana_services'; import { urlTrackerMock } from '../../../__mocks__/url_tracker.mock'; @@ -104,10 +104,10 @@ describe('test getDataStateContainer', () => { test('useSavedSearch returns plain record raw type', async () => { const stateContainer = getDiscoverStateMock({ - savedSearch: savedSearchMockWithSQL, + savedSearch: savedSearchMockWithESQL, }); - stateContainer.savedSearchState.load = jest.fn().mockResolvedValue(savedSearchMockWithSQL); - await stateContainer.actions.loadSavedSearch({ savedSearchId: savedSearchMockWithSQL.id }); + stateContainer.savedSearchState.load = jest.fn().mockResolvedValue(savedSearchMockWithESQL); + await stateContainer.actions.loadSavedSearch({ savedSearchId: savedSearchMockWithESQL.id }); expect(stateContainer.dataState.data$.main$.getValue().recordRawType).toBe(RecordRawType.PLAIN); }); diff --git a/src/plugins/discover/public/application/main/services/discover_data_state_container.ts b/src/plugins/discover/public/application/main/services/discover_data_state_container.ts index f243d9884ca9e..417fa6679501b 100644 --- a/src/plugins/discover/public/application/main/services/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/services/discover_data_state_container.ts @@ -57,7 +57,7 @@ export enum RecordRawType { */ DOCUMENT = 'document', /** - * Data returned e.g. SQL queries, flat structure + * Data returned e.g. ES|QL queries, flat structure * */ PLAIN = 'plain', } @@ -78,6 +78,7 @@ export interface DataMainMsg extends DataMsg { export interface DataDocumentsMsg extends DataMsg { result?: DataTableRecord[]; textBasedQueryColumns?: DatatableColumn[]; // columns from text-based request + textBasedHeaderWarning?: string; interceptedWarnings?: SearchResponseInterceptedWarning[]; // warnings (like shard failures) } diff --git a/src/plugins/discover/public/application/main/services/discover_state.test.ts b/src/plugins/discover/public/application/main/services/discover_state.test.ts index f402988942a10..bcd96315ce575 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.test.ts @@ -468,8 +468,8 @@ describe('Test discover state actions', () => { ); }); - test('loadSavedSearch without id containing sql, adding no warning toast with an invalid index', async () => { - const url = "/#?_a=(index:abcde,query:(sql:'Select * from test'))&_g=()"; + test('loadSavedSearch without id containing ES|QL, adding no warning toast with an invalid index', async () => { + const url = "/#?_a=(index:abcde,query:(esql:'FROM test'))&_g=()"; const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false }); await state.actions.loadSavedSearch(); expect(discoverServiceMock.toastNotifications.addWarning).not.toHaveBeenCalled(); diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 7f19ec3a23695..ba8a09e17e3d1 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -22,7 +22,7 @@ import { SavedSearchData, } from '../services/discover_data_state_container'; import { fetchDocuments } from './fetch_documents'; -import { fetchSql } from './fetch_sql'; +import { fetchTextBased } from './fetch_text_based'; import { buildDataTableRecord } from '@kbn/discover-utils'; import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__'; import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings'; @@ -31,12 +31,12 @@ jest.mock('./fetch_documents', () => ({ fetchDocuments: jest.fn().mockResolvedValue([]), })); -jest.mock('./fetch_sql', () => ({ - fetchSql: jest.fn().mockResolvedValue([]), +jest.mock('./fetch_text_based', () => ({ + fetchTextBased: jest.fn().mockResolvedValue([]), })); const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction; -const mockFetchSQL = fetchSql as unknown as jest.MockedFunction; +const mockfetchTextBased = fetchTextBased as unknown as jest.MockedFunction; function subjectCollector(subject: Subject): () => Promise { const promise = firstValueFrom( @@ -88,7 +88,7 @@ describe('test fetchAll', () => { }; mockFetchDocuments.mockReset().mockResolvedValue({ records: [] }); - mockFetchSQL.mockReset().mockResolvedValue({ records: [] }); + mockfetchTextBased.mockReset().mockResolvedValue({ records: [] }); }); test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async () => { @@ -246,18 +246,18 @@ describe('test fetchAll', () => { ]); }); - test('emits loading and documents on documents$ correctly for SQL query', async () => { + test('emits loading and documents on documents$ correctly for ES|QL query', async () => { const collect = subjectCollector(subjects.documents$); const hits = [ { _id: '1', _index: 'logs' }, { _id: '2', _index: 'logs' }, ]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); - mockFetchSQL.mockResolvedValue({ + mockfetchTextBased.mockResolvedValue({ records: documents, textBasedQueryColumns: [{ id: '1', name: 'test1', meta: { type: 'number' } }], }); - const query = { sql: 'SELECT * from foo' }; + const query = { esql: 'from foo' }; deps = { abortController: new AbortController(), inspectorAdapters: { requests: new RequestAdapter() }, diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index bf2bc0c1eb1b0..ff754b065a130 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -27,7 +27,7 @@ import { fetchDocuments } from './fetch_documents'; import { FetchStatus } from '../../types'; import { DataMsg, RecordRawType, SavedSearchData } from '../services/discover_data_state_container'; import { DiscoverServices } from '../../../build_services'; -import { fetchSql } from './fetch_sql'; +import { fetchTextBased } from './fetch_text_based'; import { InternalState } from '../services/discover_internal_state_container'; export interface FetchDeps { @@ -70,10 +70,10 @@ export function fetchAll( const query = getAppState().query; const prevQuery = dataSubjects.documents$.getValue().query; const recordRawType = getRawRecordType(query); + const useTextbased = recordRawType === RecordRawType.PLAIN; if (reset) { sendResetMsg(dataSubjects, initialFetchStatus, recordRawType); } - const useSql = recordRawType === RecordRawType.PLAIN; if (recordRawType === RecordRawType.DOCUMENT) { // Update the base searchSource, base for all child fetches @@ -92,14 +92,14 @@ export function fetchAll( // Start fetching all required requests const response = - useSql && query - ? fetchSql(query, dataView, data, services.expressions, inspectorAdapters) + useTextbased && query + ? fetchTextBased(query, dataView, data, services.expressions, inspectorAdapters) : fetchDocuments(searchSource, fetchDeps); - const fetchType = useSql && query ? 'fetchSql' : 'fetchDocuments'; + const fetchType = useTextbased && query ? 'fetchTextBased' : 'fetchDocuments'; const startTime = window.performance.now(); // Handle results of the individual queries and forward the results to the corresponding dataSubjects response - .then(({ records, textBasedQueryColumns, interceptedWarnings }) => { + .then(({ records, textBasedQueryColumns, interceptedWarnings, textBasedHeaderWarning }) => { if (services.analytics) { const duration = window.performance.now() - startTime; reportPerformanceMetricEvent(services.analytics, { @@ -125,7 +125,7 @@ export function fetchAll( * So it takes too long, a bad user experience, also a potential flakniess in tests */ const fetchStatus = - useSql && (!prevQuery || !isEqual(query, prevQuery)) + useTextbased && (!prevQuery || !isEqual(query, prevQuery)) ? FetchStatus.PARTIAL : FetchStatus.COMPLETE; @@ -133,6 +133,7 @@ export function fetchAll( fetchStatus, result: records, textBasedQueryColumns, + textBasedHeaderWarning, interceptedWarnings, recordRawType, query, diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts index 767163259304f..892da705f4b8f 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -12,7 +12,7 @@ import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-utils'; import type { EsHitRecord } from '@kbn/discover-utils/types'; import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings'; -import type { RecordsFetchResponse } from '../../../types'; +import type { RecordsFetchResponse } from '../../types'; import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants'; import { FetchDeps } from './fetch_all'; diff --git a/src/plugins/discover/public/application/main/utils/fetch_sql.ts b/src/plugins/discover/public/application/main/utils/fetch_text_based.ts similarity index 87% rename from src/plugins/discover/public/application/main/utils/fetch_sql.ts rename to src/plugins/discover/public/application/main/utils/fetch_text_based.ts index 73c716e8f9351..6a164bfd8a5f8 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_sql.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_text_based.ts @@ -15,16 +15,16 @@ import type { Datatable } from '@kbn/expressions-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { RecordsFetchResponse } from '../../../types'; +import type { RecordsFetchResponse } from '../../types'; -interface SQLErrorResponse { +interface TextBasedErrorResponse { error: { message: string; }; type: 'error'; } -export function fetchSql( +export function fetchTextBased( query: Query | AggregateQuery, dataView: DataView, data: DataPublicPluginStart, @@ -49,14 +49,16 @@ export function fetchSql( let finalData: DataTableRecord[] = []; let textBasedQueryColumns: Datatable['columns'] | undefined; let error: string | undefined; + let textBasedHeaderWarning: string | undefined; execution.pipe(pluck('result')).subscribe((resp) => { - const response = resp as Datatable | SQLErrorResponse; + const response = resp as Datatable | TextBasedErrorResponse; if (response.type === 'error') { error = response.error.message; } else { const table = response as Datatable; const rows = table?.rows ?? []; textBasedQueryColumns = table?.columns ?? undefined; + textBasedHeaderWarning = table.warning ?? undefined; finalData = rows.map( (row: Record, idx: number) => ({ @@ -74,6 +76,7 @@ export function fetchSql( return { records: finalData || [], textBasedQueryColumns, + textBasedHeaderWarning, }; } }); @@ -81,6 +84,7 @@ export function fetchSql( return { records: [] as DataTableRecord[], textBasedQueryColumns: [], + textBasedHeaderWarning: undefined, }; }) .catch((err) => { diff --git a/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.test.ts b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.test.ts index 0b81061ab68ff..0dfbd84224f4d 100644 --- a/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.test.ts @@ -20,13 +20,13 @@ describe('getDataViewByTextBasedQueryLang', () => { }); const services = discoverServiceMock; it('returns the current dataview if is adhoc and query has not changed', async () => { - const query = { sql: 'Select * from data-view-ad-hoc-title' }; + const query = { esql: 'from data-view-ad-hoc-title' }; const dataView = await getDataViewByTextBasedQueryLang(query, dataViewAdHoc, services); expect(dataView).toStrictEqual(dataViewAdHoc); }); it('creates an adhoc dataview if the current dataview is persistent and query has not changed', async () => { - const query = { sql: 'Select * from the-data-view-title' }; + const query = { esql: 'from the-data-view-title' }; const dataView = await getDataViewByTextBasedQueryLang(query, dataViewMock, services); expect(dataView.isPersisted()).toEqual(false); expect(dataView.timeFieldName).toBe('@timestamp'); @@ -40,7 +40,7 @@ describe('getDataViewByTextBasedQueryLang', () => { title: 'test-1', timeFieldName: undefined, }); - const query = { sql: 'Select * from the-data-view-title' }; + const query = { esql: 'from the-data-view-title' }; const dataView = await getDataViewByTextBasedQueryLang(query, dataViewAdHoc, services); expect(dataView.isPersisted()).toEqual(false); expect(dataView.timeFieldName).toBeUndefined(); diff --git a/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts index 3b38b95dfceb1..09ac5f1e87686 100644 --- a/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts +++ b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts @@ -5,7 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { AggregateQuery, getIndexPatternFromSQLQuery } from '@kbn/es-query'; +import { + AggregateQuery, + getIndexPatternFromSQLQuery, + getIndexPatternFromESQLQuery, +} from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/common'; import { DiscoverServices } from '../../../build_services'; @@ -14,9 +18,16 @@ export async function getDataViewByTextBasedQueryLang( currentDataView: DataView | undefined, services: DiscoverServices ) { - const text = 'sql' in query ? query.sql : undefined; + let indexPatternFromQuery = ''; + if ('sql' in query) { + indexPatternFromQuery = getIndexPatternFromSQLQuery(query.sql); + } + if ('esql' in query) { + indexPatternFromQuery = getIndexPatternFromESQLQuery(query.esql); + } + // we should find a better way to work with ESQL queries which dont need a dataview + if (!indexPatternFromQuery && currentDataView) return currentDataView; - const indexPatternFromQuery = getIndexPatternFromSQLQuery(text); if ( currentDataView?.isPersisted() || indexPatternFromQuery !== currentDataView?.getIndexPattern() diff --git a/src/plugins/discover/public/application/main/utils/get_raw_record_type.test.ts b/src/plugins/discover/public/application/main/utils/get_raw_record_type.test.ts index 146a5a80a125f..781cfef1387a9 100644 --- a/src/plugins/discover/public/application/main/utils/get_raw_record_type.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_raw_record_type.test.ts @@ -15,8 +15,8 @@ describe('getRawRecordType', () => { expect(mode).toEqual(RecordRawType.DOCUMENT); }); - it('returns sql for Query type query', () => { - const mode = getRawRecordType({ sql: 'SELECT * from foo' }); + it('returns esql for Query type query', () => { + const mode = getRawRecordType({ esql: 'from foo' }); expect(mode).toEqual(RecordRawType.PLAIN); }); diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts index 103520c71998b..19e9f6a64c88b 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts @@ -10,7 +10,7 @@ import { getStateDefaults } from './get_state_defaults'; import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks'; import { VIEW_MODE } from '@kbn/saved-search-plugin/common'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; -import { savedSearchMock, savedSearchMockWithSQL } from '../../../__mocks__/saved_search'; +import { savedSearchMock, savedSearchMockWithESQL } from '../../../__mocks__/saved_search'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { discoverServiceMock } from '../../../__mocks__/services'; @@ -81,7 +81,7 @@ describe('getStateDefaults', () => { const actualForUndefinedViewMode = getStateDefaults({ services: discoverServiceMock, savedSearch: { - ...savedSearchMockWithSQL, + ...savedSearchMockWithESQL, viewMode: undefined, }, }); @@ -90,7 +90,7 @@ describe('getStateDefaults', () => { const actualForTextBasedWithInvalidViewMode = getStateDefaults({ services: discoverServiceMock, savedSearch: { - ...savedSearchMockWithSQL, + ...savedSearchMockWithESQL, viewMode: VIEW_MODE.AGGREGATED_LEVEL, }, }); @@ -99,7 +99,7 @@ describe('getStateDefaults', () => { const actualForTextBasedWithValidViewMode = getStateDefaults({ services: discoverServiceMock, savedSearch: { - ...savedSearchMockWithSQL, + ...savedSearchMockWithESQL, viewMode: VIEW_MODE.DOCUMENT_LEVEL, }, }); diff --git a/src/plugins/discover/public/application/main/utils/is_text_based_query.test.ts b/src/plugins/discover/public/application/main/utils/is_text_based_query.test.ts index 53e85216ba0bf..78f7b24d3c10a 100644 --- a/src/plugins/discover/public/application/main/utils/is_text_based_query.test.ts +++ b/src/plugins/discover/public/application/main/utils/is_text_based_query.test.ts @@ -12,6 +12,7 @@ describe('isTextBasedQuery', () => { it('should work correctly', () => { expect(isTextBasedQuery({ query: '', language: 'lucene' })).toEqual(false); expect(isTextBasedQuery({ sql: 'SELECT * from foo' })).toEqual(true); + expect(isTextBasedQuery({ esql: 'from foo' })).toEqual(true); expect(isTextBasedQuery()).toEqual(false); }); }); diff --git a/src/plugins/discover/public/application/types.ts b/src/plugins/discover/public/application/types.ts index 57677236cbf7a..3fc375a5ebcb7 100644 --- a/src/plugins/discover/public/application/types.ts +++ b/src/plugins/discover/public/application/types.ts @@ -6,6 +6,10 @@ * Side Public License, v 1. */ +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; +import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; + export enum FetchStatus { UNINITIALIZED = 'uninitialized', LOADING = 'loading', @@ -16,3 +20,10 @@ export enum FetchStatus { } export type DiscoverDisplayMode = 'embedded' | 'standalone'; + +export interface RecordsFetchResponse { + records: DataTableRecord[]; + textBasedQueryColumns?: DatatableColumn[]; + textBasedHeaderWarning?: string; + interceptedWarnings?: SearchResponseInterceptedWarning[]; +} diff --git a/src/plugins/discover/public/components/common/deferred_spinner.tsx b/src/plugins/discover/public/components/common/deferred_spinner.tsx deleted file mode 100644 index 11f77281f9eb0..0000000000000 --- a/src/plugins/discover/public/components/common/deferred_spinner.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useEffect, useRef, useState } from 'react'; - -/** - * A component that shows it children with a 300ms delay. This is good for wrapping - * loading spinners for tasks that might potentially be very fast (e.g. loading async chunks). - * That way we don't show a quick flash of the spinner before the actual content and will only - * show the spinner once loading takes a bit longer (more than 300ms). - */ -export const DeferredSpinner: React.FC = ({ children }) => { - const timeoutRef = useRef(); - const [showContent, setShowContent] = useState(false); - useEffect(() => { - timeoutRef.current = window.setTimeout(() => { - setShowContent(true); - }, 300); - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); - - return showContent ? {children} : null; -}; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx deleted file mode 100644 index 079a26c265d27..0000000000000 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -const mockCopyToClipboard = jest.fn((value) => true); -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - copyToClipboard: (value: string) => mockCopyToClipboard(value), - }; -}); - -jest.mock('../../hooks/use_discover_services', () => { - const services = { - toastNotifications: { - addInfo: jest.fn(), - }, - }; - const originalModule = jest.requireActual('../../hooks/use_discover_services'); - return { - ...originalModule, - useDiscoverServices: () => services, - }; -}); - -import React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { FilterInBtn, FilterOutBtn, buildCellActions, CopyBtn } from './discover_grid_cell_actions'; -import { DiscoverGridContext } from './discover_grid_context'; -import { EuiButton } from '@elastic/eui'; -import { discoverGridContextMock } from '../../__mocks__/grid_context'; -import { DataViewField } from '@kbn/data-views-plugin/public'; - -describe('Discover cell actions ', function () { - it('should not show cell actions for unfilterable fields', async () => { - expect(buildCellActions({ name: 'foo', filterable: false } as DataViewField)).toEqual([ - CopyBtn, - ]); - }); - - it('should show filter actions for filterable fields', async () => { - expect(buildCellActions({ name: 'foo', filterable: true } as DataViewField, jest.fn())).toEqual( - [FilterInBtn, FilterOutBtn, CopyBtn] - ); - }); - - it('should show Copy action for _source field', async () => { - expect( - buildCellActions({ name: '_source', type: '_source', filterable: false } as DataViewField) - ).toEqual([CopyBtn]); - }); - - it('triggers filter function when FilterInBtn is clicked', async () => { - const component = mountWithIntl( - - } - rowIndex={1} - colIndex={1} - columnId="extension" - isExpanded={false} - /> - - ); - const button = findTestSubject(component, 'filterForButton'); - await button.simulate('click'); - expect(discoverGridContextMock.onFilter).toHaveBeenCalledWith( - discoverGridContextMock.dataView.fields.getByName('extension'), - 'jpg', - '+' - ); - }); - it('triggers filter function when FilterInBtn is clicked for a non-provided value', async () => { - const component = mountWithIntl( - - } - rowIndex={0} - colIndex={1} - columnId="extension" - isExpanded={false} - /> - - ); - const button = findTestSubject(component, 'filterForButton'); - await button.simulate('click'); - expect(discoverGridContextMock.onFilter).toHaveBeenCalledWith( - discoverGridContextMock.dataView.fields.getByName('extension'), - undefined, - '+' - ); - }); - it('triggers filter function when FilterInBtn is clicked for an empty string value', async () => { - const component = mountWithIntl( - - } - rowIndex={4} - colIndex={1} - columnId="message" - isExpanded={false} - /> - - ); - const button = findTestSubject(component, 'filterForButton'); - await button.simulate('click'); - expect(discoverGridContextMock.onFilter).toHaveBeenCalledWith( - discoverGridContextMock.dataView.fields.getByName('message'), - '', - '+' - ); - }); - it('triggers filter function when FilterOutBtn is clicked', async () => { - const component = mountWithIntl( - - } - rowIndex={1} - colIndex={1} - columnId="extension" - isExpanded={false} - /> - - ); - const button = findTestSubject(component, 'filterOutButton'); - await button.simulate('click'); - expect(discoverGridContextMock.onFilter).toHaveBeenCalledWith( - discoverGridContextMock.dataView.fields.getByName('extension'), - 'jpg', - '-' - ); - }); - it('triggers clipboard copy when CopyBtn is clicked', async () => { - const component = mountWithIntl( - - } - rowIndex={1} - colIndex={1} - columnId="extension" - isExpanded={false} - /> - - ); - const button = findTestSubject(component, 'copyClipboardButton'); - await button.simulate('click'); - expect(mockCopyToClipboard).toHaveBeenCalledWith('jpg'); - }); -}); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx similarity index 96% rename from src/plugins/discover/public/components/discover_grid/discover_grid_flyout.test.tsx rename to src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx index e42fae3eacd3b..9593c6c81c31e 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx @@ -14,8 +14,6 @@ import { DiscoverGridFlyout, DiscoverGridFlyoutProps } from './discover_grid_fly import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { dataViewMock, esHitsMock } from '@kbn/discover-utils/src/__mocks__'; import { DiscoverServices } from '../../build_services'; -import { DocViewsRegistry } from '../../services/doc_views/doc_views_registry'; -import { setDocViewsRegistry } from '../../kibana_services'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -23,6 +21,8 @@ import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types'; import { buildDataTableRecord } from '@kbn/discover-utils'; import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; +import { setUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/plugin'; +import { mockUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/__mocks__'; const waitNextTick = () => new Promise((resolve) => setTimeout(resolve, 0)); @@ -34,8 +34,6 @@ const waitNextUpdate = async (component: ReactWrapper) => { }; describe('Discover flyout', function () { - setDocViewsRegistry(new DocViewsRegistry()); - const mountComponent = async ({ dataView, hits, @@ -60,6 +58,7 @@ describe('Discover flyout', function () { contextLocator: { getRedirectUrl: jest.fn(() => 'mock-context-redirect-url') }, singleDocLocator: { getRedirectUrl: jest.fn(() => 'mock-doc-redirect-url') }, } as unknown as DiscoverServices; + setUnifiedDocViewerServices(mockUnifiedDocViewerServices); const hit = buildDataTableRecord( hitIndex ? esHitsMock[hitIndex] : (esHitsMock[0] as EsHitRecord), @@ -200,7 +199,7 @@ describe('Discover flyout', function () { it('should not render single/surrounding views for text based', async () => { const { component } = await mountComponent({ - query: { sql: 'Select * from indexpattern' }, + query: { esql: 'FROM indexpattern' }, }); const singleDocumentView = findTestSubject(component, 'docTableRowAction'); expect(singleDocumentView.length).toBeFalsy(); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx similarity index 95% rename from src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx rename to src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx index f277ad47200ff..a9130df52738e 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx @@ -27,8 +27,8 @@ import { } from '@elastic/eui'; import type { Filter, Query, AggregateQuery } from '@kbn/es-query'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { DocViewer } from '../../services/doc_views/components/doc_viewer/doc_viewer'; -import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public'; import { useNavigationProps } from '../../hooks/use_navigation_props'; import { useDiscoverServices } from '../../hooks/use_discover_services'; import { isTextBasedQuery } from '../../application/main/utils/is_text_based_query'; @@ -45,7 +45,7 @@ export interface DiscoverGridFlyoutProps { onClose: () => void; onFilter?: DocViewFilterFn; onRemoveColumn: (column: string) => void; - setExpandedDoc: (doc: DataTableRecord) => void; + setExpandedDoc: (doc?: DataTableRecord) => void; } function getIndexByDocId(hits: DataTableRecord[], id: string) { @@ -120,7 +120,7 @@ export function DiscoverGridFlyout({

@@ -216,7 +216,7 @@ export function DiscoverGridFlyout({ pageCount={pageCount} activePage={activePage} onPageClick={setPage} - className="dscTable__flyoutDocumentNavigation" + className="unifiedDataTable__flyoutDocumentNavigation" compressed data-test-subj="dscDocNavigation" /> @@ -225,7 +225,7 @@ export function DiscoverGridFlyout({ - ); } + +// eslint-disable-next-line import/no-default-export +export default DiscoverGridFlyout; diff --git a/src/plugins/discover/public/components/discover_grid_flyout/index.ts b/src/plugins/discover/public/components/discover_grid_flyout/index.ts new file mode 100644 index 0000000000000..da7fa274494b1 --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid_flyout/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { withSuspense } from '@kbn/shared-ux-utility'; +import { lazy } from 'react'; +export type { DiscoverGridFlyoutProps } from './discover_grid_flyout'; + +export const DiscoverGridFlyout = withSuspense(lazy(() => import('./discover_grid_flyout'))); diff --git a/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx b/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx index 18ba8817391ac..bd0e2e3439451 100644 --- a/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx +++ b/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx @@ -19,7 +19,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { euiLightVars } from '@kbn/ui-theme'; -import { getRowsPerPageOptions } from '../../../../utils/rows_per_page'; +import { getRowsPerPageOptions } from '@kbn/unified-data-table'; export const MAX_ROWS_PER_PAGE_OPTION = 100; diff --git a/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx b/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx index d9fbe1e8071ce..44775021eda9e 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx @@ -9,13 +9,10 @@ import React from 'react'; import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; import { TableRow, TableRowProps } from './table_row'; -import { setDocViewsRegistry } from '../../../kibana_services'; import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; -import { DocViewsRegistry } from '../../../services/doc_views/doc_views_registry'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { discoverServiceMock } from '../../../__mocks__/services'; -import { DocViewer } from '../../../services/doc_views/components/doc_viewer'; import { DOC_HIDE_TIME_COLUMN_SETTING, MAX_DOC_FIELDS_DISPLAYED } from '@kbn/discover-utils'; import { buildDataTableRecord } from '@kbn/discover-utils'; @@ -81,13 +78,8 @@ describe('Doc table row component', () => { useNewFieldsApi: true, filterManager: mockFilterManager, addBasePath: (path: string) => path, - DocViewer, } as unknown as TableRowProps; - beforeEach(() => { - setDocViewsRegistry(new DocViewsRegistry()); - }); - it('should render __document__ column', () => { const component = mountComponent({ ...defaultProps, columns: [] }); const docTableField = findTestSubject(component, 'docTableField'); diff --git a/src/plugins/discover/public/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/components/doc_table/components/table_row.tsx index e1c00ec3397c0..6057cd64aa473 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_row.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row.tsx @@ -19,10 +19,10 @@ import type { } from '@kbn/discover-utils/types'; import { formatFieldValue } from '@kbn/discover-utils'; import { DOC_HIDE_TIME_COLUMN_SETTING, MAX_DOC_FIELDS_DISPLAYED } from '@kbn/discover-utils'; -import { DocViewRenderProps } from '../../../services/doc_views/doc_views_types'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public'; import { TableCell } from './table_row/table_cell'; import { formatRow, formatTopLevelObject } from '../utils/row_formatter'; -import { DocViewFilterFn } from '../../../services/doc_views/doc_views_types'; import { TableRowDetails } from './table_row_details'; import { useDiscoverServices } from '../../../hooks/use_discover_services'; @@ -43,7 +43,6 @@ export interface TableRowProps { shouldShowFieldHandler: ShouldShowFieldInTableHandler; onAddColumn?: (column: string) => void; onRemoveColumn?: (column: string) => void; - DocViewer: React.ComponentType; } export const TableRow = ({ @@ -59,7 +58,6 @@ export const TableRow = ({ shouldShowFieldHandler, onAddColumn, onRemoveColumn, - DocViewer, }: TableRowProps) => { const { uiSettings, fieldFormats } = useDiscoverServices(); const [maxEntries, hideTimeColumn] = useMemo( @@ -223,7 +221,7 @@ export const TableRow = ({ savedSearchId={savedSearchId} isPlainRecord={isPlainRecord} > - ); diff --git a/src/plugins/discover/public/components/doc_table/doc_table_wrapper.test.tsx b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.test.tsx index 2f05c0c2b357d..17925373186f3 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_wrapper.test.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.test.tsx @@ -15,7 +15,6 @@ import { discoverServiceMock } from '../../__mocks__/services'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '@kbn/discover-utils'; import type { EsHitRecord } from '@kbn/discover-utils/types'; -import { DocViewer } from '../../services/doc_views/components/doc_viewer'; describe('Doc table component', () => { const mountComponent = (customProps?: Partial) => { @@ -48,7 +47,6 @@ describe('Doc table component', () => { render: () => { return
mock
; }, - DocViewer, ...(customProps || {}), }; diff --git a/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx index 8e32b97956cea..a61523b6647c3 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx @@ -14,9 +14,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { Filter } from '@kbn/es-query'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import { SHOW_MULTIFIELDS, getShouldShowFieldHandler } from '@kbn/discover-utils'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { TableHeader } from './components/table_header/table_header'; import { TableRow } from './components/table_row'; -import { DocViewFilterFn, DocViewRenderProps } from '../../services/doc_views/doc_views_types'; import { useDiscoverServices } from '../../hooks/use_discover_services'; export interface DocTableProps { @@ -89,10 +89,6 @@ export interface DocTableProps { * Remove column callback */ onRemoveColumn?: (column: string) => void; - /** - * Doc viewer component - */ - DocViewer: React.ComponentType; } export interface DocTableRenderProps { @@ -133,7 +129,6 @@ export const DocTableWrapper = forwardRef( sharedItemTitle, dataTestSubj, isLoading, - DocViewer, }: DocTableWrapperProps, ref ) => { @@ -191,7 +186,6 @@ export const DocTableWrapper = forwardRef( shouldShowFieldHandler={shouldShowFieldHandler} onAddColumn={onAddColumn} onRemoveColumn={onRemoveColumn} - DocViewer={DocViewer} isPlainRecord={isPlainRecord} rows={rows} /> @@ -207,7 +201,6 @@ export const DocTableWrapper = forwardRef( shouldShowFieldHandler, onAddColumn, onRemoveColumn, - DocViewer, isPlainRecord, rows, ] diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index e6caae3c4a7dd..9a44ae3834f7c 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -59,30 +59,34 @@ import { SORT_DEFAULT_ORDER_SETTING, buildDataTableRecord, } from '@kbn/discover-utils'; -import { VIEW_MODE, DISABLE_SHARD_FAILURE_WARNING } from '../../common/constants'; +import type { UnifiedDataTableProps } from '@kbn/unified-data-table'; +import type { UnifiedDataTableSettings } from '@kbn/unified-data-table'; +import { columnActions } from '@kbn/unified-data-table'; +import { + VIEW_MODE, + DISABLE_SHARD_FAILURE_WARNING, + getDefaultRowsPerPage, +} from '../../common/constants'; import type { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; import type { DiscoverServices } from '../build_services'; import { getSortForEmbeddable, SortPair } from '../utils/sorting'; import { SEARCH_EMBEDDABLE_TYPE, SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from './constants'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; -import * as columnActions from '../components/doc_table/actions/columns'; import { handleSourceColumnState } from '../utils/state_helpers'; -import type { DiscoverGridProps } from '../components/discover_grid/discover_grid'; -import type { DiscoverGridSettings } from '../components/discover_grid/types'; import type { DocTableProps } from '../components/doc_table/doc_table_wrapper'; import { updateSearchSource } from './utils/update_search_source'; import { FieldStatisticsTable } from '../application/main/components/field_stats_table'; +import { fetchTextBased } from '../application/main/utils/fetch_text_based'; import { isTextBasedQuery } from '../application/main/utils/is_text_based_query'; import { getValidViewMode } from '../application/main/utils/get_valid_view_mode'; -import { fetchSql } from '../application/main/utils/fetch_sql'; import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../constants'; import { getDiscoverLocatorParams } from './get_discover_locator_params'; -export type SearchProps = Partial & +export type SearchProps = Partial & Partial & { savedSearchId?: string; filters?: Filter[]; - settings?: DiscoverGridSettings; + settings?: UnifiedDataTableSettings; description?: string; sharedItemTitle?: string; inspectorAdapters?: Adapters; @@ -321,12 +325,12 @@ export class SavedSearchEmbeddable const query = savedSearch.searchSource.getField('query'); const dataView = savedSearch.searchSource.getField('index')!; - const useSql = this.isTextBasedSearch(savedSearch); + const useTextBased = this.isTextBasedSearch(savedSearch); try { - // Request SQL data - if (useSql && query) { - const result = await fetchSql( + // Request text based data + if (useTextBased && query) { + const result = await fetchTextBased( savedSearch.searchSource.getField('query')!, dataView, this.services.data, @@ -585,7 +589,10 @@ export class SavedSearchEmbeddable searchProps.sharedItemTitle = this.panelTitle; searchProps.searchTitle = this.panelTitle; searchProps.rowHeightState = this.input.rowHeight || savedSearch.rowHeight; - searchProps.rowsPerPageState = this.input.rowsPerPage || savedSearch.rowsPerPage; + searchProps.rowsPerPageState = + this.input.rowsPerPage || + savedSearch.rowsPerPage || + getDefaultRowsPerPage(this.services.uiSettings); searchProps.filters = savedSearch.searchSource.getField('filter') as Filter[]; searchProps.savedSearchId = savedSearch.id; diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx index fd28f3114211f..f8c7aa39c1a7c 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx @@ -8,11 +8,8 @@ import React from 'react'; import { AggregateQuery, Query } from '@kbn/es-query'; -import { - DiscoverGridEmbeddable, - DiscoverGridEmbeddableProps, - DataLoadingState, -} from './saved_search_grid'; +import { DataLoadingState } from '@kbn/unified-data-table'; +import { DiscoverGridEmbeddable, DiscoverGridEmbeddableProps } from './saved_search_grid'; import { DiscoverDocTableEmbeddable } from '../components/doc_table/create_doc_table_embeddable'; import { DocTableEmbeddableProps } from '../components/doc_table/doc_table_embeddable'; import { isTextBasedQuery } from '../application/main/utils/is_text_based_query'; @@ -47,7 +44,7 @@ export function SavedSearchEmbeddableComponent({ loadingState={searchProps.isLoading ? DataLoadingState.loading : DataLoadingState.loaded} showFullScreenButton={false} query={query} - className="dscDiscoverGrid" + className="unifiedDataTable" /> ); } diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx index b818286660301..580a55534b573 100644 --- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx @@ -5,43 +5,80 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { memo, useState } from 'react'; +import React, { memo, useCallback, useState } from 'react'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { AggregateQuery, Query } from '@kbn/es-query'; import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { - DiscoverGrid, - DiscoverGridProps, - DataLoadingState as DiscoverDataLoadingState, -} from '../components/discover_grid/discover_grid'; + DataLoadingState as DiscoverGridLoadingState, + UnifiedDataTable, +} from '@kbn/unified-data-table'; +import type { UnifiedDataTableProps } from '@kbn/unified-data-table'; import './saved_search_grid.scss'; -import { DiscoverGridFlyout } from '../components/discover_grid/discover_grid_flyout'; +import { MAX_DOC_FIELDS_DISPLAYED, ROW_HEIGHT_OPTION, SHOW_MULTIFIELDS } from '@kbn/discover-utils'; +import { DiscoverGridFlyout } from '../components/discover_grid_flyout'; import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base'; -export { DataLoadingState } from '../components/discover_grid/discover_grid'; +import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../components/discover_tour'; -export interface DiscoverGridEmbeddableProps extends DiscoverGridProps { +export interface DiscoverGridEmbeddableProps extends UnifiedDataTableProps { totalHitCount?: number; + query?: AggregateQuery | Query; interceptedWarnings?: SearchResponseInterceptedWarning[]; + onAddColumn: (column: string) => void; + onRemoveColumn: (column: string) => void; + savedSearchId?: string; } -export const DiscoverGridMemoized = memo(DiscoverGrid); +export const DataGridMemoized = memo(UnifiedDataTable); export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { const { interceptedWarnings, ...gridProps } = props; const [expandedDoc, setExpandedDoc] = useState(undefined); + const renderDocumentView = useCallback( + (hit: DataTableRecord, displayedRows: DataTableRecord[], displayedColumns: string[]) => ( + setExpandedDoc(undefined)} + setExpandedDoc={setExpandedDoc} + query={props.query} + /> + ), + [ + props.dataView, + props.onAddColumn, + props.onFilter, + props.onRemoveColumn, + props.query, + props.savedSearchId, + ] + ); + return ( - ); diff --git a/src/plugins/discover/public/hooks/use_row_heights_options.test.tsx b/src/plugins/discover/public/hooks/use_row_heights_options.test.tsx deleted file mode 100644 index 113f34ab723dd..0000000000000 --- a/src/plugins/discover/public/hooks/use_row_heights_options.test.tsx +++ /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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { ReactNode } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { DiscoverServices } from '../build_services'; -import { LocalStorageMock } from '../__mocks__/local_storage_mock'; -import { uiSettingsMock } from '../__mocks__/ui_settings'; -import { useRowHeightsOptions } from './use_row_heights_options'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; - -const CONFIG_ROW_HEIGHT = 3; - -const getWrapper = (services: DiscoverServices) => { - return ({ children }: { children: ReactNode }) => ( - {children} - ); -}; - -describe('useRowHeightsOptions', () => { - test('should apply rowHeight from savedSearch', () => { - const { result } = renderHook( - () => { - return useRowHeightsOptions({ - rowHeightState: 2, - }); - }, - { - wrapper: getWrapper({ - uiSettings: uiSettingsMock, - storage: new LocalStorageMock({}) as unknown as Storage, - } as DiscoverServices), - } - ); - - expect(result.current.defaultHeight).toEqual({ lineCount: 2 }); - }); - - test('should apply rowHeight from local storage', () => { - const { result } = renderHook( - () => { - return useRowHeightsOptions({}); - }, - { - wrapper: getWrapper({ - uiSettings: uiSettingsMock, - storage: new LocalStorageMock({ - ['discover:dataGridRowHeight']: { - previousRowHeight: 5, - previousConfigRowHeight: 3, - }, - }) as unknown as Storage, - } as DiscoverServices), - } - ); - - expect(result.current.defaultHeight).toEqual({ lineCount: 5 }); - }); - - test('should apply rowHeight from uiSettings', () => { - const { result } = renderHook( - () => { - return useRowHeightsOptions({}); - }, - { - wrapper: getWrapper({ - uiSettings: uiSettingsMock, - storage: new LocalStorageMock({}) as unknown as Storage, - } as unknown as DiscoverServices), - } - ); - - expect(result.current.defaultHeight).toEqual({ - lineCount: CONFIG_ROW_HEIGHT, - }); - }); - - test('should apply rowHeight from uiSettings instead of local storage value, since uiSettings has been changed', () => { - const { result } = renderHook( - () => { - return useRowHeightsOptions({}); - }, - { - wrapper: getWrapper({ - uiSettings: uiSettingsMock, - storage: new LocalStorageMock({ - ['discover:dataGridRowHeight']: { - previousRowHeight: 4, - // different from uiSettings (config), now user changed it to 3, but prev was 4 - previousConfigRowHeight: 4, - }, - }) as unknown as Storage, - } as unknown as DiscoverServices), - } - ); - - expect(result.current.defaultHeight).toEqual({ - lineCount: CONFIG_ROW_HEIGHT, - }); - }); -}); diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index 8d261d73696e5..ebab3c97c0cec 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -12,7 +12,6 @@ import type { ScopedHistory, AppMountParameters } from '@kbn/core/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; import { HistoryLocationState } from './build_services'; -import { DocViewsRegistry } from './services/doc_views/doc_views_registry'; let uiActions: UiActionsStart; export interface UrlTracker { @@ -29,9 +28,6 @@ export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = export const [getUrlTracker, setUrlTracker] = createGetterSetter('urlTracker'); -export const [getDocViewsRegistry, setDocViewsRegistry] = - createGetterSetter('DocViewsRegistry'); - /** * Makes sure discover and context are using one instance of history. */ diff --git a/src/plugins/discover/public/mocks.tsx b/src/plugins/discover/public/mocks.tsx index e733e9d32dbee..ae2bc4fa547fd 100644 --- a/src/plugins/discover/public/mocks.tsx +++ b/src/plugins/discover/public/mocks.tsx @@ -15,9 +15,6 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { - docViews: { - addDocView: jest.fn(), - }, locator: sharePluginMock.createLocator(), }; return setupContract; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index c3b92275d3956..e00d9d29516c7 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; import React, { ComponentType } from 'react'; import { BehaviorSubject, combineLatest, map } from 'rxjs'; import { @@ -26,7 +25,6 @@ import { SharePluginStart, SharePluginSetup } from '@kbn/share-plugin/public'; import { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public'; -import { EuiSkeletonText } from '@elastic/eui'; import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; import { SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; @@ -42,16 +40,14 @@ import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-taggin import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { UnifiedDocViewerStart } from '@kbn/unified-doc-viewer-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; +import { TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; -import { DOC_TABLE_LEGACY, TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils'; import { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import { PLUGIN_ID } from '../common'; -import { DocViewInput, DocViewInputFn } from './services/doc_views/doc_views_types'; -import { DocViewsRegistry } from './services/doc_views/doc_views_registry'; import { - setDocViewsRegistry, setHeaderActionMenuMounter, setScopedHistory, setUiActions, @@ -61,10 +57,8 @@ import { import { registerFeature } from './register_feature'; import { buildServices } from './build_services'; import { SearchEmbeddableFactory } from './embeddable'; -import { DeferredSpinner } from './components'; import { ViewSavedSearchAction } from './embeddable/view_saved_search_action'; import { injectTruncateStyles } from './utils/truncate_styles'; -import { useDiscoverServices } from './hooks/use_discover_services'; import { initializeKbnUrlTracking } from './utils/initialize_kbn_url_tracking'; import { DiscoverContextAppLocator, @@ -86,24 +80,10 @@ import { type DiscoverContainerProps, } from './components/discover_container'; -const DocViewerLegacyTable = React.lazy( - () => import('./services/doc_views/components/doc_viewer_table/legacy') -); -const DocViewerTable = React.lazy(() => import('./services/doc_views/components/doc_viewer_table')); -const SourceViewer = React.lazy(() => import('./services/doc_views/components/doc_viewer_source')); - /** * @public */ export interface DiscoverSetup { - docViews: { - /** - * Add new doc view shown along with table view and json view in the details of each document in Discover. - * @param docViewRaw - */ - addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; - }; - /** * `share` plugin URL locator for Discover app. Use it to generate links into * Discover application, for example, navigate: @@ -211,6 +191,7 @@ export interface DiscoverStartPlugins { savedObjectsManagement: SavedObjectsManagementPluginStart; savedSearch: SavedSearchPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; + unifiedDocViewer: UnifiedDocViewerStart; lens: LensPublicStart; contentManagement: ContentManagementPublicStart; serverless?: ServerlessPluginStart; @@ -227,7 +208,6 @@ export class DiscoverPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} private appStateUpdater = new BehaviorSubject(() => ({})); - private docViewsRegistry: DocViewsRegistry | null = null; private stopUrlTracking: (() => void) | undefined = undefined; private profileRegistry = createProfileRegistry(); private locator?: DiscoverAppLocator; @@ -253,59 +233,6 @@ export class DiscoverPlugin ); } - this.docViewsRegistry = new DocViewsRegistry(); - setDocViewsRegistry(this.docViewsRegistry); - this.docViewsRegistry.addDocView({ - title: i18n.translate('discover.docViews.table.tableTitle', { - defaultMessage: 'Table', - }), - order: 10, - component: (props) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const services = useDiscoverServices(); - const DocView = services.uiSettings.get(DOC_TABLE_LEGACY) - ? DocViewerLegacyTable - : DocViewerTable; - - return ( - - - - } - > - - - ); - }, - }); - this.docViewsRegistry.addDocView({ - title: i18n.translate('discover.docViews.json.jsonTitle', { - defaultMessage: 'JSON', - }), - order: 20, - component: ({ hit, dataView, query, textBasedHits }) => { - return ( - - - - } - > - - - ); - }, - }); - const { setTrackedUrl, restorePreviousUrl, @@ -418,9 +345,6 @@ export class DiscoverPlugin this.registerEmbeddable(core, plugins); return { - docViews: { - addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), - }, locator: this.locator, }; } diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx deleted file mode 100644 index c7c659c6f84d8..0000000000000 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mount, shallow } from 'enzyme'; -import { DocViewer } from './doc_viewer'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { getDocViewsRegistry } from '../../../../kibana_services'; -import { DocViewRenderProps } from '../../doc_views_types'; -import { buildDataTableRecord } from '@kbn/discover-utils'; - -jest.mock('../../../../kibana_services', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let registry: any[] = []; - return { - getDocViewsRegistry: () => ({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - addDocView(view: any) { - registry.push(view); - }, - getDocViewsSorted() { - return registry; - }, - resetRegistry: () => { - registry = []; - }, - }), - }; -}); - -jest.mock('../../../../hooks/use_discover_services', () => { - return { - useDiscoverServices: { - uiSettings: { - get: jest.fn(), - }, - }, - }; -}); - -beforeEach(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (getDocViewsRegistry() as any).resetRegistry(); - jest.clearAllMocks(); -}); - -test('Render with 3 different tabs', () => { - const registry = getDocViewsRegistry(); - registry.addDocView({ order: 10, title: 'Render function', render: jest.fn() }); - registry.addDocView({ order: 20, title: 'React component', component: () =>
test
}); - // @ts-expect-error This should be invalid and will throw an error when rendering - registry.addDocView({ order: 30, title: 'Invalid doc view' }); - - const renderProps = { hit: {} } as DocViewRenderProps; - - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); -}); - -test('Render with 1 tab displaying error message', () => { - function SomeComponent() { - // this is just a placeholder - return null; - } - - const registry = getDocViewsRegistry(); - registry.addDocView({ - order: 10, - title: 'React component', - component: SomeComponent, - }); - - const renderProps = { - hit: buildDataTableRecord({ _index: 't', _id: '1' }), - } as DocViewRenderProps; - const errorMsg = 'Catch me if you can!'; - - const wrapper = mount(); - const error = new Error(errorMsg); - wrapper.find(SomeComponent).simulateError(error); - const errorMsgComponent = findTestSubject(wrapper, 'docViewerError'); - expect(errorMsgComponent.text()).toMatch(new RegExp(`${errorMsg}`)); -}); diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index bd06f793a6447..d6bbbd0eed9f0 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -30,7 +30,7 @@ import { TRUNCATE_MAX_HEIGHT, SHOW_FIELD_STATISTICS, ROW_HEIGHT_OPTION, - ENABLE_SQL, + ENABLE_ESQL, } from '@kbn/discover-utils'; import { DEFAULT_ROWS_PER_PAGE, ROWS_PER_PAGE_OPTIONS } from '../common/constants'; @@ -308,18 +308,18 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record` + - i18n.translate('discover.advancedSettings.enableSQL.discussLinkText', { + i18n.translate('discover.advancedSettings.enableESQL.discussLinkText', { defaultMessage: 'discuss.elastic.co/c/elastic-stack/kibana', }) + '
', diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 8e93218385709..3328073a026e7 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -51,8 +51,6 @@ "@kbn/shared-ux-page-analytics-no-data", "@kbn/alerting-plugin", "@kbn/ui-theme", - "@kbn/react-field", - "@kbn/monaco", "@kbn/config-schema", "@kbn/storybook", "@kbn/shared-ux-router", @@ -65,12 +63,14 @@ "@kbn/core-application-browser", "@kbn/core-saved-objects-server", "@kbn/discover-utils", - "@kbn/field-types", "@kbn/search-response-warnings", "@kbn/content-management-plugin", + "@kbn/unified-doc-viewer", + "@kbn/unified-doc-viewer-plugin", "@kbn/serverless", "@kbn/react-kibana-mount", "@kbn/react-kibana-context-render", + "@kbn/unified-data-table", "@kbn/no-data-page-plugin" ], "exclude": [ diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index 38b21addd5968..fbba26f3d4dc8 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -131,6 +131,7 @@ export interface Datatable { columns: DatatableColumn[]; meta?: DatatableMeta; rows: DatatableRow[]; + warning?: string; } export interface SerializedDatatable extends Datatable { diff --git a/src/plugins/interactive_setup/public/progress_indicator.tsx b/src/plugins/interactive_setup/public/progress_indicator.tsx index 9fee7e6da7110..5da14e8081555 100644 --- a/src/plugins/interactive_setup/public/progress_indicator.tsx +++ b/src/plugins/interactive_setup/public/progress_indicator.tsx @@ -27,14 +27,7 @@ function isKibanaPastPreboot(response?: Response, body?: StatusResponse) { if (!response?.headers.get('content-type')?.includes('application/json')) { return false; } - - return ( - // Status endpoint may require authentication after `preboot` stage. - response?.status === 401 || - // We're only interested in the availability of the critical core services. - (body?.status?.core?.elasticsearch?.level === 'available' && - body?.status?.core?.savedObjects?.level === 'available') - ); + return body?.status?.overall?.level === 'available'; } export const ProgressIndicator: FunctionComponent = ({ onSuccess }) => { diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index c335f56998ce3..2b43cbe6c5a1d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -139,7 +139,7 @@ export const applicationUsageSchema = { enterpriseSearchApplications: commonSchema, enterpriseSearchEsre: commonSchema, enterpriseSearchVectorSearch: commonSchema, - elasticsearch: commonSchema, + enterpriseSearchElasticsearch: commonSchema, appSearch: commonSchema, workplaceSearch: commonSchema, searchExperiences: commonSchema, @@ -154,6 +154,7 @@ export const applicationUsageSchema = { maps: commonSchema, ml: commonSchema, monitoring: commonSchema, + 'observability-log-explorer': commonSchema, 'observability-overview': commonSchema, observabilityOnboarding: commonSchema, observabilityAIAssistant: commonSchema, 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 9822d2985bced..f17f1e1cc42e8 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -501,7 +501,7 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'discover:enableSql': { + 'discover:enableESQL': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 1b5f84019eb7b..902190f0cf675 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -34,7 +34,7 @@ export interface UsageStats { 'discover:searchFieldsFromSource': boolean; 'discover:showFieldStatistics': boolean; 'discover:showMultiFields': boolean; - 'discover:enableSql': boolean; + 'discover:enableESQL': boolean; 'discover:maxDocFieldsDisplayed': number; 'securitySolution:rulesTableRefresh': string; 'observability:enableInspectEsQueries': boolean; diff --git a/src/plugins/telemetry/common/types/v2.ts b/src/plugins/telemetry/common/types/v2.ts index dc90ad3d242a6..db64c45d96710 100644 --- a/src/plugins/telemetry/common/types/v2.ts +++ b/src/plugins/telemetry/common/types/v2.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import type { TelemetryConfigLabels } from '../../server/config'; + export interface Telemetry { /** Whether telemetry is enabled */ enabled?: boolean | null; @@ -24,6 +26,7 @@ export interface FetchTelemetryConfigResponse { optIn: boolean | null; sendUsageFrom: 'server' | 'browser'; telemetryNotifyUserAboutOptInDefault: boolean; + labels: TelemetryConfigLabels; } export interface FetchLastReportedResponse { diff --git a/src/plugins/telemetry/kibana.jsonc b/src/plugins/telemetry/kibana.jsonc index 147c7ac8b84cd..44162c1189c2e 100644 --- a/src/plugins/telemetry/kibana.jsonc +++ b/src/plugins/telemetry/kibana.jsonc @@ -6,6 +6,7 @@ "id": "telemetry", "server": true, "browser": true, + "enabledOnAnonymousPages": true, "requiredPlugins": [ "telemetryCollectionManager", "usageCollection", diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index c0d0faf0819b0..6300a56486169 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { ApmBase } from '@elastic/apm-rum'; import type { Plugin, CoreStart, @@ -23,7 +24,8 @@ import type { import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; -import { of } from 'rxjs'; +import { BehaviorSubject, map, tap } from 'rxjs'; +import type { TelemetryConfigLabels } from '../server/config'; import { FetchTelemetryConfigRoute, INTERNAL_VERSION } from '../common/routes'; import type { v2 } from '../common/types'; import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; @@ -88,6 +90,12 @@ interface TelemetryPluginStartDependencies { screenshotMode: ScreenshotModePluginStart; } +declare global { + interface Window { + elasticApm?: ApmBase; + } +} + /** * Public-exposed configuration */ @@ -131,6 +139,7 @@ export class TelemetryPlugin { private readonly currentKibanaVersion: string; private readonly config: TelemetryPluginConfig; + private readonly telemetryLabels$: BehaviorSubject; private telemetrySender?: TelemetrySender; private telemetryNotifications?: TelemetryNotifications; private telemetryService?: TelemetryService; @@ -139,6 +148,7 @@ export class TelemetryPlugin constructor(initializerContext: PluginInitializerContext) { this.currentKibanaVersion = initializerContext.env.packageInfo.version; this.config = initializerContext.config.get(); + this.telemetryLabels$ = new BehaviorSubject(this.config.labels); } public setup( @@ -163,7 +173,14 @@ export class TelemetryPlugin analytics.registerContextProvider({ name: 'telemetry labels', - context$: of({ labels: this.config.labels }), + context$: this.telemetryLabels$.pipe( + tap((labels) => { + // Hack to update the APM agent's labels. + // In the future we might want to expose APM as a core service to make reporting metrics much easier. + window.elasticApm?.addLabels(labels); + }), + map((labels) => ({ labels })) + ), schema: { labels: { type: 'pass_through', @@ -230,11 +247,6 @@ export class TelemetryPlugin this.telemetryNotifications = telemetryNotifications; application.currentAppId$.subscribe(async () => { - const isUnauthenticated = this.getIsUnauthenticated(http); - if (isUnauthenticated) { - return; - } - // Refresh and get telemetry config const updatedConfig = await this.refreshConfig(http); @@ -242,6 +254,11 @@ export class TelemetryPlugin global: { enabled: this.telemetryService!.isOptedIn && !screenshotMode.isScreenshotMode() }, }); + const isUnauthenticated = this.getIsUnauthenticated(http); + if (isUnauthenticated) { + return; + } + const telemetryBanner = updatedConfig?.banner; this.maybeStartTelemetryPoller(); @@ -285,6 +302,9 @@ export class TelemetryPlugin if (this.telemetryService) { this.telemetryService.config = updatedConfig; } + + this.telemetryLabels$.next(updatedConfig.labels); + return updatedConfig; } @@ -328,8 +348,16 @@ export class TelemetryPlugin * @private */ private async fetchUpdatedConfig(http: HttpStart | HttpSetup): Promise { - const { allowChangingOptInStatus, optIn, sendUsageFrom, telemetryNotifyUserAboutOptInDefault } = - await http.get(FetchTelemetryConfigRoute, INTERNAL_VERSION); + const { + allowChangingOptInStatus, + optIn, + sendUsageFrom, + telemetryNotifyUserAboutOptInDefault, + labels, + } = await http.get( + FetchTelemetryConfigRoute, + INTERNAL_VERSION + ); return { ...this.config, @@ -337,6 +365,7 @@ export class TelemetryPlugin optIn, sendUsageFrom, telemetryNotifyUserAboutOptInDefault, + labels, userCanChangeSettings: this.canUserChangeSettings, }; } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index ef25a78b1c0b4..e90ab7c9d6a2b 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -2622,7 +2622,7 @@ } } }, - "elasticsearch": { + "enterpriseSearchElasticsearch": { "properties": { "appId": { "type": "keyword", @@ -4587,6 +4587,137 @@ } } }, + "observability-log-explorer": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "observability-overview": { "properties": { "appId": { @@ -9810,7 +9941,7 @@ "description": "Non-default value of setting." } }, - "discover:enableSql": { + "discover:enableESQL": { "type": "boolean", "_meta": { "description": "Non-default value of setting." diff --git a/src/plugins/telemetry/server/config/config.ts b/src/plugins/telemetry/server/config/config.ts index 9ac63b8937ba3..3bb428a948ec5 100644 --- a/src/plugins/telemetry/server/config/config.ts +++ b/src/plugins/telemetry/server/config/config.ts @@ -56,6 +56,9 @@ export const config: PluginConfigDescriptor = { hidePrivacyStatement: true, labels: true, }, + dynamicConfig: { + labels: true, + }, deprecations: () => [ (cfg) => { if (cfg.telemetry?.enabled === false) { diff --git a/src/plugins/telemetry/server/config/telemetry_labels.ts b/src/plugins/telemetry/server/config/telemetry_labels.ts index f78b216b214e8..b55103839f4dc 100644 --- a/src/plugins/telemetry/server/config/telemetry_labels.ts +++ b/src/plugins/telemetry/server/config/telemetry_labels.ts @@ -27,6 +27,7 @@ export const labelsSchema = schema.object( testBuildId: schema.maybe(schema.string()), testJobId: schema.maybe(schema.string()), ciBuildName: schema.maybe(schema.string()), + performancePhase: schema.maybe(schema.string()), /** * The serverless project type. * Flagging it as maybe because these settings should never affect how Kibana runs. diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index ddbf7704d3838..6ce2a875e8549 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -40,6 +40,7 @@ import type { import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import { SavedObjectsClient } from '@kbn/core/server'; +import apm from 'elastic-apm-node'; import { type TelemetrySavedObject, getTelemetrySavedObject, @@ -173,12 +174,18 @@ export class TelemetryPlugin implements Plugin({ name: 'telemetry labels', - context$: this.config$.pipe(map(({ labels }) => ({ labels }))), + context$: this.config$.pipe( + map(({ labels }) => ({ labels })), + tap(({ labels }) => + Object.entries(labels).forEach(([key, value]) => apm.setGlobalLabel(key, value)) + ) + ), schema: { labels: { type: 'pass_through', _meta: { - description: 'Custom labels added to the telemetry.labels config in the kibana.yml', + description: + 'Custom labels added to the telemetry.labels config in the kibana.yml. Validated and limited to a known set of labels.', }, }, }, diff --git a/src/plugins/telemetry/server/routes/telemetry_config.ts b/src/plugins/telemetry/server/routes/telemetry_config.ts index 37daef537b568..d62566bdc3563 100644 --- a/src/plugins/telemetry/server/routes/telemetry_config.ts +++ b/src/plugins/telemetry/server/routes/telemetry_config.ts @@ -10,6 +10,7 @@ import { type Observable, firstValueFrom } from 'rxjs'; import type { IRouter, SavedObjectsClient } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { RequestHandler } from '@kbn/core-http-server'; +import { labelsSchema } from '../config/telemetry_labels'; import type { TelemetryConfigType } from '../config'; import { v2 } from '../../common/types'; import { @@ -70,6 +71,7 @@ export function registerTelemetryConfigRoutes({ optIn, sendUsageFrom, telemetryNotifyUserAboutOptInDefault, + labels: config.labels, }; return res.ok({ body }); @@ -83,6 +85,7 @@ export function registerTelemetryConfigRoutes({ optIn: schema.oneOf([schema.boolean(), schema.literal(null)]), sendUsageFrom: schema.oneOf([schema.literal('server'), schema.literal('browser')]), telemetryNotifyUserAboutOptInDefault: schema.boolean(), + labels: labelsSchema, }), }, }, @@ -90,7 +93,11 @@ export function registerTelemetryConfigRoutes({ // Register the internal versioned API router.versioned - .get({ access: 'internal', path: FetchTelemetryConfigRoute }) + .get({ + access: 'internal', + path: FetchTelemetryConfigRoute, + options: { authRequired: 'optional' }, + }) // Just because it used to be /v2/, we are creating identical v1 and v2. .addVersion({ version: '1', validate: v2Validations }, v2Handler) .addVersion({ version: '2', validate: v2Validations }, v2Handler); diff --git a/src/plugins/text_based_languages/kibana.jsonc b/src/plugins/text_based_languages/kibana.jsonc index 472560aa3b488..f22a87185b54c 100644 --- a/src/plugins/text_based_languages/kibana.jsonc +++ b/src/plugins/text_based_languages/kibana.jsonc @@ -6,6 +6,14 @@ "id": "textBasedLanguages", "server": false, "browser": true, + "optionalPlugins": [ + "indexManagement" + ], + "requiredPlugins": [ + "data", + "expressions", + "dataViews" + ], "requiredBundles": [ "kibanaReact", ] diff --git a/src/plugins/text_based_languages/public/create_editor.tsx b/src/plugins/text_based_languages/public/create_editor.tsx index 1fefff765f9b2..534d86420b74d 100644 --- a/src/plugins/text_based_languages/public/create_editor.tsx +++ b/src/plugins/text_based_languages/public/create_editor.tsx @@ -27,7 +27,7 @@ export const TextBasedLangEditor = (props: TextBasedLanguagesEditorProps) => { return ( diff --git a/src/plugins/text_based_languages/public/kibana_services.ts b/src/plugins/text_based_languages/public/kibana_services.ts index 01f0dd4a823d3..8592904a69370 100644 --- a/src/plugins/text_based_languages/public/kibana_services.ts +++ b/src/plugins/text_based_languages/public/kibana_services.ts @@ -7,17 +7,25 @@ */ import { BehaviorSubject } from 'rxjs'; - -import { CoreStart } from '@kbn/core/public'; +import type { CoreStart } from '@kbn/core/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; +import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public'; export let core: CoreStart; -const servicesReady$ = new BehaviorSubject<{ core: CoreStart; darkMode: boolean } | undefined>( - undefined -); +interface ServiceDeps { + core: CoreStart; + darkMode: boolean; + dataViews: DataViewsPublicPluginStart; + expressions: ExpressionsStart; + indexManagementApiService?: IndexManagementPluginSetup['apiService']; +} + +const servicesReady$ = new BehaviorSubject(undefined); export const untilPluginStartServicesReady = () => { if (servicesReady$.value) return Promise.resolve(servicesReady$.value); - return new Promise<{ core: CoreStart; darkMode: boolean }>((resolve) => { + return new Promise((resolve) => { const subscription = servicesReady$.subscribe((deps) => { if (deps) { subscription.unsubscribe(); @@ -27,9 +35,20 @@ export const untilPluginStartServicesReady = () => { }); }; -export const setKibanaServices = (kibanaCore: CoreStart) => { +export const setKibanaServices = ( + kibanaCore: CoreStart, + dataViews: DataViewsPublicPluginStart, + expressions: ExpressionsStart, + indexManagement?: IndexManagementPluginSetup +) => { core = kibanaCore; core.theme.theme$.subscribe(({ darkMode }) => { - servicesReady$.next({ core, darkMode }); + servicesReady$.next({ + core, + darkMode, + dataViews, + expressions, + indexManagementApiService: indexManagement?.apiService, + }); }); }; diff --git a/src/plugins/text_based_languages/public/plugin.ts b/src/plugins/text_based_languages/public/plugin.ts index f983baf517f8c..d496bdfe30f99 100755 --- a/src/plugins/text_based_languages/public/plugin.ts +++ b/src/plugins/text_based_languages/public/plugin.ts @@ -6,16 +6,31 @@ * Side Public License, v 1. */ -import { Plugin, CoreStart } from '@kbn/core/public'; +import type { Plugin, CoreStart, CoreSetup } from '@kbn/core/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; +import type { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public'; import { setKibanaServices } from './kibana_services'; +interface TextBasedLanguagesPluginStart { + dataViews: DataViewsPublicPluginStart; + expressions: ExpressionsStart; +} + +interface TextBasedLanguagesPluginSetup { + indexManagement: IndexManagementPluginSetup; +} + export class TextBasedLanguagesPlugin implements Plugin<{}, void> { - public setup() { + private indexManagement?: IndexManagementPluginSetup; + + public setup(_: CoreSetup, { indexManagement }: TextBasedLanguagesPluginSetup) { + this.indexManagement = indexManagement; return {}; } - public start(core: CoreStart): void { - setKibanaServices(core); + public start(core: CoreStart, { dataViews, expressions }: TextBasedLanguagesPluginStart): void { + setKibanaServices(core, dataViews, expressions, this.indexManagement); } public stop() {} diff --git a/src/plugins/text_based_languages/tsconfig.json b/src/plugins/text_based_languages/tsconfig.json index d8b5cbd5e965b..152a2aba25c6b 100644 --- a/src/plugins/text_based_languages/tsconfig.json +++ b/src/plugins/text_based_languages/tsconfig.json @@ -13,6 +13,9 @@ "@kbn/text-based-editor", "@kbn/kibana-react-plugin", "@kbn/core", + "@kbn/expressions-plugin", + "@kbn/data-views-plugin", + "@kbn/index-management-plugin", ], "exclude": [ "target/**/*", diff --git a/src/plugins/unified_doc_viewer/README.md b/src/plugins/unified_doc_viewer/README.md new file mode 100644 index 0000000000000..5a88d60f00444 --- /dev/null +++ b/src/plugins/unified_doc_viewer/README.md @@ -0,0 +1,3 @@ +# unifiedDocViewer + +This plugin contains services reliant on the plugin lifecycle for the unified doc viewer component (see @kbn/unified-doc-viewer). \ No newline at end of file diff --git a/src/plugins/unified_doc_viewer/jest.config.js b/src/plugins/unified_doc_viewer/jest.config.js new file mode 100644 index 0000000000000..5250be53c79a6 --- /dev/null +++ b/src/plugins/unified_doc_viewer/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/unified_doc_viewer'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/unified_doc_viewer', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/unified_doc_viewer/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/src/plugins/unified_doc_viewer/kibana.jsonc b/src/plugins/unified_doc_viewer/kibana.jsonc new file mode 100644 index 0000000000000..deb19cf85c0e4 --- /dev/null +++ b/src/plugins/unified_doc_viewer/kibana.jsonc @@ -0,0 +1,13 @@ +{ + "type": "plugin", + "id": "@kbn/unified-doc-viewer-plugin", + "owner": "@elastic/kibana-data-discovery", + "description": "This plugin contains services reliant on the plugin lifecycle for the unified doc viewer component (see @kbn/unified-doc-viewer).", + "plugin": { + "id": "unifiedDocViewer", + "server": false, + "browser": true, + "requiredBundles": ["kibanaUtils", "kibanaReact"], + "requiredPlugins": ["data", "fieldFormats"], + } +} diff --git a/src/plugins/unified_doc_viewer/public/__mocks__/index.ts b/src/plugins/unified_doc_viewer/public/__mocks__/index.ts new file mode 100644 index 0000000000000..d15c62a75d256 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/__mocks__/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './services'; diff --git a/src/plugins/unified_doc_viewer/public/__mocks__/services.ts b/src/plugins/unified_doc_viewer/public/__mocks__/services.ts new file mode 100644 index 0000000000000..c43b8daa86727 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/__mocks__/services.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; +import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; +import type { UnifiedDocViewerServices, UnifiedDocViewerStart } from '../types'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; + +export const mockUnifiedDocViewer: jest.Mocked = { + getDocViews: jest.fn().mockReturnValue([]), +}; + +export const mockUnifiedDocViewerServices: jest.Mocked = { + analytics: analyticsServiceMock.createAnalyticsServiceStart(), + data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsMock, + storage: new Storage(localStorage), + uiSettings: uiSettingsServiceMock.createStartContract(), + unifiedDocViewer: mockUnifiedDocViewer, +}; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer/doc_viewer.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer/doc_viewer.tsx new file mode 100644 index 0000000000000..beb9a032d8c84 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer/doc_viewer.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; +import { DocViewer } from '@kbn/unified-doc-viewer'; +import { getUnifiedDocViewerServices } from '../../plugin'; + +export function UnifiedDocViewer(props: DocViewRenderProps) { + const services = getUnifiedDocViewerServices(); + return ( + + + + ); +} diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer/index.ts b/src/plugins/unified_doc_viewer/public/components/doc_viewer/index.ts new file mode 100644 index 0000000000000..799dfb8c9289f --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UnifiedDocViewer } from './doc_viewer'; + +// Required for usage in React.lazy +// eslint-disable-next-line import/no-default-export +export default UnifiedDocViewer; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/get_height.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/get_height.test.tsx similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_source/get_height.test.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_source/get_height.test.tsx diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/get_height.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/get_height.tsx similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_source/get_height.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_source/get_height.tsx diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/index.ts b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/index.ts similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_source/index.ts rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_source/index.ts diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.scss b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.scss similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.scss rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.scss diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.test.tsx similarity index 94% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.test.tsx index 03853ba6b8d9e..02f31a2e4f46c 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.test.tsx @@ -10,10 +10,10 @@ import React from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { DocViewerSource } from './source'; -import * as hooks from '../../../../hooks/use_es_doc_search'; +import * as hooks from '../../hooks/use_es_doc_search'; import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; -import { JsonCodeEditorCommon } from '../../../../components/json_code_editor/json_code_editor_common'; +import { JsonCodeEditorCommon } from '../json_code_editor'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '@kbn/discover-utils'; import { of } from 'rxjs'; @@ -53,6 +53,7 @@ describe('Source Viewer component', () => { dataView={mockDataView} width={123} hasLineNumbers={true} + onRefresh={() => {}} /> ); @@ -71,6 +72,7 @@ describe('Source Viewer component', () => { dataView={mockDataView} width={123} hasLineNumbers={true} + onRefresh={() => {}} /> ); @@ -110,6 +112,7 @@ describe('Source Viewer component', () => { dataView={mockDataView} width={123} hasLineNumbers={true} + onRefresh={() => {}} /> ); diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.tsx similarity index 77% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.tsx index 95c3b8b34e5d9..26c771e405be8 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.tsx @@ -12,14 +12,13 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { monaco } from '@kbn/monaco'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { ElasticRequestState } from '@kbn/unified-doc-viewer'; import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils'; -import { useDiscoverServices } from '../../../../hooks/use_discover_services'; -import { JSONCodeEditorCommonMemoized } from '../../../../components/json_code_editor/json_code_editor_common'; -import { useEsDocSearch } from '../../../../hooks/use_es_doc_search'; -import { ElasticRequestState } from '../../../../application/doc/types'; +import { useEsDocSearch, useUnifiedDocViewerServices } from '../../hooks'; import { getHeight } from './get_height'; +import { JSONCodeEditorCommonMemoized } from '../json_code_editor'; interface SourceViewerProps { id: string; @@ -28,6 +27,8 @@ interface SourceViewerProps { textBasedHits?: DataTableRecord[]; hasLineNumbers: boolean; width?: number; + requestState?: ElasticRequestState; + onRefresh: () => void; } // Ihe number of lines displayed without scrolling used for classic table, which renders the component @@ -43,14 +44,15 @@ export const DocViewerSource = ({ width, hasLineNumbers, textBasedHits, + onRefresh, }: SourceViewerProps) => { const [editor, setEditor] = useState(); const [editorHeight, setEditorHeight] = useState(); const [jsonValue, setJsonValue] = useState(''); - const { uiSettings } = useDiscoverServices(); + const { uiSettings } = useUnifiedDocViewerServices(); const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const useDocExplorer = !uiSettings.get(DOC_TABLE_LEGACY); - const [reqState, hit, requestData] = useEsDocSearch({ + const [requestState, hit] = useEsDocSearch({ id, index, dataView, @@ -59,10 +61,10 @@ export const DocViewerSource = ({ }); useEffect(() => { - if (reqState === ElasticRequestState.Found && hit) { + if (requestState === ElasticRequestState.Found && hit) { setJsonValue(JSON.stringify(hit.raw, undefined, 2)); } - }, [reqState, hit]); + }, [requestState, hit]); // setting editor height // - classic view: based on lines height and count to stretch and fit its content @@ -93,26 +95,26 @@ export const DocViewerSource = ({
- +
); const errorMessageTitle = (

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

); const errorMessage = (
- {i18n.translate('discover.sourceViewer.errorMessage', { + {i18n.translate('unifiedDocViewer.sourceViewer.errorMessage', { defaultMessage: 'Could not fetch data at this time. Refresh the tab to try again.', })} - - {i18n.translate('discover.sourceViewer.refresh', { + + {i18n.translate('unifiedDocViewer.sourceViewer.refresh', { defaultMessage: 'Refresh', })} @@ -122,11 +124,11 @@ export const DocViewerSource = ({ ); - if (reqState === ElasticRequestState.Error || reqState === ElasticRequestState.NotFound) { + if (requestState === ElasticRequestState.Error || requestState === ElasticRequestState.NotFound) { return errorState; } - if (reqState === ElasticRequestState.Loading || jsonValue === '') { + if (requestState === ElasticRequestState.Loading || jsonValue === '') { return loadingState; } diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/index.ts b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/index.ts similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/index.ts rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/index.ts diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/index.ts b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/index.ts similarity index 100% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/index.ts rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/index.ts diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table.test.tsx similarity index 97% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.test.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table.test.tsx index d1ab0d3d8b3e5..3a614ef71ad5e 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.test.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table.test.tsx @@ -10,11 +10,11 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { DocViewerLegacyTable } from './table'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { DocViewRenderProps } from '../../../doc_views_types'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { DiscoverServices } from '../../../../../build_services'; import { buildDataTableRecord } from '@kbn/discover-utils'; +import type { UnifiedDocViewerServices } from '../../../hooks'; const services = { uiSettings: { @@ -73,7 +73,10 @@ dataView.fields.getByName = (name: string) => { return dataView.fields.getAll().find((field) => field.name === name); }; -const mountComponent = (props: DocViewRenderProps, overrides?: Partial) => { +const mountComponent = ( + props: DocViewRenderProps, + overrides?: Partial +) => { return mountWithIntl( {' '} @@ -419,7 +422,7 @@ describe('DocViewTable at Discover Doc with Fields API', () => { } }, }, - } as unknown as DiscoverServices; + } as unknown as UnifiedDocViewerServices; const component = mountComponent(props, overridedServices); const categoryKeywordRow = findTestSubject(component, 'tableDocViewRow-category.keyword'); diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table.tsx similarity index 94% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table.tsx index 1d465978b17b1..6b2a70b868911 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table.tsx @@ -17,8 +17,8 @@ import { getShouldShowFieldHandler, isNestedFieldParent, } from '@kbn/discover-utils'; -import { useDiscoverServices } from '../../../../../hooks/use_discover_services'; -import { DocViewRenderProps, FieldRecordLegacy } from '../../../doc_views_types'; +import type { DocViewRenderProps, FieldRecordLegacy } from '@kbn/unified-doc-viewer/types'; +import { useUnifiedDocViewerServices } from '../../../hooks'; import { ACTIONS_COLUMN, MAIN_COLUMNS } from './table_columns'; export const DocViewerLegacyTable = ({ @@ -29,7 +29,7 @@ export const DocViewerLegacyTable = ({ onAddColumn, onRemoveColumn, }: DocViewRenderProps) => { - const { fieldFormats, uiSettings } = useDiscoverServices(); + const { fieldFormats, uiSettings } = useUnifiedDocViewerServices(); const showMultiFields = useMemo(() => uiSettings.get(SHOW_MULTIFIELDS), [uiSettings]); const mapping = useCallback((name: string) => dataView.fields.getByName(name), [dataView.fields]); diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_cell_actions.tsx similarity index 93% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_cell_actions.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_cell_actions.tsx index 3ce4b789bd1ff..4499a44190747 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_cell_actions.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_cell_actions.tsx @@ -7,12 +7,12 @@ */ import React from 'react'; -import { DataViewField } from '@kbn/data-views-plugin/public'; +import type { DataViewField } from '@kbn/data-views-plugin/public'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove'; import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists'; import { DocViewTableRowBtnToggleColumn } from './table_row_btn_toggle_column'; import { DocViewTableRowBtnFilterAdd } from './table_row_btn_filter_add'; -import { DocViewFilterFn } from '../../../doc_views_types'; interface TableActionsProps { field: string; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_columns.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_columns.tsx similarity index 84% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_columns.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_columns.tsx index 121187761b2c6..4b79a84970698 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_columns.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_columns.tsx @@ -9,10 +9,10 @@ import { EuiBasicTableColumn, EuiText } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { FieldName } from '../../../../../components/field_name/field_name'; +import type { FieldRecordLegacy } from '@kbn/unified-doc-viewer/types'; +import { FieldName } from '@kbn/unified-doc-viewer'; import { TableActions } from './table_cell_actions'; import { TableFieldValue } from '../table_cell_value'; -import { FieldRecordLegacy } from '../../../doc_views_types'; export const ACTIONS_COLUMN: EuiBasicTableColumn = { field: 'action', @@ -23,7 +23,7 @@ export const ACTIONS_COLUMN: EuiBasicTableColumn = { @@ -55,7 +55,10 @@ export const MAIN_COLUMNS: Array> = [ name: ( - + ), @@ -85,7 +88,10 @@ export const MAIN_COLUMNS: Array> = [ name: ( - + ), diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_row_btn_filter_add.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_row_btn_filter_add.tsx similarity index 77% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_row_btn_filter_add.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_row_btn_filter_add.tsx index ac3768cb96ecd..180dae22cb25f 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table_row_btn_filter_add.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_row_btn_filter_add.tsx @@ -19,12 +19,12 @@ export interface Props { export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props) { const tooltipContent = disabled ? ( ) : ( ); @@ -32,9 +32,12 @@ export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props return ( ) : ( ) ) : ( ); @@ -44,9 +44,12 @@ export function DocViewTableRowBtnFilterExists({ return ( ) : ( ); @@ -32,9 +32,12 @@ export function DocViewTableRowBtnFilterRemove({ onClick, disabled = false }: Pr return ( } > { const showActionsInsideTableCell = useIsWithinBreakpoints(['xl'], true); - const { storage, uiSettings, fieldFormats } = useDiscoverServices(); + const { fieldFormats, storage, uiSettings } = useUnifiedDocViewerServices(); const showMultiFields = uiSettings.get(SHOW_MULTIFIELDS); const currentDataViewId = dataView.id!; const isSingleDocView = !filter; @@ -129,7 +129,7 @@ export const DocViewerTable = ({ [flattened, dataView, showMultiFields] ); - const searchPlaceholder = i18n.translate('discover.docView.table.searchPlaceHolder', { + const searchPlaceholder = i18n.translate('unifiedDocViewer.docView.table.searchPlaceHolder', { defaultMessage: 'Search field names', }); @@ -283,7 +283,7 @@ export const DocViewerTable = ({ @@ -293,14 +293,20 @@ export const DocViewerTable = ({ - + , - + , @@ -401,7 +407,10 @@ export const DocViewerTable = ({ {rowElements.length === 0 ? (

- +

) : ( diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx similarity index 75% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx index 9f29f3ba7f69f..8b9a35e071310 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { DataViewField } from '@kbn/data-views-plugin/public'; -import { DocViewFilterFn } from '../../doc_views_types'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; interface TableActionsProps { mode?: 'inline' | 'as_popover'; @@ -43,43 +43,49 @@ export const TableActions = ({ onTogglePinned, }: TableActionsProps) => { const [isOpen, setIsOpen] = useState(false); - const openActionsLabel = i18n.translate('discover.docView.table.actions.open', { + const openActionsLabel = i18n.translate('unifiedDocViewer.docView.table.actions.open', { defaultMessage: 'Open actions', }); - const actionsLabel = i18n.translate('discover.docView.table.actions.label', { + const actionsLabel = i18n.translate('unifiedDocViewer.docView.table.actions.label', { defaultMessage: 'Actions', }); // Filters pair const filtersPairDisabled = !fieldMapping || !fieldMapping.filterable || ignoredValue; - const filterAddLabel = i18n.translate('discover.docViews.table.filterForValueButtonTooltip', { - defaultMessage: 'Filter for value', - }); + const filterAddLabel = i18n.translate( + 'unifiedDocViewer.docViews.table.filterForValueButtonTooltip', + { + defaultMessage: 'Filter for value', + } + ); const filterAddAriaLabel = i18n.translate( - 'discover.docViews.table.filterForValueButtonAriaLabel', + 'unifiedDocViewer.docViews.table.filterForValueButtonAriaLabel', { defaultMessage: 'Filter for value' } ); - const filterOutLabel = i18n.translate('discover.docViews.table.filterOutValueButtonTooltip', { - defaultMessage: 'Filter out value', - }); + const filterOutLabel = i18n.translate( + 'unifiedDocViewer.docViews.table.filterOutValueButtonTooltip', + { + defaultMessage: 'Filter out value', + } + ); const filterOutAriaLabel = i18n.translate( - 'discover.docViews.table.filterOutValueButtonAriaLabel', + 'unifiedDocViewer.docViews.table.filterOutValueButtonAriaLabel', { defaultMessage: 'Filter out value' } ); const filtersPairToolTip = (filtersPairDisabled && - i18n.translate('discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip', { + i18n.translate('unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip', { defaultMessage: 'Unindexed fields or ignored values cannot be searched', })) || undefined; // Filter exists const filterExistsLabel = i18n.translate( - 'discover.docViews.table.filterForFieldPresentButtonTooltip', + 'unifiedDocViewer.docViews.table.filterForFieldPresentButtonTooltip', { defaultMessage: 'Filter for field present' } ); const filterExistsAriaLabel = i18n.translate( - 'discover.docViews.table.filterForFieldPresentButtonAriaLabel', + 'unifiedDocViewer.docViews.table.filterForFieldPresentButtonAriaLabel', { defaultMessage: 'Filter for field present' } ); const filtersExistsDisabled = !fieldMapping || !fieldMapping.filterable; @@ -87,35 +93,44 @@ export const TableActions = ({ (filtersExistsDisabled && (fieldMapping && fieldMapping.scripted ? i18n.translate( - 'discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip', + 'unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip', { defaultMessage: 'Unable to filter for presence of scripted fields', } ) - : i18n.translate('discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip', { - defaultMessage: 'Unable to filter for presence of meta fields', - }))) || + : i18n.translate( + 'unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip', + { + defaultMessage: 'Unable to filter for presence of meta fields', + } + ))) || undefined; // Toggle columns const toggleColumnsLabel = i18n.translate( - 'discover.docViews.table.toggleColumnInTableButtonTooltip', + 'unifiedDocViewer.docViews.table.toggleColumnInTableButtonTooltip', { defaultMessage: 'Toggle column in table' } ); const toggleColumnsAriaLabel = i18n.translate( - 'discover.docViews.table.toggleColumnInTableButtonAriaLabel', + 'unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel', { defaultMessage: 'Toggle column in table' } ); // Pinned const pinnedLabel = pinned - ? i18n.translate('discover.docViews.table.unpinFieldLabel', { defaultMessage: 'Unpin field' }) - : i18n.translate('discover.docViews.table.pinFieldLabel', { defaultMessage: 'Pin field' }); + ? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldLabel', { + defaultMessage: 'Unpin field', + }) + : i18n.translate('unifiedDocViewer.docViews.table.pinFieldLabel', { + defaultMessage: 'Pin field', + }); const pinnedAriaLabel = pinned - ? i18n.translate('discover.docViews.table.unpinFieldAriaLabel', { + ? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldAriaLabel', { defaultMessage: 'Unpin field', }) - : i18n.translate('discover.docViews.table.pinFieldAriaLabel', { defaultMessage: 'Pin field' }); + : i18n.translate('unifiedDocViewer.docViews.table.pinFieldAriaLabel', { + defaultMessage: 'Pin field', + }); const pinnedIconType = pinned ? 'pinFilled' : 'pin'; const toggleOpenPopover = useCallback(() => setIsOpen((current) => !current), []); diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_value.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx similarity index 83% rename from src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_value.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx index d1ec9d369439f..79c79e6a45836 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_value.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx @@ -26,26 +26,26 @@ const IgnoreWarning: React.FC = React.memo(({ rawValue, reas switch (reason) { case IgnoredReason.IGNORE_ABOVE: return multiValue - ? i18n.translate('discover.docView.table.ignored.multiAboveTooltip', { + ? i18n.translate('unifiedDocViewer.docView.table.ignored.multiAboveTooltip', { defaultMessage: `One or more values in this field are too long and can't be searched or filtered.`, }) - : i18n.translate('discover.docView.table.ignored.singleAboveTooltip', { + : i18n.translate('unifiedDocViewer.docView.table.ignored.singleAboveTooltip', { defaultMessage: `The value in this field is too long and can't be searched or filtered.`, }); case IgnoredReason.MALFORMED: return multiValue - ? i18n.translate('discover.docView.table.ignored.multiMalformedTooltip', { + ? i18n.translate('unifiedDocViewer.docView.table.ignored.multiMalformedTooltip', { defaultMessage: `This field has one or more malformed values that can't be searched or filtered.`, }) - : i18n.translate('discover.docView.table.ignored.singleMalformedTooltip', { + : i18n.translate('unifiedDocViewer.docView.table.ignored.singleMalformedTooltip', { defaultMessage: `The value in this field is malformed and can't be searched or filtered.`, }); case IgnoredReason.UNKNOWN: return multiValue - ? i18n.translate('discover.docView.table.ignored.multiUnknownTooltip', { + ? i18n.translate('unifiedDocViewer.docView.table.ignored.multiUnknownTooltip', { defaultMessage: `One or more values in this field were ignored by Elasticsearch and can't be searched or filtered.`, }) - : i18n.translate('discover.docView.table.ignored.singleUnknownTooltip', { + : i18n.translate('unifiedDocViewer.docView.table.ignored.singleUnknownTooltip', { defaultMessage: `The value in this field was ignored by Elasticsearch and can't be searched or filtered.`, }); } @@ -67,10 +67,10 @@ const IgnoreWarning: React.FC = React.memo(({ rawValue, reas {multiValue - ? i18n.translate('discover.docViews.table.ignored.multiValueLabel', { + ? i18n.translate('unifiedDocViewer.docViews.table.ignored.multiValueLabel', { defaultMessage: 'Contains ignored values', }) - : i18n.translate('discover.docViews.table.ignored.singleValueLabel', { + : i18n.translate('unifiedDocViewer.docViews.table.ignored.singleValueLabel', { defaultMessage: 'Ignored value', })} diff --git a/src/plugins/unified_doc_viewer/public/components/index.ts b/src/plugins/unified_doc_viewer/public/components/index.ts new file mode 100644 index 0000000000000..b5f3a8948d689 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './doc_viewer'; +export * from './doc_viewer_source'; +export * from './doc_viewer_table'; +export * from './json_code_editor'; diff --git a/src/plugins/unified_doc_viewer/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/unified_doc_viewer/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap new file mode 100644 index 0000000000000..7af546298e0d8 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`returns the \`JsonCodeEditor\` component 1`] = ` + +`; diff --git a/src/plugins/unified_doc_viewer/public/components/json_code_editor/index.ts b/src/plugins/unified_doc_viewer/public/components/json_code_editor/index.ts new file mode 100644 index 0000000000000..d0e3147e65ab1 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/json_code_editor/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './json_code_editor'; +export * from './json_code_editor_common'; diff --git a/src/plugins/discover/public/components/json_code_editor/json_code_editor.scss b/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor.scss similarity index 100% rename from src/plugins/discover/public/components/json_code_editor/json_code_editor.scss rename to src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor.scss diff --git a/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor.test.tsx b/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor.test.tsx new file mode 100644 index 0000000000000..e1ec1373f8657 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor.test.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import JsonCodeEditor from './json_code_editor'; + +it('returns the `JsonCodeEditor` component', () => { + const value = { + _index: 'test', + _type: 'doc', + _id: 'foo', + _score: 1, + _source: { test: 123 }, + }; + expect(shallow()).toMatchSnapshot(); +}); diff --git a/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor.tsx b/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor.tsx new file mode 100644 index 0000000000000..d08e35eb6d4bf --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './json_code_editor.scss'; + +import React from 'react'; +import { JsonCodeEditorCommon } from './json_code_editor_common'; + +export interface JsonCodeEditorProps { + json: Record; + width?: string | number; + height?: string | number; + hasLineNumbers?: boolean; +} + +// Required for usage in React.lazy +// eslint-disable-next-line import/no-default-export +export default function JsonCodeEditor({ + json, + width, + height, + hasLineNumbers, +}: JsonCodeEditorProps) { + const jsonValue = JSON.stringify(json, null, 2); + + return ( + void 0} + hideCopyButton={true} + /> + ); +} diff --git a/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor_common.tsx similarity index 93% rename from src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx rename to src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor_common.tsx index 777240fe2f5bb..fa5dbfbe616ea 100644 --- a/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx +++ b/src/plugins/unified_doc_viewer/public/components/json_code_editor/json_code_editor_common.tsx @@ -14,10 +14,10 @@ import { monaco, XJsonLang } from '@kbn/monaco'; import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { CodeEditor } from '@kbn/kibana-react-plugin/public'; -const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', { +const codeEditorAriaLabel = i18n.translate('unifiedDocViewer.json.codeEditorAriaLabel', { defaultMessage: 'Read only JSON view of an elasticsearch document', }); -const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', { +const copyToClipboardLabel = i18n.translate('unifiedDocViewer.json.copyToClipboardLabel', { defaultMessage: 'Copy to clipboard', }); diff --git a/src/plugins/unified_doc_viewer/public/hooks/index.ts b/src/plugins/unified_doc_viewer/public/hooks/index.ts new file mode 100644 index 0000000000000..547032ce4415e --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './use_doc_viewer_services'; +export * from './use_es_doc_search'; diff --git a/src/plugins/unified_doc_viewer/public/hooks/use_doc_viewer_services.ts b/src/plugins/unified_doc_viewer/public/hooks/use_doc_viewer_services.ts new file mode 100644 index 0000000000000..4287e87ea6aa3 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/hooks/use_doc_viewer_services.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { UnifiedDocViewerStart } from '../plugin'; + +export interface UnifiedDocViewerServices { + analytics: AnalyticsServiceStart; + data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; + storage: Storage; + uiSettings: IUiSettingsClient; + unifiedDocViewer: UnifiedDocViewerStart; +} + +export function useUnifiedDocViewerServices(): UnifiedDocViewerServices { + const { services } = useKibana(); + const { analytics, data, fieldFormats, storage, uiSettings, unifiedDocViewer } = services; + return { analytics, data, fieldFormats, storage, uiSettings, unifiedDocViewer }; +} diff --git a/src/plugins/discover/public/hooks/use_es_doc_search.test.tsx b/src/plugins/unified_doc_viewer/public/hooks/use_es_doc_search.test.tsx similarity index 93% rename from src/plugins/discover/public/hooks/use_es_doc_search.test.tsx rename to src/plugins/unified_doc_viewer/public/hooks/use_es_doc_search.test.tsx index 64a998a542069..cee1cf509e138 100644 --- a/src/plugins/discover/public/hooks/use_es_doc_search.test.tsx +++ b/src/plugins/unified_doc_viewer/public/hooks/use_es_doc_search.test.tsx @@ -7,11 +7,10 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { buildSearchBody, useEsDocSearch } from './use_es_doc_search'; +import { type EsDocSearchProps, buildSearchBody, useEsDocSearch } from './use_es_doc_search'; import { Subject } from 'rxjs'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { DocProps } from '../application/doc/components/doc'; -import { ElasticRequestState } from '../application/doc/types'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { ElasticRequestState } from '@kbn/unified-doc-viewer'; import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource, buildDataTableRecord, @@ -227,9 +226,9 @@ describe('Test of helper / hook', () => { id: '1', index: 'index1', dataView, - } as unknown as DocProps; + } as unknown as EsDocSearchProps; - const hook = renderHook((p: DocProps) => useEsDocSearch(p), { + const hook = renderHook((p: EsDocSearchProps) => useEsDocSearch(p), { initialProps: props, wrapper: ({ children }) => ( {children} @@ -251,9 +250,9 @@ describe('Test of helper / hook', () => { id: '1', index: 'index1', dataView, - } as unknown as DocProps; + } as unknown as EsDocSearchProps; - const hook = renderHook((p: DocProps) => useEsDocSearch(p), { + const hook = renderHook((p: EsDocSearchProps) => useEsDocSearch(p), { initialProps: props, wrapper: ({ children }) => ( {children} @@ -305,9 +304,9 @@ describe('Test of helper / hook', () => { flattened: { field1: 1, field2: 2 }, }, ], - } as unknown as DocProps; + } as unknown as EsDocSearchProps; - const hook = renderHook((p: DocProps) => useEsDocSearch(p), { + const hook = renderHook((p: EsDocSearchProps) => useEsDocSearch(p), { initialProps: props, wrapper: ({ children }) => ( {children} diff --git a/src/plugins/discover/public/hooks/use_es_doc_search.ts b/src/plugins/unified_doc_viewer/public/hooks/use_es_doc_search.ts similarity index 84% rename from src/plugins/discover/public/hooks/use_es_doc_search.ts rename to src/plugins/unified_doc_viewer/public/hooks/use_es_doc_search.ts index b430d6b4531b9..d215306d6f7ea 100644 --- a/src/plugins/discover/public/hooks/use_es_doc_search.ts +++ b/src/plugins/unified_doc_viewer/public/hooks/use_es_doc_search.ts @@ -9,16 +9,38 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { lastValueFrom } from 'rxjs'; -import { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import { SEARCH_FIELDS_FROM_SOURCE, buildDataTableRecord } from '@kbn/discover-utils'; -import { DocProps } from '../application/doc/components/doc'; -import { ElasticRequestState } from '../application/doc/types'; -import { useDiscoverServices } from './use_discover_services'; +import { ElasticRequestState } from '@kbn/unified-doc-viewer'; +import { useUnifiedDocViewerServices } from './use_doc_viewer_services'; type RequestBody = Pick; +export interface EsDocSearchProps { + /** + * Id of the doc in ES + */ + id: string; + /** + * Index in ES to query + */ + index: string; + /** + * DataView entity + */ + dataView: DataView; + /** + * If set, will always request source, regardless of the global `fieldsFromSource` setting + */ + requestSource?: boolean; + /** + * Records fetched from text based query + */ + textBasedHits?: DataTableRecord[]; +} + /** * Custom react hook for querying a single doc in ElasticSearch */ @@ -28,10 +50,10 @@ export function useEsDocSearch({ dataView, requestSource, textBasedHits, -}: DocProps): [ElasticRequestState, DataTableRecord | null, () => void] { +}: EsDocSearchProps): [ElasticRequestState, DataTableRecord | null, () => void] { const [status, setStatus] = useState(ElasticRequestState.Loading); const [hit, setHit] = useState(null); - const { data, uiSettings, analytics } = useDiscoverServices(); + const { data, uiSettings, analytics } = useUnifiedDocViewerServices(); const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); const requestData = useCallback(async () => { diff --git a/src/plugins/unified_doc_viewer/public/index.tsx b/src/plugins/unified_doc_viewer/public/index.tsx new file mode 100644 index 0000000000000..d08de9dcaa0eb --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/index.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; +import { EuiDelayRender, EuiSkeletonText } from '@elastic/eui'; +import { DocViewRenderProps } from '@kbn/unified-doc-viewer/src/services/types'; +import type { JsonCodeEditorProps } from './components'; +import { UnifiedDocViewerPublicPlugin } from './plugin'; + +export type { UnifiedDocViewerSetup, UnifiedDocViewerStart } from './plugin'; + +const LazyJsonCodeEditor = React.lazy( + () => import('./components/json_code_editor/json_code_editor') +); + +export const JsonCodeEditor = withSuspense( + LazyJsonCodeEditor, + + + +); + +const LazyUnifiedDocViewer = React.lazy(() => import('./components/doc_viewer')); +export const UnifiedDocViewer = withSuspense( + LazyUnifiedDocViewer, + + + +); + +export { useEsDocSearch, useUnifiedDocViewerServices } from './hooks'; + +export const plugin = () => new UnifiedDocViewerPublicPlugin(); diff --git a/src/plugins/unified_doc_viewer/public/plugin.tsx b/src/plugins/unified_doc_viewer/public/plugin.tsx new file mode 100644 index 0000000000000..018e6ffadd312 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/plugin.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { CoreSetup, Plugin } from '@kbn/core/public'; +import { DOC_TABLE_LEGACY } from '@kbn/discover-utils'; +import { i18n } from '@kbn/i18n'; +import { DocViewsRegistry } from '@kbn/unified-doc-viewer'; +import { EuiDelayRender, EuiSkeletonText } from '@elastic/eui'; +import { createGetterSetter, Storage } from '@kbn/kibana-utils-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { CoreStart } from '@kbn/core/public'; +import { type UnifiedDocViewerServices, useUnifiedDocViewerServices } from './hooks'; + +export const [getUnifiedDocViewerServices, setUnifiedDocViewerServices] = + createGetterSetter('UnifiedDocViewerServices'); + +const DocViewerLegacyTable = React.lazy(() => import('./components/doc_viewer_table/legacy')); +const DocViewerTable = React.lazy(() => import('./components/doc_viewer_table')); +const SourceViewer = React.lazy(() => import('./components/doc_viewer_source')); + +export interface UnifiedDocViewerSetup { + addDocView: DocViewsRegistry['addDocView']; +} + +export interface UnifiedDocViewerStart { + getDocViews: DocViewsRegistry['getDocViewsSorted']; +} + +export interface UnifiedDocViewerStartDeps { + data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; +} + +export class UnifiedDocViewerPublicPlugin + implements Plugin +{ + private docViewsRegistry = new DocViewsRegistry(); + + public setup(core: CoreSetup) { + this.docViewsRegistry.addDocView({ + title: i18n.translate('unifiedDocViewer.docViews.table.tableTitle', { + defaultMessage: 'Table', + }), + order: 10, + component: (props) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { uiSettings } = useUnifiedDocViewerServices(); + const DocView = uiSettings.get(DOC_TABLE_LEGACY) ? DocViewerLegacyTable : DocViewerTable; + + return ( + + + + } + > + + + ); + }, + }); + + this.docViewsRegistry.addDocView({ + title: i18n.translate('unifiedDocViewer.docViews.json.jsonTitle', { + defaultMessage: 'JSON', + }), + order: 20, + component: ({ hit, dataView, query, textBasedHits }) => { + return ( + + + + } + > + {}} + /> + + ); + }, + }); + + return { + addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), + }; + } + + public start(core: CoreStart, deps: UnifiedDocViewerStartDeps) { + const { analytics, uiSettings } = core; + const { data, fieldFormats } = deps; + const storage = new Storage(localStorage); + const unifiedDocViewer = { + getDocViews: this.docViewsRegistry.getDocViewsSorted.bind(this.docViewsRegistry), + }; + const services = { analytics, data, fieldFormats, storage, uiSettings, unifiedDocViewer }; + setUnifiedDocViewerServices(services); + return unifiedDocViewer; + } +} diff --git a/src/plugins/unified_doc_viewer/public/types.ts b/src/plugins/unified_doc_viewer/public/types.ts new file mode 100644 index 0000000000000..d9ec40eedfffb --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { JsonCodeEditorProps } from './components'; +export type { EsDocSearchProps, UnifiedDocViewerServices } from './hooks'; +export type { UnifiedDocViewerSetup, UnifiedDocViewerStart } from './plugin'; diff --git a/src/plugins/unified_doc_viewer/tsconfig.json b/src/plugins/unified_doc_viewer/tsconfig.json new file mode 100644 index 0000000000000..3e959ca047e40 --- /dev/null +++ b/src/plugins/unified_doc_viewer/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + }, + "include": [ "../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*"], + "kbn_references": [ + "@kbn/kibana-react-plugin", + "@kbn/monaco", + "@kbn/data-views-plugin", + "@kbn/test-jest-helpers", + "@kbn/discover-utils", + "@kbn/i18n-react", + "@kbn/i18n", + "@kbn/unified-doc-viewer", + "@kbn/unified-field-list", + "@kbn/kibana-utils-plugin", + "@kbn/data-plugin", + "@kbn/core-analytics-browser", + "@kbn/field-formats-plugin", + "@kbn/core-ui-settings-browser", + "@kbn/ebt-tools", + "@kbn/core", + "@kbn/shared-ux-utility", + "@kbn/core-analytics-browser-mocks", + "@kbn/core-ui-settings-browser-mocks" + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/src/plugins/unified_histogram/public/__mocks__/suggestions.ts b/src/plugins/unified_histogram/public/__mocks__/suggestions.ts index 0a92393f60ec3..bed2eee388cde 100644 --- a/src/plugins/unified_histogram/public/__mocks__/suggestions.ts +++ b/src/plugins/unified_histogram/public/__mocks__/suggestions.ts @@ -39,7 +39,7 @@ export const currentSuggestionMock = { '46aa21fa-b747-4543-bf90-0b40007c546d': { index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', query: { - sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"', + esql: 'FROM kibana_sample_data_flights | keep Dest, AvgTicketPrice', }, columns: [ { @@ -141,7 +141,7 @@ export const currentSuggestionMock = { fieldName: '', contextualFields: ['Dest', 'AvgTicketPrice'], query: { - sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"', + esql: 'FROM "kibana_sample_data_flights"', }, }, }, @@ -178,7 +178,7 @@ export const allSuggestionsMock = [ '2513a3d4-ad9d-48ea-bd58-8b6419ab97e6': { index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', query: { - sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"', + esql: 'FROM "kibana_sample_data_flights"', }, columns: [ { @@ -281,7 +281,7 @@ export const allSuggestionsMock = [ fieldName: '', contextualFields: ['Dest', 'AvgTicketPrice'], query: { - sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"', + esql: 'FROM "kibana_sample_data_flights"', }, }, }, diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index a9baa653ecee4..0172b1b6107c1 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -69,6 +69,7 @@ export interface ChartProps { disabledActions?: LensEmbeddableInput['disabledActions']; input$?: UnifiedHistogramInput$; lensTablesAdapter?: Record; + isOnHistogramMode?: boolean; onResetChartHeight?: () => void; onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; @@ -105,6 +106,7 @@ export function Chart({ disabledActions, input$: originalInput$, lensTablesAdapter, + isOnHistogramMode, onResetChartHeight, onChartHiddenChange, onTimeIntervalChange, @@ -427,7 +429,7 @@ export function Chart({ disableTriggers={disableTriggers} disabledActions={disabledActions} onTotalHitsChange={onTotalHitsChange} - hasLensSuggestions={Boolean(currentSuggestion)} + hasLensSuggestions={!Boolean(isOnHistogramMode)} onChartLoad={onChartLoad} onFilter={onFilter} onBrushEnd={onBrushEnd} diff --git a/src/plugins/unified_histogram/public/chart/chart_config_panel.test.tsx b/src/plugins/unified_histogram/public/chart/chart_config_panel.test.tsx index 710666ac6637b..ef673826672ea 100644 --- a/src/plugins/unified_histogram/public/chart/chart_config_panel.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart_config_panel.test.tsx @@ -36,7 +36,7 @@ describe('ChartConfigPanel', () => { isPlainRecord: true, lensTablesAdapter: lensTablesAdapterMock, query: { - sql: 'Select * from test', + esql: 'from test', }, }} /> diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index ef77af13209c0..78f06687a0e7e 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -38,7 +38,7 @@ const getMockLensAttributes = () => suggestion: undefined, }); -function mountComponent() { +function mountComponent(isPlainRecord = false, hasLensSuggestions = false) { const services = unifiedHistogramServicesMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; @@ -51,7 +51,8 @@ function mountComponent() { request: { searchSessionId: '123', }, - hasLensSuggestions: false, + hasLensSuggestions, + isPlainRecord, hits: { status: UnifiedHistogramFetchStatus.loading, total: undefined, @@ -237,4 +238,76 @@ describe('Histogram', () => { ); expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters }); }); + + it('should execute onLoad correctly for textbased language and no Lens suggestions', async () => { + const { component, props } = mountComponent(true, false); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + const adapters = createDefaultInspectorAdapters(); + adapters.tables.tables.layerId = { + meta: { type: 'es_ql' }, + columns: [ + { + id: 'rows', + name: 'rows', + meta: { + type: 'number', + dimensionName: 'Vertical axis', + }, + }, + ], + rows: [ + { + rows: 16, + }, + { + rows: 4, + }, + ], + } as any; + act(() => { + onLoad(false, adapters); + }); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.complete, + 20 + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters }); + }); + + it('should execute onLoad correctly for textbased language and Lens suggestions', async () => { + const { component, props } = mountComponent(true, true); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + const adapters = createDefaultInspectorAdapters(); + adapters.tables.tables.layerId = { + meta: { type: 'es_ql' }, + columns: [ + { + id: 'rows', + name: 'rows', + meta: { + type: 'number', + dimensionName: 'Vertical axis', + }, + }, + ], + rows: [ + { + var0: 5584.925311203319, + }, + { + var0: 6788.7777444444, + }, + ], + } as any; + act(() => { + onLoad(false, adapters); + }); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.complete, + 2 + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters }); + }); }); diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index ffc9cb82515a5..0046e0b6a87bd 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -10,7 +10,7 @@ import { useEuiTheme, useResizeObserver } from '@elastic/eui'; import { css } from '@emotion/react'; import React, { useState, useRef, useEffect } from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; -import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; +import type { DefaultInspectorAdapters, Datatable } from '@kbn/expressions-plugin/common'; import type { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import type { estypes } from '@elastic/elasticsearch'; import type { TimeRange } from '@kbn/es-query'; @@ -53,6 +53,29 @@ export interface HistogramProps { withDefaultActions: EmbeddableComponentProps['withDefaultActions']; } +const computeTotalHits = ( + hasLensSuggestions: boolean, + adapterTables: + | { + [key: string]: Datatable; + } + | undefined, + isPlainRecord?: boolean +) => { + if (isPlainRecord && hasLensSuggestions) { + return Object.values(adapterTables ?? {})?.[0]?.rows?.length; + } else if (isPlainRecord && !hasLensSuggestions) { + // ES|QL histogram case + let rowsCount = 0; + Object.values(adapterTables ?? {})?.[0]?.rows.forEach((r) => { + rowsCount += r.rows; + }); + return rowsCount; + } else { + return adapterTables?.unifiedHistogram?.meta?.statistics?.totalCount; + } +}; + export function Histogram({ services: { data, lens, uiSettings }, dataView, @@ -113,10 +136,7 @@ export function Histogram({ } const adapterTables = adapters?.tables?.tables; - const totalHits = - isPlainRecord && hasLensSuggestions - ? Object.values(adapterTables ?? {})?.[0]?.rows?.length - : adapterTables?.unifiedHistogram?.meta?.statistics?.totalCount; + const totalHits = computeTotalHits(hasLensSuggestions, adapterTables, isPlainRecord); onTotalHitsChange?.( isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete, diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts index 187bf76751007..e6f3aeaa3a002 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts @@ -123,7 +123,7 @@ describe('useTotalHits', () => { ...getDeps(), isPlainRecord: true, onTotalHitsChange, - query: { sql: 'select * from test' }, + query: { esql: 'from test' }, }; renderHook(() => useTotalHits(deps)); expect(onTotalHitsChange).toBeCalledTimes(1); diff --git a/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts b/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts index f54b1b5b5c456..2a4523d152066 100644 --- a/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts +++ b/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts @@ -636,7 +636,7 @@ describe('getLensAttributes', () => { }, "fieldName": "", "query": Object { - "sql": "SELECT Dest, AvgTicketPrice FROM \\"kibana_sample_data_flights\\"", + "esql": "FROM \\"kibana_sample_data_flights\\"", }, }, "layers": Object { @@ -675,7 +675,7 @@ describe('getLensAttributes', () => { ], "index": "d3d7af60-4c81-11e8-b3d7-01146121b73d", "query": Object { - "sql": "SELECT Dest, AvgTicketPrice FROM \\"kibana_sample_data_flights\\"", + "esql": "FROM kibana_sample_data_flights | keep Dest, AvgTicketPrice", }, "timeField": "timestamp", }, diff --git a/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts b/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts index c0eeb9448eee7..c9ab0d5220aed 100644 --- a/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts +++ b/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts @@ -136,13 +136,13 @@ describe('useStateProps', () => { `); }); - it('should return the correct props when an SQL query is used', () => { + it('should return the correct props when an ES|QL query is used', () => { const stateService = getStateService({ initialState }); const { result } = renderHook(() => useStateProps({ stateService, dataView: dataViewWithTimefieldMock, - query: { sql: 'SELECT * FROM index' }, + query: { esql: 'FROM index' }, requestAdapter: new RequestAdapter(), searchSessionId: '123', }) @@ -222,7 +222,7 @@ describe('useStateProps', () => { useStateProps({ stateService, dataView: dataViewWithTimefieldMock, - query: { sql: 'SELECT * FROM index' }, + query: { esql: 'FROM index' }, requestAdapter: new RequestAdapter(), searchSessionId: '123', }) diff --git a/src/plugins/unified_histogram/public/layout/hooks/compute_interval.test.ts b/src/plugins/unified_histogram/public/layout/hooks/compute_interval.test.ts new file mode 100644 index 0000000000000..04a94d172d1ac --- /dev/null +++ b/src/plugins/unified_histogram/public/layout/hooks/compute_interval.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { calculateBounds } from '@kbn/data-plugin/public'; +import { computeInterval } from './compute_interval'; + +describe('computeInterval', () => { + const dataMock = dataPluginMock.createStartContract(); + dataMock.query.timefilter.timefilter.getTime = () => { + return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; + }; + dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { + return calculateBounds(timeRange); + }; + + it('should return correct interval for 24 hours timerange', () => { + expect( + computeInterval( + { + from: '2023-08-15T10:00:00.000Z', + to: '2023-08-16T10:17:34.591Z', + }, + dataMock + ) + ).toEqual('30 minute'); + }); + + it('should return correct interval for 7 days timerange', () => { + expect( + computeInterval( + { + from: '2023-08-08T21:00:00.000Z', + to: '2023-08-16T10:18:56.569Z', + }, + dataMock + ) + ).toEqual('3 hour'); + }); + + it('should return correct interval for 1 month timerange', () => { + expect( + computeInterval( + { + from: '2023-07-16T21:00:00.000Z', + to: '2023-08-16T10:19:43.573Z', + }, + dataMock + ) + ).toEqual('12 hour'); + }); + + it('should return correct interval for 1 year timerange', () => { + expect( + computeInterval( + { + from: '2022-08-15T21:00:00.000Z', + to: '2023-08-16T10:21:18.589Z', + }, + dataMock + ) + ).toEqual('1 week'); + }); +}); diff --git a/src/plugins/unified_histogram/public/layout/hooks/compute_interval.ts b/src/plugins/unified_histogram/public/layout/hooks/compute_interval.ts new file mode 100644 index 0000000000000..bf6270e8255c7 --- /dev/null +++ b/src/plugins/unified_histogram/public/layout/hooks/compute_interval.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { TimeRange } from '@kbn/es-query'; + +// follows the same logic with vega auto_date function +// we could move to a package and reuse in the future +const barTarget = 50; // same as vega +const roundInterval = (interval: number) => { + { + switch (true) { + case interval <= 500: // <= 0.5s + return '100 millisecond'; + case interval <= 5000: // <= 5s + return '1 second'; + case interval <= 7500: // <= 7.5s + return '5 second'; + case interval <= 15000: // <= 15s + return '10 second'; + case interval <= 45000: // <= 45s + return '30 second'; + case interval <= 180000: // <= 3m + return '1 minute'; + case interval <= 450000: // <= 9m + return '5 minute'; + case interval <= 1200000: // <= 20m + return '10 minute'; + case interval <= 2700000: // <= 45m + return '30 minute'; + case interval <= 7200000: // <= 2h + return '1 hour'; + case interval <= 21600000: // <= 6h + return '3 hour'; + case interval <= 86400000: // <= 24h + return '12 hour'; + case interval <= 604800000: // <= 1w + return '24 hour'; + case interval <= 1814400000: // <= 3w + return '1 week'; + case interval < 3628800000: // < 2y + return '30 day'; + default: + return '1 year'; + } + } +}; + +export const computeInterval = (timeRange: TimeRange, data: DataPublicPluginStart): string => { + const bounds = data.query.timefilter.timefilter.calculateBounds(timeRange!); + const min = bounds.min!.valueOf(); + const max = bounds.max!.valueOf(); + const interval = (max - min) / barTarget; + return roundInterval(interval); +}; diff --git a/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts b/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts index e4936b9345a6d..b6d02753d140a 100644 --- a/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts +++ b/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts @@ -5,13 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { DataView } from '@kbn/data-views-plugin/common'; -import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { + AggregateQuery, + isOfAggregateQueryType, + getAggregateQueryMode, + Query, + TimeRange, +} from '@kbn/es-query'; import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { LensSuggestionsApi, Suggestion } from '@kbn/lens-plugin/public'; import { isEqual } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { computeInterval } from './compute_interval'; +const TRANSFORMATIONAL_COMMANDS = ['stats', 'project', 'keep']; export const useLensSuggestions = ({ dataView, @@ -19,6 +27,8 @@ export const useLensSuggestions = ({ originalSuggestion, isPlainRecord, columns, + data, + timeRange, lensSuggestionsApi, onSuggestionChange, }: { @@ -27,6 +37,8 @@ export const useLensSuggestions = ({ originalSuggestion?: Suggestion; isPlainRecord?: boolean; columns?: DatatableColumn[]; + data: DataPublicPluginStart; + timeRange?: TimeRange; lensSuggestionsApi: LensSuggestionsApi; onSuggestionChange?: (suggestion: Suggestion | undefined) => void; }) => { @@ -50,6 +62,63 @@ export const useLensSuggestions = ({ const currentSuggestion = originalSuggestion ?? suggestions.firstSuggestion; const suggestionDeps = useRef(getSuggestionDeps({ dataView, query, columns })); + const histogramSuggestion = useMemo(() => { + if ( + !currentSuggestion && + dataView.isTimeBased() && + query && + isOfAggregateQueryType(query) && + getAggregateQueryMode(query) === 'esql' && + timeRange + ) { + let queryHasTransformationalCommands = false; + if ('esql' in query) { + TRANSFORMATIONAL_COMMANDS.forEach((command: string) => { + if (query.esql.toLowerCase().includes(command)) { + queryHasTransformationalCommands = true; + return; + } + }); + } + + if (queryHasTransformationalCommands) return undefined; + + const interval = computeInterval(timeRange, data); + const language = getAggregateQueryMode(query); + const histogramQuery = `${query[language]} | eval uniqueName = 1 + | EVAL timestamp=DATE_TRUNC(${interval}, ${dataView.timeFieldName}) | stats rows = count(uniqueName) by timestamp | rename timestamp as \`${dataView.timeFieldName} every ${interval}\``; + const context = { + dataViewSpec: dataView?.toSpec(), + fieldName: '', + textBasedColumns: [ + { + id: `${dataView.timeFieldName} every ${interval}`, + name: `${dataView.timeFieldName} every ${interval}`, + meta: { + type: 'date', + }, + }, + { + id: 'rows', + name: 'rows', + meta: { + type: 'number', + }, + }, + ] as DatatableColumn[], + query: { + esql: histogramQuery, + }, + }; + const sug = lensSuggestionsApi(context, dataView, ['lnsDatatable']) ?? []; + if (sug.length) { + return sug[0]; + } + return undefined; + } + return undefined; + }, [currentSuggestion, dataView, query, timeRange, data, lensSuggestionsApi]); + useEffect(() => { const newSuggestionsDeps = getSuggestionDeps({ dataView, query, columns }); @@ -70,8 +139,9 @@ export const useLensSuggestions = ({ return { allSuggestions, - currentSuggestion, - suggestionUnsupported: !currentSuggestion && !dataView.isTimeBased(), + currentSuggestion: histogramSuggestion ?? currentSuggestion, + suggestionUnsupported: !currentSuggestion && !histogramSuggestion && isPlainRecord, + isOnHistogramMode: Boolean(histogramSuggestion), }; }; diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index d0414f0682489..d2088d4776445 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -203,15 +203,18 @@ export const UnifiedHistogramLayout = ({ children, withDefaultActions, }: UnifiedHistogramLayoutProps) => { - const { allSuggestions, currentSuggestion, suggestionUnsupported } = useLensSuggestions({ - dataView, - query, - originalSuggestion, - isPlainRecord, - columns, - lensSuggestionsApi, - onSuggestionChange, - }); + const { allSuggestions, currentSuggestion, suggestionUnsupported, isOnHistogramMode } = + useLensSuggestions({ + dataView, + query, + originalSuggestion, + isPlainRecord, + columns, + timeRange, + data: services.data, + lensSuggestionsApi, + onSuggestionChange, + }); const chart = suggestionUnsupported ? undefined : originalChart; @@ -287,6 +290,7 @@ export const UnifiedHistogramLayout = ({ onFilter={onFilter} onBrushEnd={onBrushEnd} lensTablesAdapter={lensTablesAdapter} + isOnHistogramMode={isOnHistogramMode} withDefaultActions={withDefaultActions} /> diff --git a/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx index 2b4f3257f2bf5..5cf4795e1ee75 100644 --- a/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx +++ b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx @@ -535,7 +535,7 @@ storiesOf('SearchBar', module) ], } as unknown as SearchBarProps) ) - .add('with dataviewPicker with SQL', () => + .add('with dataviewPicker with ESQL', () => wrapSearchBarInContext({ dataViewPickerComponentProps: { currentDataViewId: '1234', @@ -547,66 +547,66 @@ storiesOf('SearchBar', module) onChangeDataView: action('onChangeDataView'), onAddField: action('onAddField'), onDataViewCreated: action('onDataViewCreated'), - textBasedLanguages: ['SQL'], + textBasedLanguages: ['ESQL'], }, } as SearchBarProps) ) - .add('with dataviewPicker with SQL and sql query', () => + .add('with dataviewPicker with ESQL and ESQL query', () => wrapSearchBarInContext({ dataViewPickerComponentProps: { currentDataViewId: '1234', trigger: { 'data-test-subj': 'dataView-switch-link', - label: 'SQL', - title: 'SQL', + label: 'ESQL', + title: 'ESQL', }, onChangeDataView: action('onChangeDataView'), onAddField: action('onAddField'), onDataViewCreated: action('onDataViewCreated'), - textBasedLanguages: ['SQL'], + textBasedLanguages: ['ESQL'], }, - query: { sql: 'SELECT field1, field2 FROM DATAVIEW' }, + query: { esql: 'from dataview | project field1, field2' }, } as unknown as SearchBarProps) ) - .add('with dataviewPicker with SQL and large sql query', () => + .add('with dataviewPicker with ESQL and large ESQL query', () => wrapSearchBarInContext({ dataViewPickerComponentProps: { currentDataViewId: '1234', trigger: { 'data-test-subj': 'dataView-switch-link', - label: 'SQL', - title: 'SQL', + label: 'ESQL', + title: 'ESQL', }, onChangeDataView: action('onChangeDataView'), onAddField: action('onAddField'), onDataViewCreated: action('onDataViewCreated'), - textBasedLanguages: ['SQL'], + textBasedLanguages: ['ESQL'], }, query: { - sql: 'SELECT field1, field2, field 3, field 4, field 5 FROM DATAVIEW WHERE field5 IS NOT NULL AND field4 IS NULL', + esql: 'from dataview | project field1, field2, field 3, field 4, field 5 | where field5 > 5 | stats var = avg(field3)', }, } as unknown as SearchBarProps) ) - .add('with dataviewPicker with SQL and errors in sql query', () => + .add('with dataviewPicker with ESQL and errors in ESQL query', () => wrapSearchBarInContext({ dataViewPickerComponentProps: { currentDataViewId: '1234', trigger: { 'data-test-subj': 'dataView-switch-link', - label: 'SQL', - title: 'SQL', + label: 'ESQL', + title: 'ESQL', }, onChangeDataView: action('onChangeDataView'), onAddField: action('onAddField'), onDataViewCreated: action('onDataViewCreated'), - textBasedLanguages: ['SQL'], + textBasedLanguages: ['ESQL'], }, textBasedLanguageModeErrors: [ new Error( - '[essql] > Unexpected error from Elasticsearch: verification_exception - Found 1 problem line 1:16: Unknown column [field10]' + '[esql] > Unexpected error from Elasticsearch: verification_exception - Found 1 problem line 1:16: Unknown column [field10]' ), ], - query: { sql: 'SELECT field1, field10 FROM DATAVIEW' }, + query: { esql: 'from dataview | project field10' }, } as unknown as SearchBarProps) ) .add('in disabled state', () => diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx index be359f25a6496..7492018b3a6c2 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx @@ -137,8 +137,8 @@ describe('DataView component', () => { wrapDataViewComponentInContext( { ...props, - textBasedLanguages: [TextBasedLanguages.ESQL, TextBasedLanguages.SQL], - textBasedLanguage: TextBasedLanguages.SQL, + textBasedLanguages: [TextBasedLanguages.ESQL], + textBasedLanguage: TextBasedLanguages.ESQL, }, false ) diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx index 72f777cfcae93..7d04fd5fa3621 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -11,6 +11,8 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { css } from '@emotion/react'; import { EuiPopover, + EuiPanel, + EuiBadge, EuiHorizontalRule, EuiButton, EuiContextMenuPanel, @@ -26,11 +28,11 @@ import { EuiToolTip, } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { AggregateQuery, getLanguageDisplayName } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { IUnifiedSearchPluginServices } from '../types'; -import type { DataViewPickerPropsExtended } from './data_view_picker'; +import { type DataViewPickerPropsExtended } from './data_view_picker'; import type { DataViewListItemEnhanced } from './dataview_list'; -import type { TextBasedLanguagesListProps } from './text_languages_list'; import type { TextBasedLanguagesTransitionModalProps } from './text_languages_transition_modal'; import adhoc from './assets/adhoc.svg'; import { changeDataViewStyles } from './change_dataview.styles'; @@ -52,13 +54,6 @@ export const TextBasedLanguagesTransitionModal = ( ); -const LazyTextBasedLanguagesList = React.lazy(() => import('./text_languages_list')); -export const TextBasedLanguagesList = (props: TextBasedLanguagesListProps) => ( - }> - - -); - const mapAdHocDataView = (adHocDataView: DataView) => { return { title: adHocDataView.title, @@ -122,7 +117,7 @@ export function ChangeDataView({ useEffect(() => { if (textBasedLanguage) { - setTriggerLabel(textBasedLanguage.toUpperCase()); + setTriggerLabel(getLanguageDisplayName(textBasedLanguage).toUpperCase()); } else { setTriggerLabel(trigger.label); } @@ -245,7 +240,8 @@ export function ChangeDataView({ 'unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning', { defaultMessage: - "Switching data views removes the current SQL query. Save this search to ensure you don't lose work.", + "Switching data views removes the current {textBasedLanguage} query. Save this search to ensure you don't lose work.", + values: { textBasedLanguage }, } )} > @@ -335,42 +331,24 @@ export function ChangeDataView({ if (textBasedLanguages?.length) { panelItems.push( , - - - -
- {i18n.translate( - 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesLabel', - { - defaultMessage: 'Text-based query languages', - } - )} -
-
-
-
, - { - setTriggerLabel(lang); - setPopoverIsOpen(false); - setIsTextBasedLangSelected(true); - // also update the query with the sql query - onTextLangQuerySubmit?.({ sql: `SELECT * FROM "${trigger.title}"` }); - }} - /> + + onTextBasedSubmit({ esql: `from ${trigger.title} | limit 10` })} + data-test-subj="select-text-based-language-panel" + > + {i18n.translate('unifiedSearch.query.queryBar.textBasedLanguagesTryLabel', { + defaultMessage: 'Try ES|QL', + })} + + {i18n.translate('unifiedSearch.query.queryBar.textBasedLanguagesTechPreviewLabel', { + defaultMessage: 'Technical preview', + })} + + + ); } @@ -384,6 +362,14 @@ export function ChangeDataView({ setIsTextLangTransitionModalDismissed(true); }, [storage]); + const onTextBasedSubmit = useCallback( + (q: AggregateQuery) => { + onTextLangQuerySubmit?.(q); + setPopoverIsOpen(false); + }, + [onTextLangQuerySubmit] + ); + const cleanup = useCallback( (shouldDismissModal: boolean) => { setIsTextLangTransitionModalVisible(false); @@ -434,6 +420,7 @@ export function ChangeDataView({ ); } diff --git a/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx b/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx index 77fe947061480..c02908007200f 100644 --- a/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx +++ b/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx @@ -35,7 +35,6 @@ export const ExploreMatchingButton = ({ alignItems="center" gutterSize="none" justifyContent="spaceBetween" - data-test-subj="select-text-based-language-panel" css={css` margin: ${euiTheme.size.s}; margin-bottom: 0; diff --git a/src/plugins/unified_search/public/dataview_picker/text_languages_list.test.tsx b/src/plugins/unified_search/public/dataview_picker/text_languages_list.test.tsx deleted file mode 100644 index deb619b236338..0000000000000 --- a/src/plugins/unified_search/public/dataview_picker/text_languages_list.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { MouseEvent } from 'react'; -import { EuiSelectable } from '@elastic/eui'; -import { act } from 'react-dom/test-utils'; -import { ShallowWrapper } from 'enzyme'; -import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; -import TextBasedLanguagesList, { TextBasedLanguagesListProps } from './text_languages_list'; -import { TextBasedLanguages } from './data_view_picker'; - -function getTextLanguagesPickerList(instance: ShallowWrapper) { - return instance.find(EuiSelectable).first(); -} - -function getTextLanguagesPickerOptions(instance: ShallowWrapper) { - return getTextLanguagesPickerList(instance).prop('options'); -} - -function selectTextLanguagePickerOption(instance: ShallowWrapper, selectedLabel: string) { - const event = {} as MouseEvent; - const options: Array<{ label: string; checked?: 'on' | 'off' }> = getTextLanguagesPickerOptions( - instance - ).map((option: { label: string }) => - option.label === selectedLabel - ? { ...option, checked: 'on' } - : { ...option, checked: undefined } - ); - const selectedOption = { label: selectedLabel }; - return getTextLanguagesPickerList(instance).prop('onChange')!(options, event, selectedOption); -} - -describe('Text based languages list component', () => { - const changeLanguageSpy = jest.fn(); - let props: TextBasedLanguagesListProps; - beforeEach(() => { - props = { - selectedOption: 'ESQL', - onChange: changeLanguageSpy, - textBasedLanguages: [TextBasedLanguages.ESQL, TextBasedLanguages.SQL], - }; - }); - it('should trigger the onChange if a new language is selected', async () => { - const component = shallow(); - await act(async () => { - selectTextLanguagePickerOption(component, 'SQL'); - }); - expect(changeLanguageSpy).toHaveBeenCalled(); - }); - - it('should list all languages', () => { - const component = shallow(); - - expect(getTextLanguagesPickerOptions(component)!.map((option: any) => option.label)).toEqual([ - 'ESQL', - 'SQL', - ]); - }); -}); diff --git a/src/plugins/unified_search/public/dataview_picker/text_languages_list.tsx b/src/plugins/unified_search/public/dataview_picker/text_languages_list.tsx deleted file mode 100644 index ac4717ad40d48..0000000000000 --- a/src/plugins/unified_search/public/dataview_picker/text_languages_list.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiSelectable, EuiPanel, EuiBadge } from '@elastic/eui'; -import { TextBasedLanguages } from './data_view_picker'; - -export interface TextBasedLanguagesListProps { - textBasedLanguages: TextBasedLanguages[]; - onChange: (lang: string) => void; - selectedOption: string; -} - -// Needed for React.lazy -// eslint-disable-next-line import/no-default-export -export default function TextBasedLanguagesList({ - textBasedLanguages, - onChange, - selectedOption, -}: TextBasedLanguagesListProps) { - return ( - - key="textbasedLanguages-options" - data-test-subj="text-based-languages-switcher" - singleSelection="always" - options={textBasedLanguages.map((lang) => ({ - key: lang, - label: lang, - value: lang, - checked: lang === selectedOption ? 'on' : undefined, - append: ( - - {i18n.translate('unifiedSearch.query.queryBar.textBasedLanguagesTechPreviewLabel', { - defaultMessage: 'Technical preview', - })} - - ), - }))} - onChange={(choices) => { - const choice = choices.find(({ checked }) => checked) as unknown as { - value: string; - }; - onChange(choice.value); - }} - > - {(list) => ( - - {list} - - )} - - ); -} diff --git a/src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx b/src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx index f7eb0c4a74a22..c59599907456b 100644 --- a/src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx +++ b/src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx @@ -25,18 +25,21 @@ import { export interface TextBasedLanguagesTransitionModalProps { closeModal: (dismissFlag: boolean, needsSave?: boolean) => void; setIsTextLangTransitionModalVisible: (flag: boolean) => void; + textBasedLanguage?: string; } // Needed for React.lazy // eslint-disable-next-line import/no-default-export export default function TextBasedLanguagesTransitionModal({ closeModal, setIsTextLangTransitionModalVisible, + textBasedLanguage, }: TextBasedLanguagesTransitionModalProps) { const [dismissModalChecked, setDismissModalChecked] = useState(false); const onTransitionModalDismiss = useCallback((e) => { setDismissModalChecked(e.target.checked); }, []); + const language = textBasedLanguage?.toUpperCase(); return ( setIsTextLangTransitionModalVisible(false)} style={{ width: 700 }}> @@ -56,7 +59,8 @@ export default function TextBasedLanguagesTransitionModal({ 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody', { defaultMessage: - "Switching data views removes the current SQL query. Save this search to ensure you don't lose work.", + "Switching data views removes the current {language} query. Save this search to ensure you don't lose work.", + values: { language }, } )}
diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 6e72baf29c7b1..0a20154b0d0b6 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -12,7 +12,12 @@ import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } f import deepEqual from 'fast-deep-equal'; import useObservable from 'react-use/lib/useObservable'; import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query'; -import { getAggregateQueryMode, isOfQueryType, isOfAggregateQueryType } from '@kbn/es-query'; +import { + getAggregateQueryMode, + isOfQueryType, + isOfAggregateQueryType, + getLanguageDisplayName, +} from '@kbn/es-query'; import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; import { EMPTY } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -79,13 +84,14 @@ const getWrapperWithTooltip = ( ) => { if (enableTooltip && query && isOfAggregateQueryType(query)) { const textBasedLanguage = getAggregateQueryMode(query); + const displayName = getLanguageDisplayName(textBasedLanguage); return ( {children} @@ -138,6 +144,7 @@ export interface QueryBarTopRowProps onFiltersUpdated?: (filters: Filter[]) => void; dataViewPickerComponentProps?: DataViewPickerProps; textBasedLanguageModeErrors?: Error[]; + textBasedLanguageModeWarning?: string; onTextBasedSavedAndExit?: ({ onSave }: OnSaveTextLanguageQueryProps) => void; filterBar?: React.ReactNode; showDatePickerAsBadge?: boolean; @@ -648,6 +655,7 @@ export const QueryBarTopRow = React.memo( expandCodeEditor={(status: boolean) => setCodeEditorIsExpanded(status)} isCodeEditorExpanded={codeEditorIsExpanded} errors={props.textBasedLanguageModeErrors} + warning={props.textBasedLanguageModeWarning} detectTimestamp={detectTimestamp} onTextLangQuerySubmit={() => onSubmit({ diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index 0879a12f31cef..8ffca3fbb0bda 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -247,6 +247,7 @@ export function createSearchBar({ {...overrideDefaultBehaviors(props)} dataViewPickerComponentProps={props.dataViewPickerComponentProps} textBasedLanguageModeErrors={props.textBasedLanguageModeErrors} + textBasedLanguageModeWarning={props.textBasedLanguageModeWarning} onTextBasedSavedAndExit={props.onTextBasedSavedAndExit} displayStyle={props.displayStyle} isScreenshotMode={isScreenshotMode} diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index aa84d4136ed36..b2d3b5abc966c 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -104,6 +104,7 @@ export interface SearchBarOwnProps { fillSubmitButton?: boolean; dataViewPickerComponentProps?: DataViewPickerProps; textBasedLanguageModeErrors?: Error[]; + textBasedLanguageModeWarning?: string; onTextBasedSavedAndExit?: ({ onSave }: OnSaveTextLanguageQueryProps) => void; showSubmitButton?: boolean; submitButtonStyle?: QueryBarTopRowProps['submitButtonStyle']; @@ -597,6 +598,7 @@ class SearchBarUI extends C onFiltersUpdated={this.props.onFiltersUpdated} dataViewPickerComponentProps={this.props.dataViewPickerComponentProps} textBasedLanguageModeErrors={this.props.textBasedLanguageModeErrors} + textBasedLanguageModeWarning={this.props.textBasedLanguageModeWarning} onTextBasedSavedAndExit={this.props.onTextBasedSavedAndExit} showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()} filterBar={filterBar} diff --git a/src/plugins/usage_collection/server/collector/types.ts b/src/plugins/usage_collection/server/collector/types.ts index ff5b7135ba5c0..50a04560e384b 100644 --- a/src/plugins/usage_collection/server/collector/types.ts +++ b/src/plugins/usage_collection/server/collector/types.ts @@ -21,7 +21,7 @@ export type { /** * Helper to find out whether to keep recursively looking or if we are on an end value */ -export type RecursiveMakeSchemaFrom = U extends object +export type RecursiveMakeSchemaFrom = U extends object ? Record extends U ? | { @@ -31,19 +31,21 @@ export type RecursiveMakeSchemaFrom = U extends object description: string; // Intentionally enforcing the descriptions here } & SchemaMetaOptional; } - | MakeSchemaFrom // But still allow being explicit in the definition if they want to. - : MakeSchemaFrom + | MakeSchemaFrom // But still allow being explicit in the definition if they want to. + : MakeSchemaFrom + : RequireMeta extends true + ? { type: PossibleSchemaTypes; _meta: { description: string } } : { type: PossibleSchemaTypes; _meta?: { description: string } }; /** * The `schema` property in {@link CollectorOptions} must match the output of * the `fetch` method. This type helps ensure that is correct */ -export type MakeSchemaFrom = { +export type MakeSchemaFrom = { // Using Required to enforce all optional keys in the object [Key in keyof Required]: Required[Key] extends Array - ? { type: 'array'; items: RecursiveMakeSchemaFrom } - : RecursiveMakeSchemaFrom[Key]>; + ? { type: 'array'; items: RecursiveMakeSchemaFrom } + : RecursiveMakeSchemaFrom[Key], RequireMeta>; }; /** diff --git a/src/setup_node_env/openssl_legacy_provider/index.js b/src/setup_node_env/openssl_legacy_provider/index.js new file mode 100644 index 0000000000000..159c2a5e62e9a --- /dev/null +++ b/src/setup_node_env/openssl_legacy_provider/index.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +var branch = require('../../../package.json').branch; +var docsBranch = branch.match(/^\d\.\d\d?$/) || 'current'; +var openSSLLegacyProviderEnabled = require('./openssl_legacy_provider_enabled')(); + +if (openSSLLegacyProviderEnabled) { + console.log( + 'Kibana is currently running with legacy OpenSSL providers enabled! For details and instructions on how to disable see https://www.elastic.co/guide/en/kibana/' + + docsBranch + + '/production.html#openssl-legacy-provider' + ); +} diff --git a/src/setup_node_env/openssl_legacy_provider/openssl_legacy_provider_enabled.js b/src/setup_node_env/openssl_legacy_provider/openssl_legacy_provider_enabled.js new file mode 100644 index 0000000000000..8c9771b1bf521 --- /dev/null +++ b/src/setup_node_env/openssl_legacy_provider/openssl_legacy_provider_enabled.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +var crypto = require('crypto'); + +// The blowfish cipher is only available when node is running with the --openssl-legacy-provider flag +module.exports = function () { + return crypto.getCiphers().includes('blowfish'); +}; diff --git a/src/setup_node_env/openssl_legacy_provider/openssl_legacy_provider_enabled.test.js b/src/setup_node_env/openssl_legacy_provider/openssl_legacy_provider_enabled.test.js new file mode 100644 index 0000000000000..30772d00bb165 --- /dev/null +++ b/src/setup_node_env/openssl_legacy_provider/openssl_legacy_provider_enabled.test.js @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +var spawnSync = require('child_process').spawnSync; + +describe('openSSLLegacyProviderEnabled', function () { + function runLegacyProviderCheck(execOptions, nodeOptions) { + var result = spawnSync( + process.execPath, + (execOptions ? execOptions.split(' ') : []).concat([ + '-p', + "require('./openssl_legacy_provider_enabled')()", + ]), + { + env: { + NODE_OPTIONS: nodeOptions || '', + }, + encoding: 'utf-8', + cwd: __dirname, + } + ); + var stdout = result.stdout.trim(); + return stdout === 'true'; + } + + it('should be disabled by default', function () { + expect(runLegacyProviderCheck()).toBe(false); + }); + + describe('using NODE_OPTIONS', function () { + it('should be enabled when --openssl-legacy-provider is set', function () { + expect(runLegacyProviderCheck(null, '--openssl-legacy-provider')).toBe(true); + }); + + it('should be enabled when --openssl-legacy-provider is set after --no-openssl-legacy-provider', function () { + expect( + runLegacyProviderCheck(null, '--no-openssl-legacy-provider --openssl-legacy-provider') + ).toBe(true); + }); + + it('should be disabled when --no-openssl-legacy-provider is set', function () { + expect(runLegacyProviderCheck(null, '--no-openssl-legacy-provider')).toBe(false); + }); + + it('should be disabled when --no-openssl-legacy-provider is set after --openssl-legacy-provider', function () { + expect( + runLegacyProviderCheck(null, '--openssl-legacy-provider --no-openssl-legacy-provider') + ).toBe(false); + }); + }); + + describe('using exec arguments', function () { + it('should be enabled when --openssl-legacy-provider is set', function () { + expect(runLegacyProviderCheck('--openssl-legacy-provider')).toBe(true); + }); + + it('should be enabled when --openssl-legacy-provider is set after --no-openssl-legacy-provider', function () { + expect(runLegacyProviderCheck('--no-openssl-legacy-provider --openssl-legacy-provider')).toBe( + true + ); + }); + + it('should be disabled when --no-openssl-legacy-provider is set', function () { + expect(runLegacyProviderCheck('--no-openssl-legacy-provider')).toBe(false); + }); + + it('should be disabled when --no-openssl-legacy-provider is set after --openssl-legacy-provider', function () { + expect(runLegacyProviderCheck('--openssl-legacy-provider --no-openssl-legacy-provider')).toBe( + false + ); + }); + }); +}); diff --git a/src/setup_node_env/setup_env.js b/src/setup_node_env/setup_env.js index 08897eb5a78c5..7b37d98011cfb 100644 --- a/src/setup_node_env/setup_env.js +++ b/src/setup_node_env/setup_env.js @@ -14,3 +14,4 @@ require('./harden'); require('symbol-observable'); require('source-map-support').install(); require('./node_version_validator'); +require('./openssl_legacy_provider'); diff --git a/src/setup_node_env/tsconfig.json b/src/setup_node_env/tsconfig.json index ed753806b9f4f..931afbdfaf0a3 100644 --- a/src/setup_node_env/tsconfig.json +++ b/src/setup_node_env/tsconfig.json @@ -6,6 +6,7 @@ "include": [ "harden/**/*", "root/**/*", + "openssl_legacy_provider/**/*", "*.js", "*.ts", ], diff --git a/test/api_integration/apis/telemetry/telemetry_config.ts b/test/api_integration/apis/telemetry/telemetry_config.ts index a9a04a3986ba7..61a500cef1452 100644 --- a/test/api_integration/apis/telemetry/telemetry_config.ts +++ b/test/api_integration/apis/telemetry/telemetry_config.ts @@ -46,6 +46,7 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) optIn: null, // the config.js for this FTR sets it to `false`, we are bound to ask again. sendUsageFrom: 'server', telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in by default (that's what this flag is about) + labels: {}, }); }); @@ -69,6 +70,7 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) optIn: true, sendUsageFrom: 'server', telemetryNotifyUserAboutOptInDefault: false, + labels: {}, }); }); @@ -92,6 +94,7 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) optIn: false, sendUsageFrom: 'server', telemetryNotifyUserAboutOptInDefault: false, + labels: {}, }); }); @@ -136,6 +139,7 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) optIn: true, sendUsageFrom: 'server', telemetryNotifyUserAboutOptInDefault: false, + labels: {}, }); }); @@ -158,6 +162,7 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) optIn: null, sendUsageFrom: 'server', telemetryNotifyUserAboutOptInDefault: false, + labels: {}, }); }); }); diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index 9d3bfa9c25b81..db75c82f293ac 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -56,8 +56,7 @@ export default function ({ getService }: FtrProviderContext) { return savedObject; }; - // FLAKY: https://github.com/elastic/kibana/issues/98240 - describe.skip('UI Counters API', () => { + describe('UI Counters API', () => { const dayDate = moment().format('DDMMYYYY'); before(async () => await esArchiver.emptyKibanaIndex()); diff --git a/test/examples/search/warnings.ts b/test/examples/search/warnings.ts index 24656ed68285c..3f2166447f652 100644 --- a/test/examples/search/warnings.ts +++ b/test/examples/search/warnings.ts @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); - describe('handling warnings with search source fetch', function () { + // Failing: See https://github.com/elastic/kibana/issues/139879 + describe.skip('handling warnings with search source fetch', function () { const dataViewTitle = 'sample-01,sample-01-rollup'; const fromTime = 'Jun 17, 2022 @ 00:00:00.000'; const toTime = 'Jun 23, 2022 @ 00:00:00.000'; @@ -167,6 +168,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // click "see full error" button in the toast const [openShardModalButton] = await testSubjects.findAll('openShardFailureModalBtn'); await openShardModalButton.click(); + await testSubjects.exists('shardFailureModalTitle'); const modalHeader = await testSubjects.find('shardFailureModalTitle'); expect(await modalHeader.getVisibleText()).to.be('2 of 4 shards failed'); // request diff --git a/test/examples/unified_field_list_examples/field_stats.ts b/test/examples/unified_field_list_examples/field_stats.ts index de662e737cb2d..0c1ebca9f3a11 100644 --- a/test/examples/unified_field_list_examples/field_stats.ts +++ b/test/examples/unified_field_list_examples/field_stats.ts @@ -52,7 +52,8 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await PageObjects.unifiedFieldList.cleanSidebarLocalStorage(); }); - describe('field distribution', () => { + // FLAKY: https://github.com/elastic/kibana/issues/165608 + describe.skip('field distribution', () => { before(async () => { await PageObjects.unifiedFieldList.toggleSidebarSection('empty'); // it will allow to render more fields in Available fields section }); diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index af5e9e6741197..8f2a927a4ff24 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -14,7 +14,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'console']); + const PageObjects = getPageObjects(['common', 'console', 'header']); const find = getService('find'); describe('console autocomplete feature', function describeIndexTests() { @@ -44,11 +44,160 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(PageObjects.console.isAutocompleteVisible()).to.be.eql(true); }); + // FLAKY: https://github.com/elastic/kibana/issues/165465 + describe.skip('Autocomplete behavior', () => { + beforeEach(async () => { + await PageObjects.console.clearTextArea(); + await PageObjects.console.pressEnter(); + }); + + it('HTTP methods', async () => { + const suggestions = { + G: ['GET'], + P: ['PUT', 'POST'], + D: ['DELETE'], + H: ['HEAD'], + }; + for (const [char, methods] of Object.entries(suggestions)) { + await PageObjects.console.sleepForDebouncePeriod(); + log.debug('Key type "%s"', char); + await PageObjects.console.enterText(char); + + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.isAutocompleteVisible() + ); + expect(await PageObjects.console.isAutocompleteVisible()).to.be.eql(true); + + for (const [i, method] of methods.entries()) { + expect(await PageObjects.console.getAutocompleteSuggestion(i)).to.be.eql(method); + } + + await PageObjects.console.pressEscape(); + await PageObjects.console.pressEnter(); + } + }); + + it('ES API endpoints', async () => { + const suggestions = { + 'GET _': ['_alias', '_all'], + 'PUT _': ['_all'], + 'POST _': ['_aliases', '_all'], + 'DELETE _': ['_all'], + 'HEAD _': ['_alias', '_all'], + }; + for (const [text, endpoints] of Object.entries(suggestions)) { + for (const char of text) { + await PageObjects.console.sleepForDebouncePeriod(); + log.debug('Key type "%s"', char); + await PageObjects.console.enterText(char); + } + + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.isAutocompleteVisible() + ); + expect(await PageObjects.console.isAutocompleteVisible()).to.be.eql(true); + + for (const [i, endpoint] of endpoints.entries()) { + expect(await PageObjects.console.getAutocompleteSuggestion(i)).to.be.eql(endpoint); + } + + await PageObjects.console.pressEscape(); + await PageObjects.console.pressEnter(); + } + }); + + it('JSON autocompletion with placeholder fields', async () => { + await PageObjects.console.enterText('GET _search\n{'); + await PageObjects.console.pressEnter(); + + for (const char of '"ag') { + await PageObjects.console.sleepForDebouncePeriod(); + log.debug('Key type "%s"', char); + await PageObjects.console.enterText(char); + } + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.isAutocompleteVisible() + ); + expect(await PageObjects.console.isAutocompleteVisible()).to.be.eql(true); + await PageObjects.console.pressEnter(); + await PageObjects.console.sleepForDebouncePeriod(); + + expect(await PageObjects.console.getAllVisibleText()).to.be.eql( + ` +GET _search +{ + "aggs": { + "NAME": { + "AGG_TYPE": {} + } + } +} +`.replace(/\n/g, '') + ); + // cursor should be located between '"' and 'N' + expect(await PageObjects.console.getCurrentLineNumber()).to.be.eql(5); + + await PageObjects.console.pressDown(); + await PageObjects.console.pressRight(); + await PageObjects.console.pressRight(); + for (let i = 0; i < 8; i++) { + await PageObjects.console.pressRight(true); // select 'AGG_TYPE' + } + + for (const char of 'ter') { + await PageObjects.console.sleepForDebouncePeriod(); + log.debug('Key type "%s"', char); + await PageObjects.console.enterText(char); + } + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.isAutocompleteVisible() + ); + expect(await PageObjects.console.isAutocompleteVisible()).to.be.eql(true); + await PageObjects.console.pressEnter(); + await PageObjects.console.sleepForDebouncePeriod(); + + expect(await PageObjects.console.getAllVisibleText()).to.be.eql( + ` +GET _search +{ + "aggs": { + "NAME": { + "terms": { + "field": "" + } + } + } +} +`.replace(/\n/g, '') + ); + }); + + it('Dynamic autocomplete', async () => { + await PageObjects.console.enterRequest('POST test/_doc\n{}'); + await PageObjects.console.clickPlay(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.console.getResponseStatus()).to.be('201'); + + await PageObjects.console.pressEnter(); + for (const char of 'POST t') { + await PageObjects.console.sleepForDebouncePeriod(); + log.debug('Key type "%s"', char); + await PageObjects.console.enterText(char); + } + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.isAutocompleteVisible() + ); + expect(await PageObjects.console.isAutocompleteVisible()).to.be.eql(true); + + expect(await PageObjects.console.getAutocompleteSuggestion(0)).to.be.eql('test'); + }); + }); + // FLAKY: https://github.com/elastic/kibana/issues/164584 describe.skip('anti-regression watchdogs', () => { beforeEach(async () => { await PageObjects.console.clearTextArea(); - await PageObjects.console.pressEnter(); }); it('should suppress auto-complete on arrow keys', async () => { @@ -92,7 +241,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { for (const method of methods) { await PageObjects.console.clearTextArea(); - await PageObjects.console.pressEnter(); for (const char of method.slice(0, -1)) { await PageObjects.console.sleepForDebouncePeriod(); diff --git a/test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts b/test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts index 6a6294d1869d1..73e97aab3cb36 100644 --- a/test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts +++ b/test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts @@ -102,14 +102,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('does not show unsaved changes on new dashboard when no panels have been added', async () => { - await PageObjects.dashboard.expectUnsavedChangesDoesNotExist(unsavedDashboardTitle); + await PageObjects.dashboard.expectUnsavedChangesListingDoesNotExist(unsavedDashboardTitle); }); it('can discard unsaved changes using the discard link', async () => { await PageObjects.dashboard.clickUnsavedChangesDiscard( `discard-unsaved-${dashboardTitle.split(' ').join('-')}` ); - await PageObjects.dashboard.expectUnsavedChangesDoesNotExist(dashboardTitle); + await PageObjects.dashboard.expectUnsavedChangesListingDoesNotExist(dashboardTitle); await PageObjects.dashboard.loadSavedDashboard(dashboardTitle); await PageObjects.dashboard.switchToEditMode(); const currentPanelCount = await PageObjects.dashboard.getPanelCount(); @@ -132,7 +132,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.saveDashboard(newDashboartTitle); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.expectUnsavedChangesDoesNotExist(unsavedDashboardTitle); + await PageObjects.dashboard.expectUnsavedChangesListingDoesNotExist(unsavedDashboardTitle); }); it('does not list unsaved changes when unsaved version of the dashboard is the same', async () => { @@ -167,7 +167,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Check that it now does not exist await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.expectUnsavedChangesDoesNotExist(newDashboartTitle); + await PageObjects.dashboard.expectUnsavedChangesListingDoesNotExist(newDashboartTitle); }); }); } diff --git a/test/functional/apps/discover/group1/_discover.ts b/test/functional/apps/discover/group1/_discover.ts index a3cf7b2d29fcf..286a30a6bb227 100644 --- a/test/functional/apps/discover/group1/_discover.ts +++ b/test/functional/apps/discover/group1/_discover.ts @@ -31,7 +31,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover test', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/146223 + describe.skip('discover test', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); diff --git a/test/functional/apps/discover/group2/_data_grid_footer.ts b/test/functional/apps/discover/group2/_data_grid_footer.ts index a1d2ff0294d23..4cc7b96a52607 100644 --- a/test/functional/apps/discover/group2/_data_grid_footer.ts +++ b/test/functional/apps/discover/group2/_data_grid_footer.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; -const FOOTER_SELECTOR = 'discoverTableFooter'; +const FOOTER_SELECTOR = 'unifiedDataTableFooter'; const LOAD_MORE_SELECTOR = 'dscGridSampleSizeFetchMoreLink'; export default function ({ getService, getPageObjects }: FtrProviderContext) { diff --git a/test/functional/apps/discover/group2/_data_grid_pagination.ts b/test/functional/apps/discover/group2/_data_grid_pagination.ts index 7da02308c73be..4d0c81c4cfebc 100644 --- a/test/functional/apps/discover/group2/_data_grid_pagination.ts +++ b/test/functional/apps/discover/group2/_data_grid_pagination.ts @@ -52,18 +52,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show footer only for the last page', async () => { // footer is not shown - await testSubjects.missingOrFail('discoverTableFooter'); + await testSubjects.missingOrFail('unifiedDataTableFooter'); // go to next page await testSubjects.click('pagination-button-next'); // footer is not shown yet await retry.try(async function () { - await testSubjects.missingOrFail('discoverTableFooter'); + await testSubjects.missingOrFail('unifiedDataTableFooter'); }); // go to the last page await testSubjects.click('pagination-button-4'); // footer is shown now await retry.try(async function () { - await testSubjects.existOrFail('discoverTableFooter'); + await testSubjects.existOrFail('unifiedDataTableFooter'); }); }); @@ -80,7 +80,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async function () { return !testSubjects.exists('pagination-button-1'); // only page 0 is left }); - await testSubjects.existOrFail('discoverTableFooter'); + await testSubjects.existOrFail('unifiedDataTableFooter'); }); it('should render exact number of rows which where configured in the saved search or in settings', async () => { diff --git a/test/functional/apps/discover/group2/_sql_view.ts b/test/functional/apps/discover/group2/_esql_view.ts similarity index 77% rename from test/functional/apps/discover/group2/_sql_view.ts rename to test/functional/apps/discover/group2/_esql_view.ts index 95ce1516728d2..1f7493b6ff905 100644 --- a/test/functional/apps/discover/group2/_sql_view.ts +++ b/test/functional/apps/discover/group2/_esql_view.ts @@ -28,10 +28,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const defaultSettings = { defaultIndex: 'logstash-*', - 'discover:enableSql': true, + 'discover:enableESQL': true, }; - describe('discover sql view', async function () { + describe('discover esql view', async function () { before(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); log.debug('load kibana index with default index pattern'); @@ -44,7 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('test', () => { - it('should render sql view correctly', async function () { + it('should render esql view correctly', async function () { await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); expect(await testSubjects.exists('showQueryBarMenu')).to.be(true); @@ -62,7 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('field-@message-showDetails'); expect(await testSubjects.exists('discoverFieldListPanelEdit-@message')).to.be(true); - await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.discover.selectTextBaseLang(); await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); expect(await testSubjects.exists('fieldListFiltersFieldSearch')).to.be(true); @@ -72,12 +72,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('showQueryBarMenu')).to.be(false); expect(await testSubjects.exists('addFilter')).to.be(false); expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(false); - // when Lens suggests a table, we render the histogram + // when Lens suggests a table, we render an ESQL based histogram expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(true); expect(await testSubjects.exists('discoverAlertsButton')).to.be(false); expect(await testSubjects.exists('shareTopNavButton')).to.be(true); - expect(await testSubjects.exists('dataGridColumnSortingButton')).to.be(true); + expect(await testSubjects.exists('dataGridColumnSortingButton')).to.be(false); expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(true); expect(await testSubjects.exists('fieldListFiltersFieldTypeFilterToggle')).to.be(true); await testSubjects.click('field-@message-showDetails'); @@ -85,33 +85,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should perform test query correctly', async function () { - await PageObjects.discover.selectTextBaseLang('SQL'); - const testQuery = `SELECT "@tags", geo.dest, count(*) occurred FROM "logstash-*" - GROUP BY "@tags", geo.dest - HAVING occurred > 20 - ORDER BY occurred DESC`; + await PageObjects.discover.selectTextBaseLang(); + const testQuery = `from logstash-* | limit 10 | stats countB = count(bytes) by geo.dest | sort countB`; await monacoEditor.setCodeEditorValue(testQuery); await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); - // here Lens suggests a heatmap so it is rendered + // here Lens suggests a XY so it is rendered expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); - expect(await testSubjects.exists('heatmapChart')).to.be(true); - const cell = await dataGrid.getCellElement(0, 4); - expect(await cell.getVisibleText()).to.be('2269'); + expect(await testSubjects.exists('xyVisChart')).to.be(true); + const cell = await dataGrid.getCellElement(0, 2); + expect(await cell.getVisibleText()).to.be('1'); }); it('should render when switching to a time range with no data, then back to a time range with data', async () => { - await PageObjects.discover.selectTextBaseLang('SQL'); - const testQuery = `SELECT "@tags", geo.dest, count(*) occurred FROM "logstash-*" - GROUP BY "@tags", geo.dest - HAVING occurred > 20 - ORDER BY occurred DESC`; + await PageObjects.discover.selectTextBaseLang(); + const testQuery = `from logstash-* | limit 10 | stats countB = count(bytes) by geo.dest | sort countB`; await monacoEditor.setCodeEditorValue(testQuery); await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); - let cell = await dataGrid.getCellElement(0, 4); - expect(await cell.getVisibleText()).to.be('2269'); + let cell = await dataGrid.getCellElement(0, 2); + expect(await cell.getVisibleText()).to.be('1'); await PageObjects.timePicker.setAbsoluteRange( 'Sep 19, 2015 @ 06:31:44.000', 'Sep 19, 2015 @ 06:31:44.000' @@ -120,23 +114,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('discoverNoResults')).to.be(true); await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.header.waitUntilLoadingHasFinished(); - cell = await dataGrid.getCellElement(0, 4); - expect(await cell.getVisibleText()).to.be('2269'); + cell = await dataGrid.getCellElement(0, 2); + expect(await cell.getVisibleText()).to.be('1'); }); it('should query an index pattern that doesnt translate to a dataview correctly', async function () { - await PageObjects.discover.selectTextBaseLang('SQL'); - const testQuery = `SELECT "@tags", geo.dest, count(*) occurred FROM "logstash*" - GROUP BY "@tags", geo.dest - HAVING occurred > 20 - ORDER BY occurred DESC`; + await PageObjects.discover.selectTextBaseLang(); + const testQuery = `from logstash* | limit 10 | stats countB = count(bytes) by geo.dest | sort countB`; await monacoEditor.setCodeEditorValue(testQuery); await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); - const cell = await dataGrid.getCellElement(0, 4); - expect(await cell.getVisibleText()).to.be('2269'); + const cell = await dataGrid.getCellElement(0, 2); + expect(await cell.getVisibleText()).to.be('1'); }); }); }); diff --git a/test/functional/apps/discover/group2/index.ts b/test/functional/apps/discover/group2/index.ts index 17562157f444e..163c6b1a9f205 100644 --- a/test/functional/apps/discover/group2/index.ts +++ b/test/functional/apps/discover/group2/index.ts @@ -32,7 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_data_grid_pagination')); loadTestFile(require.resolve('./_data_grid_footer')); loadTestFile(require.resolve('./_adhoc_data_views')); - loadTestFile(require.resolve('./_sql_view')); + loadTestFile(require.resolve('./_esql_view')); loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields')); loadTestFile(require.resolve('./_runtime_fields_editor')); loadTestFile(require.resolve('./_huge_fields')); diff --git a/test/functional/apps/discover/group3/_request_counts.ts b/test/functional/apps/discover/group3/_request_counts.ts index c1224596d3e00..fdee64ada9965 100644 --- a/test/functional/apps/discover/group3/_request_counts.ts +++ b/test/functional/apps/discover/group3/_request_counts.ts @@ -38,7 +38,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', 'bfetch:disable': true, - 'discover:enableSql': true, + 'discover:enableESQL': true, }); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); }); @@ -54,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); }); - const getSearchCount = async (type: 'ese' | 'sql') => { + const getSearchCount = async (type: 'ese' | 'esql') => { const requests = await browser.execute(() => performance .getEntries() @@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await elasticChart.canvasExists(); }; - const expectSearches = async (type: 'ese' | 'sql', expected: number, cb: Function) => { + const expectSearches = async (type: 'ese' | 'esql', expected: number, cb: Function) => { await browser.execute(async () => { performance.clearResourceTimings(); }); @@ -86,12 +86,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { savedSearch, query1, query2, + savedSearchesRequests, setQuery, }: { - type: 'ese' | 'sql'; + type: 'ese' | 'esql'; savedSearch: string; query1: string; query2: string; + savedSearchesRequests?: number; setQuery: (query: string) => Promise; }) => { it('should send 2 search requests (documents + chart) on page load', async () => { @@ -143,8 +145,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Sep 23, 2015 @ 00:00:00.000' ); await waitForLoadingToFinish(); + // TODO: Check why the request happens 4 times in case of opening a saved search + // https://github.com/elastic/kibana/issues/165192 // creating the saved search - await expectSearches(type, 2, async () => { + await expectSearches(type, savedSearchesRequests ?? 2, async () => { await PageObjects.discover.saveSearch(savedSearch); }); // resetting the saved search @@ -160,7 +164,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await waitForLoadingToFinish(); }); // loading the saved search - await expectSearches(type, 2, async () => { + // TODO: https://github.com/elastic/kibana/issues/165192 + await expectSearches(type, savedSearchesRequests ?? 2, async () => { await PageObjects.discover.loadSavedSearch(savedSearch); }); }); @@ -218,21 +223,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('SQL mode', () => { - const type = 'sql'; + describe('ES|QL mode', () => { + const type = 'esql'; beforeEach(async () => { - await PageObjects.discover.selectTextBaseLang('SQL'); - monacoEditor.setCodeEditorValue('SELECT count(*) FROM "logstash-*" WHERE bytes > 1000'); + await PageObjects.discover.selectTextBaseLang(); + monacoEditor.setCodeEditorValue( + 'from logstash-* | where bytes > 1000 | stats countB = count(bytes)' + ); await queryBar.clickQuerySubmitButton(); await waitForLoadingToFinish(); }); getSharedTests({ type, - savedSearch: 'sql test', - query1: 'SELECT type, count(*) FROM "logstash-*" WHERE bytes > 1000 GROUP BY type', - query2: 'SELECT type, count(*) FROM "logstash-*" WHERE bytes < 2000 GROUP BY type', + savedSearch: 'esql test', + query1: 'from logstash-* | where bytes > 1000 | stats countB = count(bytes) ', + query2: 'from logstash-* | where bytes < 2000 | stats countB = count(bytes) ', + savedSearchesRequests: 4, setQuery: (query) => monacoEditor.setCodeEditorValue(query), }); }); diff --git a/test/functional/apps/discover/group3/_sidebar.ts b/test/functional/apps/discover/group3/_sidebar.ts index 265466b5c67f9..eefda4891390b 100644 --- a/test/functional/apps/discover/group3/_sidebar.ts +++ b/test/functional/apps/discover/group3/_sidebar.ts @@ -96,7 +96,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show filters by type in text-based view', async function () { - await kibanaServer.uiSettings.update({ 'discover:enableSql': true }); + await kibanaServer.uiSettings.update({ 'discover:enableESQL': true }); await browser.refresh(); await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); @@ -105,7 +105,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(options).to.have.length(6); await PageObjects.unifiedFieldList.closeSidebarFieldFilter(); - await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.discover.selectTextBaseLang(); await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); await PageObjects.unifiedFieldList.openSidebarFieldFilter(); @@ -113,7 +113,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(options).to.have.length(3); expect(await PageObjects.unifiedFieldList.getSidebarAriaDescription()).to.be( - '50 selected fields. 51 available fields.' + '82 available fields.' ); await testSubjects.click('typeFilter-number'); @@ -121,7 +121,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.waitFor('updates', async () => { return ( (await PageObjects.unifiedFieldList.getSidebarAriaDescription()) === - '6 selected fields. 6 available fields.' + '6 available fields.' ); }); }); @@ -366,7 +366,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show selected and available fields in text-based mode', async function () { - await kibanaServer.uiSettings.update({ 'discover:enableSql': true }); + await kibanaServer.uiSettings.update({ 'discover:enableESQL': true }); await browser.refresh(); await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); @@ -375,24 +375,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { INITIAL_FIELD_LIST_SUMMARY ); - await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.discover.selectTextBaseLang(); await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); expect(await PageObjects.unifiedFieldList.getSidebarAriaDescription()).to.be( - '50 selected fields. 51 available fields.' + '82 available fields.' ); await PageObjects.unifiedFieldList.clickFieldListItemRemove('extension'); await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); expect(await PageObjects.unifiedFieldList.getSidebarAriaDescription()).to.be( - '49 selected fields. 51 available fields.' + '82 available fields.' ); - const testQuery = `SELECT "@tags", geo.dest, count(*) occurred FROM "logstash-*" - GROUP BY "@tags", geo.dest - HAVING occurred > 20 - ORDER BY occurred DESC`; + const testQuery = `from logstash-* | limit 10 | stats countB = count(bytes) by geo.dest | sort countB`; await monacoEditor.setCodeEditorValue(testQuery); await testSubjects.click('querySubmitButton'); @@ -400,11 +397,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); expect(await PageObjects.unifiedFieldList.getSidebarAriaDescription()).to.be( - '3 selected fields. 3 available fields.' + '2 selected fields. 2 available fields.' ); expect( (await PageObjects.unifiedFieldList.getSidebarSectionFieldNames('selected')).join(', ') - ).to.be('@tags, geo.dest, occurred'); + ).to.be('countB, geo.dest'); await PageObjects.unifiedSearch.switchDataView( 'discover-dataView-switch-link', diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json index 3dfde2c248862..d4929d3201878 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json @@ -9,7 +9,6 @@ "title": "hidden object 1" }, "type": "test-actions-export-hidden", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" } } @@ -26,7 +25,6 @@ "title": "hidden object 2" }, "type": "test-actions-export-hidden", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" } } diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 9efe80741621e..21a183c832606 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -150,6 +150,18 @@ export class ConsolePageObject extends FtrService { return !attribute.includes('display: none;'); } + public async getAutocompleteSuggestion(index: number = 0) { + const children1 = await this.find + .allByCssSelector('.ace_autocomplete .ace_line :nth-child(1)') + .catch(() => null); + const children2 = await this.find + .allByCssSelector('.ace_autocomplete .ace_line :nth-child(2)') + .catch(() => null); + if (!children1 || !children2) return null; + + return (await children1[index].getVisibleText()) + (await children2[index].getVisibleText()); + } + public async enterRequest(request: string = '\nGET _search') { const textArea = await this.getEditorTextArea(); await textArea.pressKeys(request); @@ -206,24 +218,24 @@ export class ConsolePageObject extends FtrService { await textArea.pressKeys(Key.ESCAPE); } - public async pressDown() { + public async pressDown(shift: boolean = false) { const textArea = await this.testSubjects.find('console-textarea'); - await textArea.pressKeys(Key.DOWN); + await textArea.pressKeys(shift ? [Key.SHIFT, Key.DOWN] : Key.DOWN); } - public async pressLeft() { + public async pressLeft(shift: boolean = false) { const textArea = await this.testSubjects.find('console-textarea'); - await textArea.pressKeys(Key.LEFT); + await textArea.pressKeys(shift ? [Key.SHIFT, Key.LEFT] : Key.LEFT); } - public async pressRight() { + public async pressRight(shift: boolean = false) { const textArea = await this.testSubjects.find('console-textarea'); - await textArea.pressKeys(Key.RIGHT); + await textArea.pressKeys(shift ? [Key.SHIFT, Key.RIGHT] : Key.RIGHT); } - public async pressUp() { + public async pressUp(shift: boolean = false) { const textArea = await this.testSubjects.find('console-textarea'); - await textArea.pressKeys(Key.UP); + await textArea.pressKeys(shift ? [Key.SHIFT, Key.UP] : Key.UP); } public async clearTextArea() { diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 897ceb5564d93..36da398b59d1f 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -138,7 +138,7 @@ export class DashboardPageObject extends FtrService { await this.testSubjects.existOrFail(`edit-unsaved-${title.split(' ').join('-')}`); } - public async expectUnsavedChangesDoesNotExist(title: string) { + public async expectUnsavedChangesListingDoesNotExist(title: string) { this.log.debug(`Expect Unsaved Changes Listing Does Not Exist for `, title); await this.testSubjects.missingOrFail(`edit-unsaved-${title.split(' ').join('-')}`); } @@ -174,11 +174,6 @@ export class DashboardPageObject extends FtrService { await this.testSubjects.existOrFail('dashboardLandingPage'); } - public async clickDashboardBreadcrumbLink() { - this.log.debug('clickDashboardBreadcrumbLink'); - await this.testSubjects.click('breadcrumb dashboardListingBreadcrumb first'); - } - public async expectOnDashboard(expectedTitle: string) { await this.retry.waitFor( `last breadcrumb to have dashboard title: ${expectedTitle}`, @@ -192,19 +187,21 @@ export class DashboardPageObject extends FtrService { public async gotoDashboardLandingPage(ignorePageLeaveWarning = true) { this.log.debug('gotoDashboardLandingPage'); - const onPage = await this.onDashboardLandingPage(); - if (!onPage) { - await this.clickDashboardBreadcrumbLink(); - await this.retry.try(async () => { - const warning = await this.testSubjects.exists('confirmModalTitleText'); - if (warning) { - await this.testSubjects.click( - ignorePageLeaveWarning ? 'confirmModalConfirmButton' : 'confirmModalCancelButton' - ); - } - }); - await this.expectExistsDashboardLandingPage(); - } + if (await this.onDashboardLandingPage()) return; + + const breadcrumbLink = this.config.get('serverless') + ? 'breadcrumb breadcrumb-deepLinkId-dashboards' + : 'breadcrumb dashboardListingBreadcrumb first'; + await this.testSubjects.click(breadcrumbLink); + await this.retry.try(async () => { + const warning = await this.testSubjects.exists('confirmModalTitleText'); + if (warning) { + await this.testSubjects.click( + ignorePageLeaveWarning ? 'confirmModalConfirmButton' : 'confirmModalCancelButton' + ); + } + }); + await this.expectExistsDashboardLandingPage(); } public async clickClone() { @@ -336,6 +333,12 @@ export class DashboardPageObject extends FtrService { }); } + public async expectMissingUnsavedChangesBadge() { + await this.retry.try(async () => { + await this.testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + }); + } + public async clickNewDashboard(continueEditing = false) { const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton'); if (!continueEditing && discardButtonExists) { diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 7ddd4f303d975..1545975667c60 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -500,11 +500,9 @@ export class DiscoverPageObject extends FtrService { return items; } - public async selectTextBaseLang(lang: 'SQL') { + public async selectTextBaseLang() { await this.testSubjects.click('discover-dataView-switch-link'); - await this.find.clickByCssSelector( - `[data-test-subj="text-based-languages-switcher"] [title="${lang}"]` - ); + await this.testSubjects.click('select-text-based-language-panel'); await this.header.waitUntilLoadingHasFinished(); } diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index ac942c70b5d90..61eb1f40c49e7 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -53,7 +53,9 @@ export class HomePageObject extends FtrService { } async isWelcomeInterstitialDisplayed() { - return await this.testSubjects.isDisplayed('homeWelcomeInterstitial'); + // give the interstitial enough time to fade in + await new Promise((resolve) => setTimeout(resolve, 500)); + return await this.testSubjects.isDisplayed('homeWelcomeInterstitial', 2000); } async isGuidedOnboardingLandingDisplayed() { diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index be43b0d2b29ec..495d4089321b4 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -92,7 +92,9 @@ export class TimePickerPageObject extends FtrService { * @param option 'Today' | 'This_week' | 'Last_15 minutes' | 'Last_24 hours' ... */ async setCommonlyUsedTime(option: CommonlyUsed | string) { + await this.testSubjects.exists('superDatePickerToggleQuickMenuButton', { timeout: 5000 }); await this.testSubjects.click('superDatePickerToggleQuickMenuButton'); + await this.testSubjects.exists(`superDatePickerCommonlyUsed_${option}`, { timeout: 5000 }); await this.testSubjects.click(`superDatePickerCommonlyUsed_${option}`); } diff --git a/test/functional/page_objects/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts index 57a22103f6409..91864a7995779 100644 --- a/test/functional/page_objects/time_to_visualize_page.ts +++ b/test/functional/page_objects/time_to_visualize_page.ts @@ -43,7 +43,7 @@ export class TimeToVisualizePageObject extends FtrService { public async resetNewDashboard() { await this.common.navigateToApp('dashboard'); - await this.dashboard.gotoDashboardLandingPage(true); + await this.dashboard.gotoDashboardLandingPage(); await this.dashboard.clickNewDashboard(false); } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index fd46e5ac1448b..0a1798442f360 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -73,6 +73,11 @@ class BrowserService extends FtrService { return await this.driver.manage().window().getRect(); } + public async getWindowInnerSize(): Promise<{ height: number; width: number }> { + const JS_GET_INNER_WIDTH = 'return { width: window.innerWidth, height: window.innerHeight };'; + return await this.driver.executeScript(JS_GET_INNER_WIDTH); + } + /** * Sets the dimensions of a window. * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Window.html @@ -659,6 +664,12 @@ class BrowserService extends FtrService { this.log.error( `WebDriver session is no longer valid.\nProbably Chrome process crashed when it tried to use more memory than what was available.` ); + // TODO: Remove this after a while. We are enabling richer logs in order to try catch the real error cause. + this.log.error( + `Original Error Logging.\n Name: ${err.name};\n Message: ${err.message};\n Stack: ${ + err.stack + }\n RemoteStack: ${(err as NoSuchSessionError).remoteStacktrace}` + ); } return false; } diff --git a/test/functional/services/common/test_subjects.ts b/test/functional/services/common/test_subjects.ts index e54a1caa08d26..9cbce20b05068 100644 --- a/test/functional/services/common/test_subjects.ts +++ b/test/functional/services/common/test_subjects.ts @@ -285,9 +285,9 @@ export class TestSubjects extends FtrService { return await element.isEnabled(); } - public async isDisplayed(selector: string): Promise { + public async isDisplayed(selector: string, timeout?: number): Promise { this.log.debug(`TestSubjects.isDisplayed(${selector})`); - const element = await this.find(selector); + const element = await this.find(selector, timeout); return await element.isDisplayed(); } diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index df4ac37b96464..5af71ba799457 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -49,11 +49,11 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const navigateTo = async (path: string) => await browser.navigateTo(`${deployment.getHostPort()}${path}`); - // FLAKY: https://github.com/elastic/kibana/issues/127545 - describe.skip('ui applications', function describeIndexTests() { + describe('ui applications', function describeIndexTests() { before(async () => { await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('foo'); + await PageObjects.common.dismissBanner(); }); it('starts on home page', async () => { @@ -126,7 +126,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide expect(await testSubjects.exists('headerGlobalNav')).to.be(false); const wrapperHeight = await getAppWrapperHeight(); - const windowHeight = (await browser.getWindowSize()).height; + const windowHeight = (await browser.getWindowInnerSize()).height; expect(wrapperHeight).to.eql(windowHeight); }); @@ -136,7 +136,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide expect(await testSubjects.exists('headerGlobalNav')).to.be(true); const wrapperHeight = await getAppWrapperHeight(); - const windowHeight = (await browser.getWindowSize()).height; + const windowHeight = (await browser.getWindowInnerSize()).height; expect(wrapperHeight).to.be.below(windowHeight); }); }); diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 03928a378f6f3..49951b9a3100a 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -161,7 +161,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'telemetry.labels.testBuildId (string)', 'telemetry.labels.testJobId (string)', 'telemetry.labels.ciBuildName (string)', - 'telemetry.labels.serverless (any)', + 'telemetry.labels.performancePhase (string)', + 'telemetry.labels.serverless (any)', // It's the project type (string), claims any because schema.conditional. Can only be set on Serverless. 'telemetry.hidePrivacyStatement (boolean)', 'telemetry.optIn (boolean)', 'telemetry.sendUsageFrom (alternatives)', @@ -280,6 +281,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.securitySolution.prebuiltRulesPackageVersion (string)', 'xpack.snapshot_restore.slm_ui.enabled (boolean)', 'xpack.snapshot_restore.ui.enabled (boolean)', + 'xpack.stack_connectors.enableExperimental (array)', 'xpack.trigger_actions_ui.enableExperimental (array)', 'xpack.trigger_actions_ui.enableGeoTrackingThresholdAlert (boolean)', 'xpack.upgrade_assistant.featureSet.migrateSystemIndices (boolean)', @@ -292,10 +294,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observability.unsafe.alertDetails.observability.enabled (boolean)', 'xpack.observability.unsafe.thresholdRule.enabled (boolean)', 'xpack.observability_onboarding.ui.enabled (boolean)', - /** - * xpack.discoverLogExplorer.featureFlags is conditional and will never resolve if used in non-serverless environment - */ - 'xpack.discoverLogExplorer.featureFlags.deepLinkVisible (any)', + 'xpack.observabilityLogExplorer.navigation.showAppLink (any)', // conditional, is actually a boolean ]; // We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large // arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's @@ -331,6 +330,30 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.security.showInsecureClusterWarning (boolean)', 'xpack.security.showNavLinks (boolean)', 'xpack.security.ui (any)', + + 'telemetry.allowChangingOptInStatus (boolean)', + 'telemetry.appendServerlessChannelsSuffix (any)', // It's a boolean (any because schema.conditional) + 'telemetry.banner (boolean)', + 'telemetry.labels.branch (string)', + 'telemetry.labels.ciBuildId (string)', + 'telemetry.labels.ciBuildJobId (string)', + 'telemetry.labels.ciBuildNumber (number)', + 'telemetry.labels.ftrConfig (string)', + 'telemetry.labels.gitRev (string)', + 'telemetry.labels.isPr (boolean)', + 'telemetry.labels.journeyName (string)', + 'telemetry.labels.prId (number)', + 'telemetry.labels.testBuildId (string)', + 'telemetry.labels.testJobId (string)', + 'telemetry.labels.ciBuildName (string)', + 'telemetry.labels.performancePhase (string)', + 'telemetry.labels.serverless (any)', // It's the project type (string), claims any because schema.conditional. Can only be set on Serverless. + 'telemetry.hidePrivacyStatement (boolean)', + 'telemetry.optIn (boolean)', + 'telemetry.sendUsageFrom (alternatives)', + 'telemetry.sendUsageTo (any)', + 'usageCollection.uiCounters.debug (boolean)', + 'usageCollection.uiCounters.enabled (boolean)', ]; // We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large // arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's diff --git a/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts b/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts index 439ece04e615f..8e7adb504ebee 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts @@ -21,8 +21,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - // Failing: See https://github.com/elastic/kibana/issues/116059 - describe.skip('saved objects management with hidden types', () => { + describe('saved objects management with hidden types', () => { before(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_types' diff --git a/tsconfig.base.json b/tsconfig.base.json index a347a249b68ca..afbbdcfe7601b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -652,8 +652,6 @@ "@kbn/discover-customization-examples-plugin/*": ["examples/discover_customization_examples/*"], "@kbn/discover-enhanced-plugin": ["x-pack/plugins/discover_enhanced"], "@kbn/discover-enhanced-plugin/*": ["x-pack/plugins/discover_enhanced/*"], - "@kbn/discover-log-explorer-plugin": ["x-pack/plugins/discover_log_explorer"], - "@kbn/discover-log-explorer-plugin/*": ["x-pack/plugins/discover_log_explorer/*"], "@kbn/discover-plugin": ["src/plugins/discover"], "@kbn/discover-plugin/*": ["src/plugins/discover/*"], "@kbn/discover-utils": ["packages/kbn-discover-utils"], @@ -938,6 +936,8 @@ "@kbn/locator-examples-plugin/*": ["examples/locator_examples/*"], "@kbn/locator-explorer-plugin": ["examples/locator_explorer"], "@kbn/locator-explorer-plugin/*": ["examples/locator_explorer/*"], + "@kbn/log-explorer-plugin": ["x-pack/plugins/log_explorer"], + "@kbn/log-explorer-plugin/*": ["x-pack/plugins/log_explorer/*"], "@kbn/logging": ["packages/kbn-logging"], "@kbn/logging/*": ["packages/kbn-logging/*"], "@kbn/logging-mocks": ["packages/kbn-logging-mocks"], @@ -1042,6 +1042,8 @@ "@kbn/observability-alert-details/*": ["x-pack/packages/observability/alert_details/*"], "@kbn/observability-fixtures-plugin": ["x-pack/test/cases_api_integration/common/plugins/observability"], "@kbn/observability-fixtures-plugin/*": ["x-pack/test/cases_api_integration/common/plugins/observability/*"], + "@kbn/observability-log-explorer-plugin": ["x-pack/plugins/observability_log_explorer"], + "@kbn/observability-log-explorer-plugin/*": ["x-pack/plugins/observability_log_explorer/*"], "@kbn/observability-onboarding-plugin": ["x-pack/plugins/observability_onboarding"], "@kbn/observability-onboarding-plugin/*": ["x-pack/plugins/observability_onboarding/*"], "@kbn/observability-plugin": ["x-pack/plugins/observability"], @@ -1082,6 +1084,8 @@ "@kbn/preboot-example-plugin/*": ["examples/preboot_example/*"], "@kbn/presentation-util-plugin": ["src/plugins/presentation_util"], "@kbn/presentation-util-plugin/*": ["src/plugins/presentation_util/*"], + "@kbn/profiling-data-access-plugin": ["x-pack/plugins/profiling_data_access"], + "@kbn/profiling-data-access-plugin/*": ["x-pack/plugins/profiling_data_access/*"], "@kbn/profiling-plugin": ["x-pack/plugins/profiling"], "@kbn/profiling-plugin/*": ["x-pack/plugins/profiling/*"], "@kbn/random-sampling": ["x-pack/packages/kbn-random-sampling"], @@ -1194,6 +1198,8 @@ "@kbn/security-plugin/*": ["x-pack/plugins/security/*"], "@kbn/security-solution-ess": ["x-pack/plugins/security_solution_ess"], "@kbn/security-solution-ess/*": ["x-pack/plugins/security_solution_ess/*"], + "@kbn/security-solution-features": ["x-pack/packages/security-solution/features"], + "@kbn/security-solution-features/*": ["x-pack/packages/security-solution/features/*"], "@kbn/security-solution-fixtures-plugin": ["x-pack/test/cases_api_integration/common/plugins/security_solution"], "@kbn/security-solution-fixtures-plugin/*": ["x-pack/test/cases_api_integration/common/plugins/security_solution/*"], "@kbn/security-solution-navigation": ["x-pack/packages/security-solution/navigation"], @@ -1490,6 +1496,14 @@ "@kbn/ui-shared-deps-src/*": ["packages/kbn-ui-shared-deps-src/*"], "@kbn/ui-theme": ["packages/kbn-ui-theme"], "@kbn/ui-theme/*": ["packages/kbn-ui-theme/*"], + "@kbn/unified-data-table": ["packages/kbn-unified-data-table"], + "@kbn/unified-data-table/*": ["packages/kbn-unified-data-table/*"], + "@kbn/unified-doc-viewer": ["packages/kbn-unified-doc-viewer"], + "@kbn/unified-doc-viewer/*": ["packages/kbn-unified-doc-viewer/*"], + "@kbn/unified-doc-viewer-examples": ["examples/unified_doc_viewer"], + "@kbn/unified-doc-viewer-examples/*": ["examples/unified_doc_viewer/*"], + "@kbn/unified-doc-viewer-plugin": ["src/plugins/unified_doc_viewer"], + "@kbn/unified-doc-viewer-plugin/*": ["src/plugins/unified_doc_viewer/*"], "@kbn/unified-field-list": ["packages/kbn-unified-field-list"], "@kbn/unified-field-list/*": ["packages/kbn-unified-field-list/*"], "@kbn/unified-field-list-examples-plugin": ["examples/unified_field_list_examples"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 17fd0b07ec44e..bcca0b5e14318 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -21,7 +21,6 @@ "xpack.customBranding": "plugins/custom_branding", "xpack.dashboard": "plugins/dashboard_enhanced", "xpack.discover": "plugins/discover_enhanced", - "xpack.discoverLogExplorer": "plugins/discover_log_explorer", "xpack.crossClusterReplication": "plugins/cross_cluster_replication", "xpack.elasticAssistant": "packages/kbn-elastic-assistant", "xpack.ecsDataQualityDashboard": "plugins/ecs_data_quality_dashboard", @@ -39,6 +38,7 @@ "xpack.idxMgmt": "plugins/index_management", "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", + "xpack.logExplorer": "plugins/log_explorer", "xpack.logsShared": "plugins/logs_shared", "xpack.fleet": "plugins/fleet", "xpack.ingestPipelines": "plugins/ingest_pipelines", @@ -61,6 +61,7 @@ ], "xpack.monitoring": ["plugins/monitoring"], "xpack.observability": "plugins/observability", + "xpack.observabilityLogExplorer": "plugins/observability_log_explorer", "xpack.observabilityShared": "plugins/observability_shared", "xpack.observability_onboarding": "plugins/observability_onboarding", "xpack.observabilityAiAssistant": "plugins/observability_ai_assistant", diff --git a/x-pack/packages/security-solution/features/README.mdx b/x-pack/packages/security-solution/features/README.mdx new file mode 100644 index 0000000000000..e87fe71c4fac9 --- /dev/null +++ b/x-pack/packages/security-solution/features/README.mdx @@ -0,0 +1,4 @@ +## Security Solution App Features + +This package provides resources to be used for Security Solution app features + diff --git a/x-pack/packages/security-solution/features/app_features.ts b/x-pack/packages/security-solution/features/app_features.ts new file mode 100644 index 0000000000000..b9209441cff85 --- /dev/null +++ b/x-pack/packages/security-solution/features/app_features.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 { getSecurityFeature } from './src/security'; +export { getCasesFeature } from './src/cases'; +export { getAssistantFeature } from './src/assistant'; diff --git a/x-pack/packages/security-solution/features/config.ts b/x-pack/packages/security-solution/features/config.ts new file mode 100644 index 0000000000000..8f382fc13487f --- /dev/null +++ b/x-pack/packages/security-solution/features/config.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 { securityDefaultAppFeaturesConfig } from './src/security/app_feature_config'; +export { getCasesDefaultAppFeaturesConfig } from './src/cases/app_feature_config'; +export { assistantDefaultAppFeaturesConfig } from './src/assistant/app_feature_config'; + +export { createEnabledAppFeaturesConfigMap } from './src/helpers'; diff --git a/x-pack/packages/security-solution/features/index.ts b/x-pack/packages/security-solution/features/index.ts new file mode 100644 index 0000000000000..a7fe0b5131c73 --- /dev/null +++ b/x-pack/packages/security-solution/features/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 './src/types'; diff --git a/x-pack/packages/security-solution/features/jest.config.js b/x-pack/packages/security-solution/features/jest.config.js new file mode 100644 index 0000000000000..47da21e7adff0 --- /dev/null +++ b/x-pack/packages/security-solution/features/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/security-solution/features'], +}; diff --git a/x-pack/packages/security-solution/features/keys.ts b/x-pack/packages/security-solution/features/keys.ts new file mode 100644 index 0000000000000..11063c154567c --- /dev/null +++ b/x-pack/packages/security-solution/features/keys.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 './src/app_features_keys'; diff --git a/x-pack/packages/security-solution/features/kibana.jsonc b/x-pack/packages/security-solution/features/kibana.jsonc new file mode 100644 index 0000000000000..0e5a360ea9929 --- /dev/null +++ b/x-pack/packages/security-solution/features/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/security-solution-features", + "owner": "@elastic/security-threat-hunting-explore" +} diff --git a/x-pack/packages/security-solution/features/package.json b/x-pack/packages/security-solution/features/package.json new file mode 100644 index 0000000000000..77abf87117eb8 --- /dev/null +++ b/x-pack/packages/security-solution/features/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/security-solution-features", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/packages/security-solution/features/privileges.ts b/x-pack/packages/security-solution/features/privileges.ts new file mode 100644 index 0000000000000..2e5a99095e4f5 --- /dev/null +++ b/x-pack/packages/security-solution/features/privileges.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { AppFeaturesPrivilegeId, AppFeaturesPrivileges } from './src/app_features_privileges'; diff --git a/x-pack/plugins/security_solution/common/types/app_features.ts b/x-pack/packages/security-solution/features/src/app_features_keys.ts similarity index 64% rename from x-pack/plugins/security_solution/common/types/app_features.ts rename to x-pack/packages/security-solution/features/src/app_features_keys.ts index a8c65aeadfc8a..ea3939e2b9f28 100644 --- a/x-pack/plugins/security_solution/common/types/app_features.ts +++ b/x-pack/packages/security-solution/features/src/app_features_keys.ts @@ -6,60 +6,48 @@ */ export enum AppFeatureSecurityKey { - /** - * Enables Advanced Insights (Entity Risk, GenAI) - */ + /** Enables Advanced Insights (Entity Risk, GenAI) */ advancedInsights = 'advanced_insights', - /** * Enables Investigation guide in Timeline */ investigationGuide = 'investigation_guide', - /** * Enables access to the Endpoint List and associated views that allows management of hosts * running endpoint security */ endpointHostManagement = 'endpoint_host_management', - /** * Enables endpoint policy views that enables user to manage endpoint security policies */ endpointPolicyManagement = 'endpoint_policy_management', - /** * Enables Endpoint Policy protections (like Malware, Ransomware, etc) */ endpointPolicyProtections = 'endpoint_policy_protections', - /** * Enables management of all endpoint related artifacts (ex. Trusted Applications, Event Filters, * Host Isolation Exceptions, Blocklist. */ endpointArtifactManagement = 'endpoint_artifact_management', - /** * Enables all of endpoint's supported response actions - like host isolation, file operations, * process operations, command execution, etc. */ endpointResponseActions = 'endpoint_response_actions', - /** * Enables Threat Intelligence */ threatIntelligence = 'threat-intelligence', - /** * Enables Osquery Response Actions */ osqueryAutomatedResponseActions = 'osquery_automated_response_actions', -} -export enum AppFeatureAssistantKey { /** - * Enables Elastic AI Assistant + * Enables managing endpoint exceptions on rules and alerts */ - assistant = 'assistant', + endpointExceptions = 'endpointExceptions', } export enum AppFeatureCasesKey { @@ -69,14 +57,46 @@ export enum AppFeatureCasesKey { casesConnectors = 'cases_connectors', } -// Merges the two enums. -export type AppFeatureKey = AppFeatureSecurityKey | AppFeatureCasesKey | AppFeatureAssistantKey; -export type AppFeatureKeys = AppFeatureKey[]; +export enum AppFeatureAssistantKey { + /** + * Enables Elastic AI Assistant + */ + assistant = 'assistant', +} -// We need to merge the value and the type and export both to replicate how enum works. +// Merges the two enums. export const AppFeatureKey = { ...AppFeatureSecurityKey, ...AppFeatureCasesKey, ...AppFeatureAssistantKey, }; +// We need to merge the value and the type and export both to replicate how enum works. +export type AppFeatureKeyType = AppFeatureSecurityKey | AppFeatureCasesKey | AppFeatureAssistantKey; + export const ALL_APP_FEATURE_KEYS = Object.freeze(Object.values(AppFeatureKey)); + +/** Sub-features IDs for Security */ +export enum SecuritySubFeatureId { + endpointList = 'endpointListSubFeature', + endpointExceptions = 'endpointExceptionsSubFeature', + trustedApplications = 'trustedApplicationsSubFeature', + hostIsolationExceptions = 'hostIsolationExceptionsSubFeature', + blocklist = 'blocklistSubFeature', + eventFilters = 'eventFiltersSubFeature', + policyManagement = 'policyManagementSubFeature', + responseActionsHistory = 'responseActionsHistorySubFeature', + hostIsolation = 'hostIsolationSubFeature', + processOperations = 'processOperationsSubFeature', + fileOperations = 'fileOperationsSubFeature', + executeAction = 'executeActionSubFeature', +} + +/** Sub-features IDs for Cases */ +export enum CasesSubFeatureId { + deleteCases = 'deleteCasesSubFeature', +} + +/** Sub-features IDs for Security Assistant */ +export enum AssistantSubFeatureId { + createConversation = 'createConversationSubFeature', +} diff --git a/x-pack/packages/security-solution/features/src/app_features_privileges.ts b/x-pack/packages/security-solution/features/src/app_features_privileges.ts new file mode 100644 index 0000000000000..24fe0e25a19cc --- /dev/null +++ b/x-pack/packages/security-solution/features/src/app_features_privileges.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { APP_ID } from './constants'; + +export enum AppFeaturesPrivilegeId { + endpointExceptions = 'endpoint_exceptions', +} + +/** + * This is the mapping of the privileges that are registered + * using a different Kibana feature configuration (sub-feature, main feature privilege, etc) + * in each offering type (ess, serverless) + */ +export const AppFeaturesPrivileges = { + [AppFeaturesPrivilegeId.endpointExceptions]: { + all: { + ui: ['showEndpointExceptions', 'crudEndpointExceptions'], + api: [`${APP_ID}-showEndpointExceptions`, `${APP_ID}-crudEndpointExceptions`], + }, + read: { + ui: ['showEndpointExceptions'], + api: [`${APP_ID}-showEndpointExceptions`], + }, + }, +}; diff --git a/x-pack/packages/security-solution/features/src/assistant/app_feature_config.ts b/x-pack/packages/security-solution/features/src/assistant/app_feature_config.ts new file mode 100644 index 0000000000000..b55c43f82c953 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/assistant/app_feature_config.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AssistantSubFeatureId } from '../app_features_keys'; +import { AppFeatureAssistantKey } from '../app_features_keys'; +import type { AppFeatureKibanaConfig } from '../types'; + +/** + * App features privileges configuration for the Security Assistant Kibana Feature app. + * These are the configs that are shared between both offering types (ess and serverless). + * They can be extended on each offering plugin to register privileges using different way on each offering type. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. + */ +export const assistantDefaultAppFeaturesConfig: Record< + AppFeatureAssistantKey, + AppFeatureKibanaConfig +> = { + [AppFeatureAssistantKey.assistant]: { + privileges: { + all: { + ui: ['ai-assistant'], + }, + }, + }, +}; diff --git a/x-pack/packages/security-solution/features/src/assistant/index.ts b/x-pack/packages/security-solution/features/src/assistant/index.ts new file mode 100644 index 0000000000000..d1319fd637913 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/assistant/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { AssistantSubFeatureId } from '../app_features_keys'; +import type { AppFeatureParams } from '../types'; +import { getAssistantBaseKibanaFeature } from './kibana_features'; +import { + getAssistantBaseKibanaSubFeatureIds, + assistantSubFeaturesMap, +} from './kibana_sub_features'; + +export const getAssistantFeature = (): AppFeatureParams => ({ + baseKibanaFeature: getAssistantBaseKibanaFeature(), + baseKibanaSubFeatureIds: getAssistantBaseKibanaSubFeatureIds(), + subFeaturesMap: assistantSubFeaturesMap, +}); diff --git a/x-pack/packages/security-solution/features/src/assistant/kibana_features.ts b/x-pack/packages/security-solution/features/src/assistant/kibana_features.ts new file mode 100644 index 0000000000000..e04b1f44df739 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/assistant/kibana_features.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 { i18n } from '@kbn/i18n'; + +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { type BaseKibanaFeatureConfig } from '../types'; +import { APP_ID, ASSISTANT_FEATURE_ID } from '../constants'; + +export const getAssistantBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ + id: ASSISTANT_FEATURE_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionAssistantTitle', + { + defaultMessage: 'Elastic AI Assistant', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + app: [ASSISTANT_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + minimumLicense: 'enterprise', + privileges: { + all: { + api: [], + app: [ASSISTANT_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + // No read-only mode currently supported + disabled: true, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, +}); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts similarity index 66% rename from x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts rename to x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts index bc495e8c24d60..253b98f602c92 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts +++ b/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts @@ -12,13 +12,13 @@ import type { SubFeatureConfig } from '@kbn/features-plugin/common'; // @ts-expect-error unused variable const createConversationSubFeature: SubFeatureConfig = { name: i18n.translate( - 'xpack.securitySolution.featureRegistry.assistant.createConversationSubFeatureName', + 'securitySolutionPackages.features.featureRegistry.assistant.createConversationSubFeatureName', { defaultMessage: 'Create Conversations', } ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.assistant.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.assistant.description', { defaultMessage: 'Create custom conversations.' } ), privilegeGroups: [ @@ -29,7 +29,7 @@ const createConversationSubFeature: SubFeatureConfig = { api: [], id: 'create_conversation', name: i18n.translate( - 'xpack.securitySolution.featureRegistry.assistant.createConversationSubFeatureDetails', + 'securitySolutionPackages.features.featureRegistry.assistant.createConversationSubFeatureDetails', { defaultMessage: 'Create conversations', } @@ -50,7 +50,19 @@ export enum AssistantSubFeatureId { createConversation = 'createConversationSubFeature', } -// Defines all the ordered Security Assistant subFeatures available +/** + * Sub-features that will always be available for Security Assistant + * regardless of the product type. + */ +export const getAssistantBaseKibanaSubFeatureIds = (): AssistantSubFeatureId[] => [ + // This is a sample sub-feature that can be used for future implementations + // AssistantSubFeatureId.createConversation, +]; + +/** + * Defines all the Security Assistant subFeatures available. + * The order of the subFeatures is the order they will be displayed + */ export const assistantSubFeaturesMap = Object.freeze( new Map([ // This is a sample sub-feature that can be used for future implementations diff --git a/x-pack/packages/security-solution/features/src/cases/app_feature_config.ts b/x-pack/packages/security-solution/features/src/cases/app_feature_config.ts new file mode 100644 index 0000000000000..cfad7bfa7715d --- /dev/null +++ b/x-pack/packages/security-solution/features/src/cases/app_feature_config.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AppFeatureCasesKey } from '../app_features_keys'; +import { APP_ID } from '../constants'; +import type { DefaultCasesAppFeaturesConfig } from './types'; + +/** + * App features privileges configuration for the Security Cases Kibana Feature app. + * These are the configs that are shared between both offering types (ess and serverless). + * They can be extended on each offering plugin to register privileges using different way on each offering type. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. + */ +export const getCasesDefaultAppFeaturesConfig = ({ + apiTags, + uiCapabilities, +}: { + apiTags: { connectors: string }; + uiCapabilities: { connectors: string }; +}): DefaultCasesAppFeaturesConfig => ({ + [AppFeatureCasesKey.casesConnectors]: { + privileges: { + all: { + api: [apiTags.connectors], + ui: [uiCapabilities.connectors], + cases: { + push: [APP_ID], + }, + }, + read: { + api: [apiTags.connectors], + ui: [uiCapabilities.connectors], + }, + }, + }, +}); diff --git a/x-pack/packages/security-solution/features/src/cases/index.ts b/x-pack/packages/security-solution/features/src/cases/index.ts new file mode 100644 index 0000000000000..dbc0355d36565 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/cases/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { CasesSubFeatureId } from '../app_features_keys'; +import type { AppFeatureParams } from '../types'; +import { getCasesBaseKibanaFeature } from './kibana_features'; +import { getCasesBaseKibanaSubFeatureIds, getCasesSubFeaturesMap } from './kibana_sub_features'; +import type { CasesFeatureParams } from './types'; + +export const getCasesFeature = ( + params: CasesFeatureParams +): AppFeatureParams => ({ + baseKibanaFeature: getCasesBaseKibanaFeature(params), + baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIds(), + subFeaturesMap: getCasesSubFeaturesMap(params), +}); diff --git a/x-pack/packages/security-solution/features/src/cases/kibana_features.ts b/x-pack/packages/security-solution/features/src/cases/kibana_features.ts new file mode 100644 index 0000000000000..a8da25bb6e40b --- /dev/null +++ b/x-pack/packages/security-solution/features/src/cases/kibana_features.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import type { BaseKibanaFeatureConfig } from '../types'; +import { APP_ID, CASES_FEATURE_ID } from '../constants'; +import type { CasesFeatureParams } from './types'; + +export const getCasesBaseKibanaFeature = ({ + uiCapabilities, + apiTags, + savedObjects, +}: CasesFeatureParams): BaseKibanaFeatureConfig => { + return { + id: CASES_FEATURE_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitle', + { + defaultMessage: 'Cases', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: [APP_ID], + privileges: { + all: { + api: apiTags.all, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: { + create: [APP_ID], + read: [APP_ID], + update: [APP_ID], + }, + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + ui: uiCapabilities.all, + }, + read: { + api: apiTags.read, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: { + read: [APP_ID], + }, + savedObject: { + all: [], + read: [...savedObjects.files], + }, + ui: uiCapabilities.read, + }, + }, + }; +}; diff --git a/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts new file mode 100644 index 0000000000000..3cbdb3f0e9123 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { SubFeatureConfig } from '@kbn/features-plugin/common'; +import { CasesSubFeatureId } from '../app_features_keys'; +import { APP_ID } from '../constants'; +import type { CasesFeatureParams } from './types'; + +/** + * Sub-features that will always be available for Security Cases + * regardless of the product type. + */ +export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ + CasesSubFeatureId.deleteCases, +]; + +/** + * Defines all the Security Assistant subFeatures available. + * The order of the subFeatures is the order they will be displayed + */ +export const getCasesSubFeaturesMap = ({ + uiCapabilities, + apiTags, + savedObjects, +}: CasesFeatureParams) => { + const deleteCasesSubFeature: SubFeatureConfig = { + name: i18n.translate('securitySolutionPackages.features.featureRegistry.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.delete, + id: 'cases_delete', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', + { + defaultMessage: 'Delete cases and comments', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + delete: [APP_ID], + }, + ui: uiCapabilities.delete, + }, + ], + }, + ], + }; + + return new Map([ + [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], + ]); +}; diff --git a/x-pack/packages/security-solution/features/src/cases/types.ts b/x-pack/packages/security-solution/features/src/cases/types.ts new file mode 100644 index 0000000000000..b7c093b0cadc3 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/cases/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 type { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common'; +import type { AppFeatureCasesKey, CasesSubFeatureId } from '../app_features_keys'; +import type { AppFeatureKibanaConfig } from '../types'; + +export interface CasesFeatureParams { + uiCapabilities: CasesUiCapabilities; + apiTags: CasesApiTags; + savedObjects: { files: string[] }; +} + +export type DefaultCasesAppFeaturesConfig = Record< + AppFeatureCasesKey, + AppFeatureKibanaConfig +>; diff --git a/x-pack/packages/security-solution/features/src/constants.ts b/x-pack/packages/security-solution/features/src/constants.ts new file mode 100644 index 0000000000000..c92376fd36209 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/constants.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// Same as the plugin id defined by Security Solution +export const APP_ID = 'securitySolution' as const; +export const SERVER_APP_ID = 'siem' as const; + +export const CASES_FEATURE_ID = 'securitySolutionCases' as const; +export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; + +// Same as the plugin id defined by Cloud Security Posture +export const CLOUD_POSTURE_APP_ID = 'csp' as const; + +// Same as the plugin id defined by Defend for containers (cloud_defend) +export const CLOUD_DEFEND_APP_ID = 'cloudDefend' as const; + +/** + * Id for the notifications alerting type + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const LEGACY_NOTIFICATIONS_ID = `siem.notifications` as const; diff --git a/x-pack/packages/security-solution/features/src/helpers.ts b/x-pack/packages/security-solution/features/src/helpers.ts new file mode 100644 index 0000000000000..1beb8a3a6284c --- /dev/null +++ b/x-pack/packages/security-solution/features/src/helpers.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AppFeatureKeys, AppFeatureKeyType, AppFeatureKibanaConfig } from './types'; + +/** + * Creates the AppFeaturesConfig Map from the given appFeatures object and a set of enabled appFeatures keys. + */ +export const createEnabledAppFeaturesConfigMap = < + K extends AppFeatureKeyType, + T extends string = string +>( + appFeatures: Record>, + enabledAppFeaturesKeys: AppFeatureKeys +) => { + return new Map( + Object.entries>(appFeatures).reduce< + Array<[K, AppFeatureKibanaConfig]> + >((acc, [key, value]) => { + if (enabledAppFeaturesKeys.includes(key as K)) { + acc.push([key as K, value]); + } + return acc; + }, []) + ); +}; diff --git a/x-pack/packages/security-solution/features/src/security/app_feature_config.ts b/x-pack/packages/security-solution/features/src/security/app_feature_config.ts new file mode 100644 index 0000000000000..a27dccd6c5bf6 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/security/app_feature_config.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AppFeatureSecurityKey, SecuritySubFeatureId } from '../app_features_keys'; +import { APP_ID } from '../constants'; +import type { DefaultSecurityAppFeaturesConfig } from './types'; + +/** + * App features privileges configuration for the Security Solution Kibana Feature app. + * These are the configs that are shared between both offering types (ess and serverless). + * They can be extended on each offering plugin to register privileges using different way on each offering type. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. + */ +export const securityDefaultAppFeaturesConfig: DefaultSecurityAppFeaturesConfig = { + [AppFeatureSecurityKey.advancedInsights]: { + privileges: { + all: { + ui: ['entity-analytics'], + api: [`${APP_ID}-entity-analytics`], + }, + read: { + ui: ['entity-analytics'], + api: [`${APP_ID}-entity-analytics`], + }, + }, + }, + [AppFeatureSecurityKey.investigationGuide]: { + privileges: { + all: { + ui: ['investigation-guide'], + }, + read: { + ui: ['investigation-guide'], + }, + }, + }, + + [AppFeatureSecurityKey.threatIntelligence]: { + privileges: { + all: { + ui: ['threat-intelligence'], + api: [`${APP_ID}-threat-intelligence`], + }, + read: { + ui: ['threat-intelligence'], + api: [`${APP_ID}-threat-intelligence`], + }, + }, + }, + + [AppFeatureSecurityKey.endpointHostManagement]: { + subFeatureIds: [SecuritySubFeatureId.endpointList], + }, + + [AppFeatureSecurityKey.endpointPolicyManagement]: { + subFeatureIds: [SecuritySubFeatureId.policyManagement], + }, + + // Adds no additional kibana feature controls + [AppFeatureSecurityKey.endpointPolicyProtections]: {}, + + [AppFeatureSecurityKey.endpointArtifactManagement]: { + subFeatureIds: [ + SecuritySubFeatureId.trustedApplications, + SecuritySubFeatureId.blocklist, + SecuritySubFeatureId.eventFilters, + ], + subFeaturesPrivileges: [ + { + id: 'host_isolation_exceptions_all', + api: [`${APP_ID}-accessHostIsolationExceptions`, `${APP_ID}-writeHostIsolationExceptions`], + ui: ['accessHostIsolationExceptions', 'writeHostIsolationExceptions'], + }, + { + id: 'host_isolation_exceptions_read', + api: [`${APP_ID}-accessHostIsolationExceptions`], + ui: ['accessHostIsolationExceptions'], + }, + ], + }, + + [AppFeatureSecurityKey.endpointResponseActions]: { + subFeatureIds: [ + SecuritySubFeatureId.hostIsolationExceptions, + SecuritySubFeatureId.responseActionsHistory, + SecuritySubFeatureId.hostIsolation, + SecuritySubFeatureId.processOperations, + SecuritySubFeatureId.fileOperations, + SecuritySubFeatureId.executeAction, + ], + subFeaturesPrivileges: [ + { + id: 'host_isolation_all', + api: [`${APP_ID}-writeHostIsolation`], + ui: ['writeHostIsolation'], + }, + ], + }, + + [AppFeatureSecurityKey.osqueryAutomatedResponseActions]: {}, +}; diff --git a/x-pack/packages/security-solution/features/src/security/index.ts b/x-pack/packages/security-solution/features/src/security/index.ts new file mode 100644 index 0000000000000..67f72361fb0cc --- /dev/null +++ b/x-pack/packages/security-solution/features/src/security/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { SecuritySubFeatureId } from '../app_features_keys'; +import type { AppFeatureParams } from '../types'; +import { getSecurityBaseKibanaFeature } from './kibana_features'; +import { securitySubFeaturesMap, getSecurityBaseKibanaSubFeatureIds } from './kibana_sub_features'; +import type { SecurityFeatureParams } from './types'; + +export const getSecurityFeature = ( + params: SecurityFeatureParams +): AppFeatureParams => ({ + baseKibanaFeature: getSecurityBaseKibanaFeature(params), + baseKibanaSubFeatureIds: getSecurityBaseKibanaSubFeatureIds(params), + subFeaturesMap: securitySubFeaturesMap, +}); diff --git a/x-pack/packages/security-solution/features/src/security/kibana_features.ts b/x-pack/packages/security-solution/features/src/security/kibana_features.ts new file mode 100644 index 0000000000000..f4176dfa53719 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/security/kibana_features.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { + EQL_RULE_TYPE_ID, + INDICATOR_RULE_TYPE_ID, + ML_RULE_TYPE_ID, + NEW_TERMS_RULE_TYPE_ID, + QUERY_RULE_TYPE_ID, + SAVED_QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, +} from '@kbn/securitysolution-rules'; +import type { BaseKibanaFeatureConfig } from '../types'; +import { + APP_ID, + SERVER_APP_ID, + LEGACY_NOTIFICATIONS_ID, + CLOUD_POSTURE_APP_ID, + CLOUD_DEFEND_APP_ID, +} from '../constants'; +import type { SecurityFeatureParams } from './types'; + +const SECURITY_RULE_TYPES = [ + LEGACY_NOTIFICATIONS_ID, + EQL_RULE_TYPE_ID, + INDICATOR_RULE_TYPE_ID, + ML_RULE_TYPE_ID, + QUERY_RULE_TYPE_ID, + SAVED_QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, + NEW_TERMS_RULE_TYPE_ID, +]; + +export const getSecurityBaseKibanaFeature = ({ + savedObjects, +}: SecurityFeatureParams): BaseKibanaFeatureConfig => ({ + id: SERVER_APP_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionTitle', + { + defaultMessage: 'Security', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'], + catalogue: [APP_ID], + management: { + insightsAndAlerting: ['triggersActions'], + }, + alerting: SECURITY_RULE_TYPES, + privileges: { + all: { + app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'], + catalogue: [APP_ID], + api: [ + APP_ID, + 'lists-all', + 'lists-read', + 'lists-summary', + 'rac', + 'cloud-security-posture-all', + 'cloud-security-posture-read', + 'cloud-defend-all', + 'cloud-defend-read', + ], + savedObject: { + all: ['alert', ...savedObjects], + read: [], + }, + alerting: { + rule: { + all: SECURITY_RULE_TYPES, + }, + alert: { + all: SECURITY_RULE_TYPES, + }, + }, + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['show', 'crud'], + }, + read: { + app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'], + catalogue: [APP_ID], + api: [APP_ID, 'lists-read', 'rac', 'cloud-security-posture-read', 'cloud-defend-read'], + savedObject: { + all: [], + read: [...savedObjects], + }, + alerting: { + rule: { + read: SECURITY_RULE_TYPES, + }, + alert: { + all: SECURITY_RULE_TYPES, + }, + }, + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['show'], + }, + }, +}); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/security/kibana_sub_features.ts similarity index 68% rename from x-pack/plugins/security_solution/server/lib/app_features/security_kibana_sub_features.ts rename to x-pack/packages/security-solution/features/src/security/kibana_sub_features.ts index a8410e4e4253d..86cbf89f26a6f 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/security_kibana_sub_features.ts +++ b/x-pack/packages/security-solution/features/src/security/kibana_sub_features.ts @@ -8,21 +8,27 @@ import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; -import { APP_ID } from '../../../common'; +import { AppFeaturesPrivilegeId, AppFeaturesPrivileges } from '../app_features_privileges'; +import { SecuritySubFeatureId } from '../app_features_keys'; +import { APP_ID } from '../constants'; +import type { SecurityFeatureParams } from './types'; const endpointListSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.endpointList.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.privilegesTooltip', { defaultMessage: 'All Spaces is required for Endpoint List access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.endpointList', { - defaultMessage: 'Endpoint List', - }), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList', + { + defaultMessage: 'Endpoint List', + } + ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.endpointList.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.description', { defaultMessage: 'Displays all hosts running Elastic Defend and their relevant integration details.', @@ -61,16 +67,19 @@ const endpointListSubFeature: SubFeatureConfig = { const trustedApplicationsSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.privilegesTooltip', { defaultMessage: 'All Spaces is required for Trusted Applications access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.trustedApplications', { - defaultMessage: 'Trusted Applications', - }), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications', + { + defaultMessage: 'Trusted Applications', + } + ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.description', { defaultMessage: 'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.', @@ -115,19 +124,19 @@ const trustedApplicationsSubFeature: SubFeatureConfig = { const hostIsolationExceptionsSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip', { defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.', } ), name: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions', + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions', { defaultMessage: 'Host Isolation Exceptions', } ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.description', { defaultMessage: 'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.', @@ -172,16 +181,16 @@ const hostIsolationExceptionsSubFeature: SubFeatureConfig = { const blocklistSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.blockList.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.privilegesTooltip', { defaultMessage: 'All Spaces is required for Blocklist access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.blockList', { + name: i18n.translate('securitySolutionPackages.features.featureRegistry.subFeatures.blockList', { defaultMessage: 'Blocklist', }), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.blockList.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.description', { defaultMessage: 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', @@ -226,16 +235,19 @@ const blocklistSubFeature: SubFeatureConfig = { const eventFiltersSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.privilegesTooltip', { defaultMessage: 'All Spaces is required for Event Filters access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.eventFilters', { - defaultMessage: 'Event Filters', - }), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters', + { + defaultMessage: 'Event Filters', + } + ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.description', { defaultMessage: 'Filter out endpoint events that you do not need or want stored in Elasticsearch.', @@ -280,16 +292,19 @@ const eventFiltersSubFeature: SubFeatureConfig = { const policyManagementSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.privilegesTooltip', { defaultMessage: 'All Spaces is required for Policy Management access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.policyManagement', { - defaultMessage: 'Elastic Defend Policy Management', - }), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement', + { + defaultMessage: 'Elastic Defend Policy Management', + } + ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.description', { defaultMessage: 'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.', @@ -329,19 +344,19 @@ const policyManagementSubFeature: SubFeatureConfig = { const responseActionsHistorySubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip', { defaultMessage: 'All Spaces is required for Response Actions History access.', } ), name: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory', + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory', { defaultMessage: 'Response Actions History', } ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.description', { defaultMessage: 'Access the history of response actions performed on endpoints.', } @@ -379,16 +394,19 @@ const responseActionsHistorySubFeature: SubFeatureConfig = { const hostIsolationSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.privilegesTooltip', { defaultMessage: 'All Spaces is required for Host Isolation access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.hostIsolation', { - defaultMessage: 'Host Isolation', - }), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation', + { + defaultMessage: 'Host Isolation', + } + ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.description', { defaultMessage: 'Perform the "isolate" and "release" response actions.' } ), privilegeGroups: [ @@ -396,6 +414,7 @@ const hostIsolationSubFeature: SubFeatureConfig = { groupType: 'mutually_exclusive', privileges: [ { + api: [`${APP_ID}-writeHostIsolationRelease`], id: 'host_isolation_all', includeIn: 'none', name: 'All', @@ -403,11 +422,6 @@ const hostIsolationSubFeature: SubFeatureConfig = { all: [], read: [], }, - // FYI: The current set of values below (`api`, `ui`) cover only `release` response action. - // There is a second set of values for API and UI that are added later if `endpointResponseActions` - // appFeature is enabled. Needed to ensure that in a downgrade of license condition, - // users are still able to un-isolate a host machine. - api: [`${APP_ID}-writeHostIsolationRelease`], ui: ['writeHostIsolationRelease'], }, ], @@ -418,16 +432,19 @@ const hostIsolationSubFeature: SubFeatureConfig = { const processOperationsSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.processOperations.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.privilegesTooltip', { defaultMessage: 'All Spaces is required for Process Operations access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.processOperations', { - defaultMessage: 'Process Operations', - }), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations', + { + defaultMessage: 'Process Operations', + } + ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.processOperations.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.description', { defaultMessage: 'Perform process-related response actions in the response console.', } @@ -454,16 +471,19 @@ const processOperationsSubFeature: SubFeatureConfig = { const fileOperationsSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.fileOperations.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.privilegesTooltip', { defaultMessage: 'All Spaces is required for File Operations access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.fileOperations', { - defaultMessage: 'File Operations', - }), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations', + { + defaultMessage: 'File Operations', + } + ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.fileOperations.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.description', { defaultMessage: 'Perform file-related response actions in the response console.', } @@ -493,16 +513,19 @@ const fileOperationsSubFeature: SubFeatureConfig = { const executeActionSubFeature: SubFeatureConfig = { requireAllSpaces: true, privilegesTooltip: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.executeOperations.privilegesTooltip', + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.privilegesTooltip', { defaultMessage: 'All Spaces is required for Execute Operations access.', } ), - name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.executeOperations', { - defaultMessage: 'Execute Operations', - }), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations', + { + defaultMessage: 'Execute Operations', + } + ), description: i18n.translate( - 'xpack.securitySolution.featureRegistry.subFeatures.executeOperations.description', + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.description', { // TODO: Update this description before 8.8 FF defaultMessage: 'Perform script execution on the endpoint.', @@ -528,24 +551,71 @@ const executeActionSubFeature: SubFeatureConfig = { ], }; -export enum SecuritySubFeatureId { - endpointList = 'endpointListSubFeature', - trustedApplications = 'trustedApplicationsSubFeature', - hostIsolationExceptions = 'hostIsolationExceptionsSubFeature', - blocklist = 'blocklistSubFeature', - eventFilters = 'eventFiltersSubFeature', - policyManagement = 'policyManagementSubFeature', - responseActionsHistory = 'responseActionsHistorySubFeature', - hostIsolation = 'hostIsolationSubFeature', - processOperations = 'processOperationsSubFeature', - fileOperations = 'fileOperationsSubFeature', - executeAction = 'executeActionSubFeature', -} +const endpointExceptionsSubFeature: SubFeatureConfig = { + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Endpoint Exceptions access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions', + { + defaultMessage: 'Endpoint Exceptions', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.description', + { + defaultMessage: 'Use Endpoint Exceptions (this is a test sub-feature).', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'endpoint_exceptions_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ...AppFeaturesPrivileges[AppFeaturesPrivilegeId.endpointExceptions].all, + }, + { + id: 'endpoint_exceptions_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ...AppFeaturesPrivileges[AppFeaturesPrivilegeId.endpointExceptions].read, + }, + ], + }, + ], +}; + +/** + * Sub-features that will always be available for Security + * regardless of the product type. + */ +export const getSecurityBaseKibanaSubFeatureIds = ( + { experimentalFeatures }: SecurityFeatureParams // currently un-used, but left here as a convenience for possible future use +): SecuritySubFeatureId[] => [SecuritySubFeatureId.hostIsolation]; -// Defines all the ordered Security subFeatures available +/** + * Defines all the Security Assistant subFeatures available. + * The order of the subFeatures is the order they will be displayed + */ export const securitySubFeaturesMap = Object.freeze( new Map([ [SecuritySubFeatureId.endpointList, endpointListSubFeature], + [SecuritySubFeatureId.endpointExceptions, endpointExceptionsSubFeature], [SecuritySubFeatureId.trustedApplications, trustedApplicationsSubFeature], [SecuritySubFeatureId.hostIsolationExceptions, hostIsolationExceptionsSubFeature], [SecuritySubFeatureId.blocklist, blocklistSubFeature], diff --git a/x-pack/packages/security-solution/features/src/security/types.ts b/x-pack/packages/security-solution/features/src/security/types.ts new file mode 100644 index 0000000000000..4c2fee865ecd2 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/security/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 { AppFeatureSecurityKey, SecuritySubFeatureId } from '../app_features_keys'; +import type { AppFeatureKibanaConfig } from '../types'; + +export interface SecurityFeatureParams { + experimentalFeatures: Record; + savedObjects: string[]; +} + +export type DefaultSecurityAppFeaturesConfig = Omit< + Record>, + AppFeatureSecurityKey.endpointExceptions + // | add not default security app features here +>; diff --git a/x-pack/packages/security-solution/features/src/types.ts b/x-pack/packages/security-solution/features/src/types.ts new file mode 100644 index 0000000000000..825e2e8e4c3b2 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/types.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 { + KibanaFeatureConfig, + SubFeatureConfig, + SubFeaturePrivilegeConfig, +} from '@kbn/features-plugin/common'; +import type { RecursivePartial } from '@kbn/utility-types'; +import type { + AppFeatureAssistantKey, + AppFeatureCasesKey, + AppFeatureKeyType, + AppFeatureSecurityKey, + AssistantSubFeatureId, + CasesSubFeatureId, + SecuritySubFeatureId, +} from './app_features_keys'; + +export type { AppFeatureKeyType }; +export type AppFeatureKeys = AppFeatureKeyType[]; + +// Features types +export type BaseKibanaFeatureConfig = Omit; +export type SubFeaturesPrivileges = RecursivePartial; +export type AppFeatureKibanaConfig = + RecursivePartial & { + subFeatureIds?: T[]; + subFeaturesPrivileges?: SubFeaturesPrivileges[]; + }; +export type AppFeaturesConfig = Map< + AppFeatureKeyType, + AppFeatureKibanaConfig +>; + +export type AppFeaturesSecurityConfig = Map< + AppFeatureSecurityKey, + AppFeatureKibanaConfig +>; +export type AppFeaturesCasesConfig = Map< + AppFeatureCasesKey, + AppFeatureKibanaConfig +>; + +export type AppFeaturesAssistantConfig = Map< + AppFeatureAssistantKey, + AppFeatureKibanaConfig +>; + +export type AppSubFeaturesMap = Map; + +export interface AppFeatureParams { + baseKibanaFeature: BaseKibanaFeatureConfig; + baseKibanaSubFeatureIds: T[]; + subFeaturesMap: AppSubFeaturesMap; +} diff --git a/x-pack/packages/security-solution/features/tsconfig.json b/x-pack/packages/security-solution/features/tsconfig.json new file mode 100644 index 0000000000000..2c153f831721d --- /dev/null +++ b/x-pack/packages/security-solution/features/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + ] + }, + "include": ["**/*.ts", "**/*.tsx"], + "kbn_references": [ + "@kbn/features-plugin", + "@kbn/utility-types", + "@kbn/i18n", + "@kbn/core-application-common", + "@kbn/cases-plugin", + "@kbn/securitysolution-rules", + "@kbn/securitysolution-list-constants", + ], + "exclude": ["target/**/*"] +} diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_icons.tsx b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_icons.tsx index b81b25144fcd0..f2b8247840cac 100644 --- a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_icons.tsx +++ b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_icons.tsx @@ -81,9 +81,7 @@ export const LandingLinkIcon: React.FC = React.memo(functi - - {description} - + {description} {children} diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images.tsx b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images.tsx index 18c8ef07c10cb..20caecd5a4e2c 100644 --- a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images.tsx +++ b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images.tsx @@ -89,7 +89,7 @@ export const LandingLinksImages: React.FC = React.memo( {isBeta && }
- + {description} diff --git a/x-pack/plugins/actions/common/routes/connector/apis/connector_types/index.ts b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/index.ts new file mode 100644 index 0000000000000..b4d93fc1be5e6 --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/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 { connectorTypesQuerySchema } from './schemas/latest'; +export type { ConnectorTypesRequestQuery } from './types/latest'; + +export { connectorTypesQuerySchema as connectorTypesQuerySchemaV1 } from './schemas/v1'; +export type { ConnectorTypesRequestQuery as ConnectorTypesRequestQueryV1 } from './types/v1'; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/index.ts b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/schemas/latest.ts similarity index 89% rename from x-pack/plugins/transform/public/app/lib/authorization/index.ts rename to x-pack/plugins/actions/common/routes/connector/apis/connector_types/schemas/latest.ts index e78d12e73a8ea..25300c97a6d2e 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/index.ts +++ b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/schemas/latest.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './components'; +export * from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/connector_types/schemas/v1.ts b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/schemas/v1.ts new file mode 100644 index 0000000000000..bdbc01efc4b7a --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/schemas/v1.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const connectorTypesQuerySchema = schema.object({ + feature_id: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/actions/common/routes/connector/apis/connector_types/types/latest.ts b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/types/latest.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 './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/connector_types/types/v1.ts b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/types/v1.ts new file mode 100644 index 0000000000000..cef43f8b41b18 --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/connector_types/types/v1.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 { TypeOf } from '@kbn/config-schema'; +import { connectorTypesQuerySchemaV1 } from '..'; + +export type ConnectorTypesRequestQuery = TypeOf; diff --git a/x-pack/plugins/actions/common/routes/connector/response/index.ts b/x-pack/plugins/actions/common/routes/connector/response/index.ts index cfc4d4a4a0098..99d08664c96a4 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/index.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/index.ts @@ -8,6 +8,7 @@ // Latest export type { ConnectorResponse, ActionTypeConfig } from './types/latest'; export { connectorResponseSchema } from './schemas/latest'; +export { connectorTypesResponseSchema } from './schemas/latest'; // v1 export type { @@ -15,3 +16,5 @@ export type { ActionTypeConfig as ActionTypeConfigV1, } from './types/v1'; export { connectorResponseSchema as connectorResponseSchemaV1 } from './schemas/v1'; +export type { ConnectorTypesResponse as ConnectorTypesResponseV1 } from './types/v1'; +export { connectorTypesResponseSchema as connectorTypesResponseSchemaV1 } from './schemas/v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts b/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts index 96d490935d339..dbb5659baf8c4 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts @@ -6,3 +6,4 @@ */ export { connectorResponseSchema } from './v1'; +export { connectorTypesResponseSchema } from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts b/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts index 6726aa740cf87..4f8be5b0f344f 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts @@ -18,3 +18,21 @@ export const connectorResponseSchema = schema.object({ is_system_action: schema.boolean(), referenced_by_count: schema.number(), }); + +export const connectorTypesResponseSchema = schema.object({ + id: schema.string(), + name: schema.string(), + enabled: schema.boolean(), + enabled_in_config: schema.boolean(), + enabled_in_license: schema.boolean(), + minimum_license_required: schema.oneOf([ + schema.literal('basic'), + schema.literal('standard'), + schema.literal('gold'), + schema.literal('platinum'), + schema.literal('enterprise'), + schema.literal('trial'), + ]), + supported_feature_ids: schema.arrayOf(schema.string()), + is_system_action_type: schema.boolean(), +}); diff --git a/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts b/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts index 9f49c048f92bf..5aed107c6f4f6 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts @@ -6,7 +6,7 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { connectorResponseSchemaV1 } from '..'; +import { connectorResponseSchemaV1, connectorTypesResponseSchemaV1 } from '..'; export type ActionTypeConfig = Record; type ConnectorResponseSchemaType = TypeOf; @@ -22,3 +22,15 @@ export interface ConnectorResponse; +export interface ConnectorTypesResponse { + id: ConnectorTypesResponseSchemaType['id']; + name: ConnectorTypesResponseSchemaType['name']; + enabled: ConnectorTypesResponseSchemaType['enabled']; + enabled_in_config: ConnectorTypesResponseSchemaType['enabled_in_config']; + enabled_in_license: ConnectorTypesResponseSchemaType['enabled_in_license']; + minimum_license_required: ConnectorTypesResponseSchemaType['minimum_license_required']; + supported_feature_ids: ConnectorTypesResponseSchemaType['supported_feature_ids']; + is_system_action_type: ConnectorTypesResponseSchemaType['is_system_action_type']; +} diff --git a/x-pack/plugins/actions/kibana.jsonc b/x-pack/plugins/actions/kibana.jsonc index 9152e64ba898a..78f66742c2a03 100644 --- a/x-pack/plugins/actions/kibana.jsonc +++ b/x-pack/plugins/actions/kibana.jsonc @@ -21,7 +21,8 @@ "usageCollection", "spaces", "security", - "monitoringCollection" + "monitoringCollection", + "serverless" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client/actions_client.mock.ts similarity index 97% rename from x-pack/plugins/actions/server/actions_client.mock.ts rename to x-pack/plugins/actions/server/actions_client/actions_client.mock.ts index 3fb08e5008793..5a369272617a2 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.mock.ts @@ -21,7 +21,6 @@ const createActionsClientMock = () => { getBulk: jest.fn(), getOAuthAccessToken: jest.fn(), execute: jest.fn(), - enqueueExecution: jest.fn(), ephemeralEnqueuedExecution: jest.fn(), bulkEnqueueExecution: jest.fn(), listTypes: jest.fn(), 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 94adb4a6205c2..fa7410bb71178 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 @@ -97,7 +97,6 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const actionExecutor = actionExecutorMock.create(); const authorization = actionsAuthorizationMock.create(); -const executionEnqueuer = jest.fn(); const ephemeralExecutionEnqueuer = jest.fn(); const bulkExecutionEnqueuer = jest.fn(); const request = httpServerMock.createKibanaRequest(); @@ -144,7 +143,6 @@ beforeEach(() => { kibanaIndices, inMemoryConnectors: [], actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -612,7 +610,6 @@ describe('create()', () => { kibanaIndices, inMemoryConnectors: [], actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -739,7 +736,6 @@ describe('create()', () => { ], actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -802,7 +798,6 @@ describe('create()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -864,7 +859,6 @@ describe('get()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -902,7 +896,6 @@ describe('get()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -960,7 +953,6 @@ describe('get()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1004,7 +996,6 @@ describe('get()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1127,7 +1118,6 @@ describe('get()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1172,7 +1162,6 @@ describe('get()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1207,7 +1196,6 @@ describe('get()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1279,7 +1267,6 @@ describe('getBulk()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1418,7 +1405,6 @@ describe('getBulk()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1514,7 +1500,6 @@ describe('getBulk()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1589,7 +1574,6 @@ describe('getBulk()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -1675,7 +1659,6 @@ describe('getOAuthAccessToken()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -2058,7 +2041,6 @@ describe('delete()', () => { }, ], actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -2095,7 +2077,6 @@ describe('delete()', () => { }, ], actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -2611,7 +2592,6 @@ describe('update()', () => { }, ], actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -2655,7 +2635,6 @@ describe('update()', () => { }, ], actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -2764,7 +2743,6 @@ describe('execute()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -2829,7 +2807,6 @@ describe('execute()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -2893,7 +2870,6 @@ describe('execute()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -3029,85 +3005,6 @@ describe('execute()', () => { }); }); -describe('enqueueExecution()', () => { - describe('authorization', () => { - test('ensures user is authorised to excecute actions', async () => { - (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { - return AuthorizationMode.RBAC; - }); - await actionsClient.enqueueExecution({ - id: uuidv4(), - params: {}, - spaceId: 'default', - executionId: '123abc', - apiKey: null, - source: asHttpRequestExecutionSource(request), - }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - operation: 'execute', - }); - }); - - test('throws when user is not authorised to create the type of action', async () => { - (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { - return AuthorizationMode.RBAC; - }); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to execute all actions`) - ); - - await expect( - actionsClient.enqueueExecution({ - id: uuidv4(), - params: {}, - spaceId: 'default', - executionId: '123abc', - apiKey: null, - source: asHttpRequestExecutionSource(request), - }) - ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - operation: 'execute', - }); - }); - - test('tracks legacy RBAC', async () => { - (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { - return AuthorizationMode.Legacy; - }); - - await actionsClient.enqueueExecution({ - id: uuidv4(), - params: {}, - spaceId: 'default', - executionId: '123abc', - apiKey: null, - source: asHttpRequestExecutionSource(request), - }); - - expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith( - 'enqueueExecution', - mockUsageCounter - ); - }); - }); - - test('calls the executionEnqueuer with the appropriate parameters', async () => { - const opts = { - id: uuidv4(), - params: { baz: false }, - spaceId: 'default', - executionId: '123abc', - apiKey: Buffer.from('123:abc').toString('base64'), - source: asHttpRequestExecutionSource(request), - }; - await expect(actionsClient.enqueueExecution(opts)).resolves.toMatchInlineSnapshot(`undefined`); - - expect(executionEnqueuer).toHaveBeenCalledWith(unsecuredSavedObjectsClient, opts); - }); -}); - describe('bulkEnqueueExecution()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { @@ -3233,172 +3130,6 @@ describe('bulkEnqueueExecution()', () => { }); }); -describe('listType()', () => { - it('filters action types by feature ID', async () => { - mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); - - actionTypeRegistry.register({ - id: 'my-action-type', - name: 'My action type', - minimumLicenseRequired: 'basic', - supportedFeatureIds: ['alerting'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - executor, - }); - - actionTypeRegistry.register({ - id: 'my-action-type-2', - name: 'My action type 2', - minimumLicenseRequired: 'basic', - supportedFeatureIds: ['cases'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - executor, - }); - - expect(await actionsClient.listTypes({ featureId: 'alerting' })).toEqual([ - { - id: 'my-action-type', - name: 'My action type', - minimumLicenseRequired: 'basic', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - supportedFeatureIds: ['alerting'], - isSystemActionType: false, - }, - ]); - }); - - it('filters out system action types when not defining options', async () => { - mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); - - actionTypeRegistry.register({ - id: 'my-action-type', - name: 'My action type', - minimumLicenseRequired: 'basic', - supportedFeatureIds: ['alerting'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - executor, - }); - - actionTypeRegistry.register({ - id: 'my-action-type-2', - name: 'My action type 2', - minimumLicenseRequired: 'basic', - supportedFeatureIds: ['cases'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - executor, - }); - - actionTypeRegistry.register({ - id: '.cases', - name: 'Cases', - minimumLicenseRequired: 'platinum', - supportedFeatureIds: ['alerting'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - isSystemActionType: true, - executor, - }); - - expect(await actionsClient.listTypes()).toEqual([ - { - id: 'my-action-type', - name: 'My action type', - minimumLicenseRequired: 'basic', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - supportedFeatureIds: ['alerting'], - isSystemActionType: false, - }, - { - id: 'my-action-type-2', - name: 'My action type 2', - isSystemActionType: false, - minimumLicenseRequired: 'basic', - supportedFeatureIds: ['cases'], - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }, - ]); - }); - - it('return system action types when defining options', async () => { - mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); - - actionTypeRegistry.register({ - id: 'my-action-type', - name: 'My action type', - minimumLicenseRequired: 'basic', - supportedFeatureIds: ['alerting'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - executor, - }); - - actionTypeRegistry.register({ - id: '.cases', - name: 'Cases', - minimumLicenseRequired: 'platinum', - supportedFeatureIds: ['alerting'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - isSystemActionType: true, - executor, - }); - - expect(await actionsClient.listTypes({ includeSystemActionTypes: true })).toEqual([ - { - id: 'my-action-type', - name: 'My action type', - minimumLicenseRequired: 'basic', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - supportedFeatureIds: ['alerting'], - isSystemActionType: false, - }, - { - id: '.cases', - name: 'Cases', - isSystemActionType: true, - minimumLicenseRequired: 'platinum', - supportedFeatureIds: ['alerting'], - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }, - ]); - }); -}); - describe('isActionTypeEnabled()', () => { const fooActionType: ActionType = { id: 'foo', @@ -3442,7 +3173,6 @@ describe('isPreconfigured()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -3493,7 +3223,6 @@ describe('isPreconfigured()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -3546,7 +3275,6 @@ describe('isSystemAction()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -3597,7 +3325,6 @@ describe('isSystemAction()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, 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 9fbf7c988c8f0..392b5ec7354b6 100644 --- a/x-pack/plugins/actions/server/actions_client/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.ts @@ -25,9 +25,10 @@ import { RunNowResult } from '@kbn/task-manager-plugin/server'; import { IEventLogClient } from '@kbn/event-log-plugin/server'; import { KueryNode } from '@kbn/es-query'; import { FindConnectorResult } from '../application/connector/types'; +import { ConnectorType } from '../application/connector/types'; import { getAll } from '../application/connector/methods/get_all'; +import { listTypes } from '../application/connector/methods/list_types'; import { - ActionType, GetGlobalExecutionKPIParams, GetGlobalExecutionLogParams, IExecutionLogResult, @@ -48,7 +49,6 @@ import { ActionTypeExecutorResult, ConnectorTokenClientContract, } from '../types'; - import { PreconfiguredActionDisabledModificationError } from '../lib/errors/preconfigured_action_disabled_modification'; import { ExecuteOptions } from '../lib/action_executor'; import { @@ -88,6 +88,7 @@ import { getExecutionLogAggregation, } from '../lib/get_execution_log_aggregation'; import { connectorFromSavedObject, isConnectorDeprecated } from '../application/connector/lib'; +import { ListTypesParams } from '../application/connector/methods/list_types/types'; interface ActionUpdate { name: string; @@ -104,7 +105,7 @@ export interface CreateOptions { options?: { id?: string }; } -interface ConstructorOptions { +export interface ConstructorOptions { logger: Logger; kibanaIndices: string[]; scopedClusterClient: IScopedClusterClient; @@ -112,7 +113,6 @@ interface ConstructorOptions { unsecuredSavedObjectsClient: SavedObjectsClientContract; inMemoryConnectors: InMemoryConnector[]; actionExecutor: ActionExecutorContract; - executionEnqueuer: ExecutionEnqueuer; ephemeralExecutionEnqueuer: ExecutionEnqueuer; bulkExecutionEnqueuer: BulkExecutionEnqueuer; request: KibanaRequest; @@ -128,11 +128,6 @@ export interface UpdateOptions { action: ActionUpdate; } -interface ListTypesOptions { - featureId?: string; - includeSystemActionTypes?: boolean; -} - export interface ActionsClientContext { logger: Logger; kibanaIndices: string[]; @@ -143,7 +138,6 @@ export interface ActionsClientContext { actionExecutor: ActionExecutorContract; request: KibanaRequest; authorization: ActionsAuthorization; - executionEnqueuer: ExecutionEnqueuer; ephemeralExecutionEnqueuer: ExecutionEnqueuer; bulkExecutionEnqueuer: BulkExecutionEnqueuer; auditLogger?: AuditLogger; @@ -163,7 +157,6 @@ export class ActionsClient { unsecuredSavedObjectsClient, inMemoryConnectors, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -181,7 +174,6 @@ export class ActionsClient { kibanaIndices, inMemoryConnectors, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -774,25 +766,6 @@ export class ActionsClient { }); } - public async enqueueExecution(options: EnqueueExecutionOptions): Promise { - const { source } = options; - if ( - (await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) === - AuthorizationMode.RBAC - ) { - /** - * For scheduled executions the additional authorization check - * for system actions (kibana privileges) will be performed - * inside the ActionExecutor at execution time - */ - - await this.context.authorization.ensureAuthorized({ operation: 'execute' }); - } else { - trackLegacyRBACExemption('enqueueExecution', this.context.usageCounter); - } - return this.context.executionEnqueuer(this.context.unsecuredSavedObjectsClient, options); - } - public async bulkEnqueueExecution(options: EnqueueExecutionOptions[]): Promise { const sources: Array> = []; options.forEach((option) => { @@ -839,21 +812,11 @@ export class ActionsClient { ); } - /** - * Return all available action types - * expect system action types - */ public async listTypes({ featureId, includeSystemActionTypes = false, - }: ListTypesOptions = {}): Promise { - const actionTypes = this.context.actionTypeRegistry.list(featureId); - - const filteredActionTypes = includeSystemActionTypes - ? actionTypes - : actionTypes.filter((actionType) => !Boolean(actionType.isSystemActionType)); - - return filteredActionTypes; + }: ListTypesParams = {}): Promise { + return listTypes(this.context, { featureId, includeSystemActionTypes }); } public isActionTypeEnabled( diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts index 938961f5f38f5..7e2ceb5b653c8 100644 --- a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts +++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts @@ -67,7 +67,6 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const actionExecutor = actionExecutorMock.create(); const authorization = actionsAuthorizationMock.create(); -const executionEnqueuer = jest.fn(); const ephemeralExecutionEnqueuer = jest.fn(); const bulkExecutionEnqueuer = jest.fn(); const request = httpServerMock.createKibanaRequest(); @@ -93,7 +92,6 @@ describe('getAll()', () => { kibanaIndices, inMemoryConnectors: [], actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -149,7 +147,6 @@ describe('getAll()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -294,7 +291,6 @@ describe('getAll()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -398,7 +394,6 @@ describe('getAll()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, @@ -504,7 +499,6 @@ describe('getAll()', () => { scopedClusterClient, kibanaIndices, actionExecutor, - executionEnqueuer, ephemeralExecutionEnqueuer, bulkExecutionEnqueuer, request, diff --git a/x-pack/plugins/security_solution/server/lib/app_features/index.ts b/x-pack/plugins/actions/server/application/connector/methods/list_types/index.ts similarity index 84% rename from x-pack/plugins/security_solution/server/lib/app_features/index.ts rename to x-pack/plugins/actions/server/application/connector/methods/list_types/index.ts index 5f30b21f9c862..e46209cd26fba 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/index.ts +++ b/x-pack/plugins/actions/server/application/connector/methods/list_types/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { AppFeatures } from './app_features'; +export { listTypes } from './list_types'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/list_types/list_types.test.ts b/x-pack/plugins/actions/server/application/connector/methods/list_types/list_types.test.ts new file mode 100644 index 0000000000000..9ff5e05d45506 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/list_types/list_types.test.ts @@ -0,0 +1,235 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { actionsConfigMock } from '../../../../actions_config.mock'; +import { ActionTypeRegistry, ActionTypeRegistryOpts } from '../../../../action_type_registry'; +import { ActionsAuthorization } from '../../../../authorization/actions_authorization'; +import { ActionExecutor, ILicenseState, TaskRunnerFactory } from '../../../../lib'; +import { actionExecutorMock } from '../../../../lib/action_executor.mock'; +import { connectorTokenClientMock } from '../../../../lib/connector_token_client.mock'; +import { licenseStateMock } from '../../../../lib/license_state.mock'; +import { actionsAuthorizationMock } from '../../../../mocks'; +import { inMemoryMetricsMock } from '../../../../monitoring/in_memory_metrics.mock'; +import { schema } from '@kbn/config-schema'; +import { + httpServerMock, + loggingSystemMock, + elasticsearchServiceMock, + savedObjectsClientMock, +} from '@kbn/core/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { ActionsClient } from '../../../../actions_client/actions_client'; +import { ExecutorType } from '../../../../types'; + +let mockedLicenseState: jest.Mocked; +let actionTypeRegistryParams: ActionTypeRegistryOpts; +let actionTypeRegistry: ActionTypeRegistry; + +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { + return { status: 'ok', actionId: options.actionId }; +}; + +describe('listTypes()', () => { + let actionsClient: ActionsClient; + + beforeEach(async () => { + jest.resetAllMocks(); + mockedLicenseState = licenseStateMock.create(); + actionTypeRegistryParams = { + licensing: licensingMock.createSetup(), + taskManager: taskManagerMock.createSetup(), + taskRunnerFactory: new TaskRunnerFactory( + new ActionExecutor({ isESOCanEncrypt: true }), + inMemoryMetricsMock.create() + ), + actionsConfigUtils: actionsConfigMock.create(), + licenseState: mockedLicenseState, + inMemoryConnectors: [], + }; + actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionsClient = new ActionsClient({ + logger: loggingSystemMock.create().get(), + kibanaIndices: ['.kibana'], + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + actionTypeRegistry, + unsecuredSavedObjectsClient: savedObjectsClientMock.create(), + inMemoryConnectors: [], + actionExecutor: actionExecutorMock.create(), + ephemeralExecutionEnqueuer: jest.fn(), + bulkExecutionEnqueuer: jest.fn(), + request: httpServerMock.createKibanaRequest(), + authorization: actionsAuthorizationMock.create() as unknown as ActionsAuthorization, + connectorTokenClient: connectorTokenClientMock.create(), + getEventLogClient: jest.fn(), + }); + }); + + it('filters action types by feature ID', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + actionTypeRegistry.register({ + id: 'my-action-type-2', + name: 'My action type 2', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['cases'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + expect(await actionsClient.listTypes({ featureId: 'alerting' })).toEqual([ + { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + supportedFeatureIds: ['alerting'], + isSystemActionType: false, + }, + ]); + }); + + it('filters out system action types when not defining options', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + actionTypeRegistry.register({ + id: 'my-action-type-2', + name: 'My action type 2', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['cases'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + actionTypeRegistry.register({ + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + isSystemActionType: true, + executor, + }); + + expect(await actionsClient.listTypes({})).toEqual([ + { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + supportedFeatureIds: ['alerting'], + isSystemActionType: false, + }, + { + id: 'my-action-type-2', + name: 'My action type 2', + isSystemActionType: false, + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['cases'], + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + ]); + }); + + it('return system action types when defining options', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + actionTypeRegistry.register({ + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + isSystemActionType: true, + executor, + }); + + expect(await actionsClient.listTypes({ includeSystemActionTypes: true })).toEqual([ + { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + supportedFeatureIds: ['alerting'], + isSystemActionType: false, + }, + { + id: '.cases', + name: 'Cases', + isSystemActionType: true, + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + ]); + }); +}); diff --git a/x-pack/plugins/actions/server/application/connector/methods/list_types/list_types.ts b/x-pack/plugins/actions/server/application/connector/methods/list_types/list_types.ts new file mode 100644 index 0000000000000..cf218eba89355 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/list_types/list_types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { ActionsClientContext } from '../../../../actions_client'; +import { ConnectorType } from '../../types'; +import { listTypesParamsSchema } from './schemas'; +import { ListTypesParams } from './types'; + +export async function listTypes( + context: ActionsClientContext, + options: ListTypesParams +): Promise { + try { + listTypesParamsSchema.validate(options); + } catch (error) { + throw Boom.badRequest(`Error validating params - ${error.message}`); + } + + const { featureId, includeSystemActionTypes } = options; + + const connectorTypes = context.actionTypeRegistry.list(featureId); + + const filteredConnectorTypes = includeSystemActionTypes + ? connectorTypes + : connectorTypes.filter((type) => !Boolean(type.isSystemActionType)); + + return filteredConnectorTypes; +} diff --git a/x-pack/plugins/actions/server/application/connector/methods/list_types/schemas/index.ts b/x-pack/plugins/actions/server/application/connector/methods/list_types/schemas/index.ts new file mode 100644 index 0000000000000..afc2a2e545ac1 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/list_types/schemas/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 { listTypesParamsSchema } from './list_types_params_schema'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/list_types/schemas/list_types_params_schema.ts b/x-pack/plugins/actions/server/application/connector/methods/list_types/schemas/list_types_params_schema.ts new file mode 100644 index 0000000000000..848345c79ff31 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/list_types/schemas/list_types_params_schema.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 { schema } from '@kbn/config-schema'; + +export const listTypesParamsSchema = schema.object({ + featureId: schema.maybe(schema.string()), + includeSystemActionTypes: schema.maybe(schema.boolean()), +}); diff --git a/x-pack/plugins/actions/server/application/connector/methods/list_types/types/index.ts b/x-pack/plugins/actions/server/application/connector/methods/list_types/types/index.ts new file mode 100644 index 0000000000000..763aba62f135d --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/list_types/types/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 type { ListTypesParams } from './list_types_params'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/list_types/types/list_types_params.ts b/x-pack/plugins/actions/server/application/connector/methods/list_types/types/list_types_params.ts new file mode 100644 index 0000000000000..ac6d8b292964c --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/list_types/types/list_types_params.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 { TypeOf } from '@kbn/config-schema'; +import { listTypesParamsSchema } from '../schemas'; + +type ListTypesParamsType = TypeOf; + +export interface ListTypesParams { + featureId?: ListTypesParamsType['featureId']; + includeSystemActionTypes?: ListTypesParamsType['includeSystemActionTypes']; +} diff --git a/x-pack/plugins/actions/server/application/connector/schemas/connector_type_schema.ts b/x-pack/plugins/actions/server/application/connector/schemas/connector_type_schema.ts new file mode 100644 index 0000000000000..e5556ab5c4a33 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/schemas/connector_type_schema.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const connectorTypeSchema = schema.object({ + id: schema.string(), + name: schema.string(), + enabled: schema.boolean(), + enabledInConfig: schema.boolean(), + enabledInLicense: schema.boolean(), + minimumLicenseRequired: schema.oneOf([ + schema.literal('basic'), + schema.literal('standard'), + schema.literal('gold'), + schema.literal('platinum'), + schema.literal('enterprise'), + schema.literal('trial'), + ]), + supportedFeatureIds: schema.arrayOf(schema.string()), + isSystemActionType: schema.boolean(), +}); diff --git a/x-pack/plugins/actions/server/application/connector/schemas/index.ts b/x-pack/plugins/actions/server/application/connector/schemas/index.ts index f2a1bc4c6096a..b3cfc462c4208 100644 --- a/x-pack/plugins/actions/server/application/connector/schemas/index.ts +++ b/x-pack/plugins/actions/server/application/connector/schemas/index.ts @@ -6,3 +6,4 @@ */ export * from './connector_schema'; +export * from './connector_type_schema'; diff --git a/x-pack/plugins/actions/server/application/connector/types/connector_type.ts b/x-pack/plugins/actions/server/application/connector/types/connector_type.ts new file mode 100644 index 0000000000000..64be01365a8ba --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/types/connector_type.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { connectorTypeSchema } from '../schemas'; + +type ConnectorTypeSchemaType = TypeOf; + +export interface ConnectorType { + id: ConnectorTypeSchemaType['id']; + name: ConnectorTypeSchemaType['name']; + enabled: ConnectorTypeSchemaType['enabled']; + enabledInConfig: ConnectorTypeSchemaType['enabledInConfig']; + enabledInLicense: ConnectorTypeSchemaType['enabledInLicense']; + minimumLicenseRequired: ConnectorTypeSchemaType['minimumLicenseRequired']; + supportedFeatureIds: ConnectorTypeSchemaType['supportedFeatureIds']; + isSystemActionType: ConnectorTypeSchemaType['isSystemActionType']; +} diff --git a/x-pack/plugins/actions/server/application/connector/types/index.ts b/x-pack/plugins/actions/server/application/connector/types/index.ts index ab87e9a5baaad..973513ec7b5cb 100644 --- a/x-pack/plugins/actions/server/application/connector/types/index.ts +++ b/x-pack/plugins/actions/server/application/connector/types/index.ts @@ -6,3 +6,4 @@ */ export type { Connector, FindConnectorResult } from './connector'; +export type { ConnectorType } from './connector_type'; diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 9a620d1452f23..da45fd40cf925 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -64,6 +64,15 @@ const connectorTypeSchema = schema.object({ maxAttempts: schema.maybe(schema.number({ min: MIN_MAX_ATTEMPTS, max: MAX_MAX_ATTEMPTS })), }); +// We leverage enabledActionTypes list by allowing the other plugins to overwrite it by using "setEnabledConnectorTypes" in the plugin setup. +// The list can be overwritten only if it's not already been set in the config. +const enabledConnectorTypesSchema = schema.arrayOf( + schema.oneOf([schema.string(), schema.literal(EnabledActionTypes.Any)]), + { + defaultValue: [AllowedHosts.Any], + } +); + export const configSchema = schema.object({ allowedHosts: schema.arrayOf( schema.oneOf([schema.string({ hostname: true }), schema.literal(AllowedHosts.Any)]), @@ -71,12 +80,7 @@ export const configSchema = schema.object({ defaultValue: [AllowedHosts.Any], } ), - enabledActionTypes: schema.arrayOf( - schema.oneOf([schema.string(), schema.literal(EnabledActionTypes.Any)]), - { - defaultValue: [AllowedHosts.Any], - } - ), + enabledActionTypes: enabledConnectorTypesSchema, preconfiguredAlertHistoryEsIndex: schema.boolean({ defaultValue: false }), preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, { defaultValue: {}, @@ -129,6 +133,7 @@ export const configSchema = schema.object({ }); export type ActionsConfig = TypeOf; +export type EnabledConnectorTypes = TypeOf; // It would be nicer to add the proxyBypassHosts / proxyOnlyHosts restriction on // simultaneous usage in the config validator directly, but there's no good way to express 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 fae2ca6c0f9d9..72903ca433b4e 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -8,10 +8,7 @@ import { KibanaRequest } from '@kbn/core/server'; import { v4 as uuidv4 } from 'uuid'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { - createExecutionEnqueuerFunction, - createBulkExecutionEnqueuerFunction, -} from './create_execute_function'; +import { createBulkExecutionEnqueuerFunction } from './create_execute_function'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { actionTypeRegistryMock } from './action_type_registry.mock'; import { @@ -25,776 +22,6 @@ const request = {} as KibanaRequest; beforeEach(() => jest.resetAllMocks()); -describe('execute()', () => { - test('schedules the action with all given parameters', async () => { - const actionTypeRegistry = actionTypeRegistryMock.create(); - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - actionTypeRegistry, - isESOCanEncrypt: true, - inMemoryConnectors: [], - }); - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - await executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - executionId: '123abc', - apiKey: Buffer.from('123:abc').toString('base64'), - source: asHttpRequestExecutionSource(request), - }); - expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "actionTaskParamsId": "234", - "spaceId": "default", - }, - "scope": Array [ - "actions", - ], - "state": Object {}, - "taskType": "actions:mock-action", - }, - ] - `); - expect(savedObjectsClient.get).toHaveBeenCalledWith('action', '123'); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - 'action_task_params', - { - actionId: '123', - params: { baz: false }, - executionId: '123abc', - source: 'HTTP_REQUEST', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - { - references: [ - { - id: '123', - name: 'actionRef', - type: 'action', - }, - ], - } - ); - expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', { - notifyUsage: true, - }); - }); - - test('schedules the action with all given parameters and consumer', async () => { - const actionTypeRegistry = actionTypeRegistryMock.create(); - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - actionTypeRegistry, - isESOCanEncrypt: true, - inMemoryConnectors: [], - }); - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - await executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - executionId: '123abc', - consumer: 'test-consumer', - apiKey: Buffer.from('123:abc').toString('base64'), - source: asHttpRequestExecutionSource(request), - }); - expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "actionTaskParamsId": "234", - "spaceId": "default", - }, - "scope": Array [ - "actions", - ], - "state": Object {}, - "taskType": "actions:mock-action", - }, - ] - `); - expect(savedObjectsClient.get).toHaveBeenCalledWith('action', '123'); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - 'action_task_params', - { - actionId: '123', - params: { baz: false }, - executionId: '123abc', - consumer: 'test-consumer', - source: 'HTTP_REQUEST', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - { - references: [ - { - id: '123', - name: 'actionRef', - type: 'action', - }, - ], - } - ); - expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', { - notifyUsage: true, - }); - }); - - test('schedules the action with all given parameters and relatedSavedObjects', async () => { - const actionTypeRegistry = actionTypeRegistryMock.create(); - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - actionTypeRegistry, - isESOCanEncrypt: true, - inMemoryConnectors: [], - }); - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - await executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - apiKey: Buffer.from('123:abc').toString('base64'), - source: asHttpRequestExecutionSource(request), - executionId: '123abc', - relatedSavedObjects: [ - { - id: 'some-id', - namespace: 'some-namespace', - type: 'some-type', - typeId: 'some-typeId', - }, - ], - }); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - 'action_task_params', - { - actionId: '123', - params: { baz: false }, - apiKey: Buffer.from('123:abc').toString('base64'), - executionId: '123abc', - source: 'HTTP_REQUEST', - relatedSavedObjects: [ - { - id: 'related_some-type_0', - namespace: 'some-namespace', - type: 'some-type', - typeId: 'some-typeId', - }, - ], - }, - { - references: [ - { - id: '123', - name: 'actionRef', - type: 'action', - }, - { - id: 'some-id', - name: 'related_some-type_0', - type: 'some-type', - }, - ], - } - ); - }); - - test('schedules the action with all given parameters with a preconfigured action', async () => { - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - actionTypeRegistry: actionTypeRegistryMock.create(), - isESOCanEncrypt: true, - inMemoryConnectors: [ - { - id: '123', - actionTypeId: 'mock-action-preconfigured', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - ], - }); - const source = { type: 'alert', id: uuidv4() }; - - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - await executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - executionId: '123abc', - apiKey: Buffer.from('123:abc').toString('base64'), - source: asSavedObjectExecutionSource(source), - }); - expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "actionTaskParamsId": "234", - "spaceId": "default", - }, - "scope": Array [ - "actions", - ], - "state": Object {}, - "taskType": "actions:mock-action-preconfigured", - }, - ] - `); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - 'action_task_params', - { - actionId: '123', - params: { baz: false }, - executionId: '123abc', - source: 'SAVED_OBJECT', - apiKey: Buffer.from('123:abc').toString('base64'), - }, - { - references: [ - { - id: source.id, - name: 'source', - type: source.type, - }, - ], - } - ); - }); - - test('schedules the action with all given parameters with a system action', async () => { - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - actionTypeRegistry: actionTypeRegistryMock.create(), - isESOCanEncrypt: true, - inMemoryConnectors: [ - { - actionTypeId: 'test.system-action', - config: {}, - id: 'system-connector-test.system-action', - name: 'System action: test.system-action', - secrets: {}, - isPreconfigured: false, - isDeprecated: false, - isSystemAction: true, - }, - ], - }); - const source = { type: 'alert', id: uuidv4() }; - - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'test.system-action', - }, - references: [], - }); - - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - - await executeFn(savedObjectsClient, { - id: 'system-connector-test.system-action', - params: { baz: false }, - spaceId: 'default', - executionId: 'system-connector-.casesabc', - apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'), - source: asSavedObjectExecutionSource(source), - }); - - expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "actionTaskParamsId": "234", - "spaceId": "default", - }, - "scope": Array [ - "actions", - ], - "state": Object {}, - "taskType": "actions:test.system-action", - }, - ] - `); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - 'action_task_params', - { - actionId: 'system-connector-test.system-action', - params: { baz: false }, - executionId: 'system-connector-.casesabc', - source: 'SAVED_OBJECT', - apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'), - }, - { - references: [ - { - id: source.id, - name: 'source', - type: source.type, - }, - ], - } - ); - }); - - test('schedules the action with all given parameters with a preconfigured action and relatedSavedObjects', async () => { - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - actionTypeRegistry: actionTypeRegistryMock.create(), - isESOCanEncrypt: true, - inMemoryConnectors: [ - { - id: '123', - actionTypeId: 'mock-action-preconfigured', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - ], - }); - const source = { type: 'alert', id: uuidv4() }; - - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - await executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - apiKey: Buffer.from('123:abc').toString('base64'), - source: asSavedObjectExecutionSource(source), - executionId: '123abc', - relatedSavedObjects: [ - { - id: 'some-id', - namespace: 'some-namespace', - type: 'some-type', - typeId: 'some-typeId', - }, - ], - }); - expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "actionTaskParamsId": "234", - "spaceId": "default", - }, - "scope": Array [ - "actions", - ], - "state": Object {}, - "taskType": "actions:mock-action-preconfigured", - }, - ] - `); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - 'action_task_params', - { - actionId: '123', - params: { baz: false }, - apiKey: Buffer.from('123:abc').toString('base64'), - executionId: '123abc', - source: 'SAVED_OBJECT', - relatedSavedObjects: [ - { - id: 'related_some-type_0', - namespace: 'some-namespace', - type: 'some-type', - typeId: 'some-typeId', - }, - ], - }, - { - references: [ - { - id: source.id, - name: 'source', - type: source.type, - }, - { - id: 'some-id', - name: 'related_some-type_0', - type: 'some-type', - }, - ], - } - ); - }); - - test('schedules the action with all given parameters with a system action and relatedSavedObjects', async () => { - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - actionTypeRegistry: actionTypeRegistryMock.create(), - isESOCanEncrypt: true, - inMemoryConnectors: [ - { - actionTypeId: 'test.system-action', - config: {}, - id: 'system-connector-test.system-action', - name: 'System action: test.system-action', - secrets: {}, - isPreconfigured: false, - isDeprecated: false, - isSystemAction: true, - }, - ], - }); - const source = { type: 'alert', id: uuidv4() }; - - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'test.system-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - await executeFn(savedObjectsClient, { - id: 'system-connector-test.system-action', - params: { baz: false }, - spaceId: 'default', - apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'), - source: asSavedObjectExecutionSource(source), - executionId: 'system-connector-.casesabc', - relatedSavedObjects: [ - { - id: 'some-id', - namespace: 'some-namespace', - type: 'some-type', - typeId: 'some-typeId', - }, - ], - }); - expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "actionTaskParamsId": "234", - "spaceId": "default", - }, - "scope": Array [ - "actions", - ], - "state": Object {}, - "taskType": "actions:test.system-action", - }, - ] - `); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - 'action_task_params', - { - actionId: 'system-connector-test.system-action', - params: { baz: false }, - apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'), - executionId: 'system-connector-.casesabc', - source: 'SAVED_OBJECT', - relatedSavedObjects: [ - { - id: 'related_some-type_0', - namespace: 'some-namespace', - type: 'some-type', - typeId: 'some-typeId', - }, - ], - }, - { - references: [ - { - id: source.id, - name: 'source', - type: source.type, - }, - { - id: 'some-id', - name: 'related_some-type_0', - type: 'some-type', - }, - ], - } - ); - }); - - test('throws when passing isESOCanEncrypt with false as a value', async () => { - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - isESOCanEncrypt: false, - actionTypeRegistry: actionTypeRegistryMock.create(), - inMemoryConnectors: [], - }); - await expect( - executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - executionId: '123abc', - apiKey: null, - source: asHttpRequestExecutionSource(request), - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` - ); - }); - - test('throws when isMissingSecrets is true for connector', async () => { - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - isESOCanEncrypt: true, - actionTypeRegistry: actionTypeRegistryMock.create(), - inMemoryConnectors: [], - }); - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - name: 'mock-action', - isMissingSecrets: true, - actionTypeId: 'mock-action', - }, - references: [], - }); - await expect( - executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - executionId: '123abc', - apiKey: null, - source: asHttpRequestExecutionSource(request), - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to execute action because no secrets are defined for the \\"mock-action\\" connector."` - ); - }); - - test('should ensure action type is enabled', async () => { - const mockedActionTypeRegistry = actionTypeRegistryMock.create(); - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - isESOCanEncrypt: true, - actionTypeRegistry: mockedActionTypeRegistry, - inMemoryConnectors: [], - }); - mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { - throw new Error('Fail'); - }); - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - - await expect( - executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - executionId: '123abc', - apiKey: null, - source: asHttpRequestExecutionSource(request), - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); - }); - - test('should skip ensure action type if action type is preconfigured and license is valid', async () => { - const mockedActionTypeRegistry = actionTypeRegistryMock.create(); - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - isESOCanEncrypt: true, - actionTypeRegistry: mockedActionTypeRegistry, - inMemoryConnectors: [ - { - actionTypeId: 'mock-action', - config: {}, - id: 'my-slack1', - name: 'Slack #xyz', - secrets: {}, - isPreconfigured: true, - isSystemAction: false, - isDeprecated: false, - }, - ], - }); - mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true); - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'mock-action', - }, - references: [], - }); - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - - await executeFn(savedObjectsClient, { - id: '123', - params: { baz: false }, - spaceId: 'default', - executionId: '123abc', - apiKey: null, - source: asHttpRequestExecutionSource(request), - }); - - expect(mockedActionTypeRegistry.ensureActionTypeEnabled).not.toHaveBeenCalled(); - }); - - test('should ensure if a system action type is enabled', async () => { - const mockedActionTypeRegistry = actionTypeRegistryMock.create(); - const executeFn = createExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - isESOCanEncrypt: true, - actionTypeRegistry: mockedActionTypeRegistry, - inMemoryConnectors: [ - { - actionTypeId: 'test.system-action', - config: {}, - id: 'system-connector-test.system-action', - name: 'System action: test.system-action', - secrets: {}, - isPreconfigured: false, - isDeprecated: false, - isSystemAction: true, - }, - ], - }); - - mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { - throw new Error('Fail'); - }); - - savedObjectsClient.get.mockResolvedValueOnce({ - id: '123', - type: 'action', - attributes: { - actionTypeId: 'test.system-action', - }, - references: [], - }); - - savedObjectsClient.create.mockResolvedValueOnce({ - id: '234', - type: 'action_task_params', - attributes: {}, - references: [], - }); - - await expect( - executeFn(savedObjectsClient, { - id: 'system-connector-test.system-action', - params: { baz: false }, - spaceId: 'default', - executionId: 'system-connector-.test.system-action-abc', - apiKey: null, - source: asHttpRequestExecutionSource(request), - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); - - expect(mockedActionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith( - 'test.system-action' - ); - }); -}); - describe('bulkExecute()', () => { test('schedules the action with all given parameters', async () => { const actionTypeRegistry = actionTypeRegistryMock.create(); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 6752b17fd5ffd..3b4233ddf5710 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -54,87 +54,6 @@ export type BulkExecutionEnqueuer = ( actionsToExectute: ExecuteOptions[] ) => Promise; -export function createExecutionEnqueuerFunction({ - taskManager, - actionTypeRegistry, - isESOCanEncrypt, - inMemoryConnectors, -}: CreateExecuteFunctionOptions): ExecutionEnqueuer { - return async function execute( - unsecuredSavedObjectsClient: SavedObjectsClientContract, - { - id, - params, - spaceId, - consumer, - source, - apiKey, - executionId, - relatedSavedObjects, - }: ExecuteOptions - ) { - if (!isESOCanEncrypt) { - throw new Error( - `Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` - ); - } - - const { action, isInMemory } = await getAction( - unsecuredSavedObjectsClient, - inMemoryConnectors, - id - ); - validateCanActionBeUsed(action); - - const { actionTypeId } = action; - if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - } - - // Get saved object references from action ID and relatedSavedObjects - const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( - id, - isInMemory, - relatedSavedObjects - ); - const executionSourceReference = executionSourceAsSavedObjectReferences(source); - - const taskReferences = []; - if (executionSourceReference.references) { - taskReferences.push(...executionSourceReference.references); - } - if (references) { - taskReferences.push(...references); - } - - const actionTaskParamsRecord = await unsecuredSavedObjectsClient.create( - ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, - { - actionId: id, - params, - apiKey, - executionId, - consumer, - relatedSavedObjects: relatedSavedObjectWithRefs, - ...(source ? { source: source.type } : {}), - }, - { - references: taskReferences, - } - ); - - await taskManager.schedule({ - taskType: `actions:${action.actionTypeId}`, - params: { - spaceId, - actionTaskParamsId: actionTaskParamsRecord.id, - }, - state: {}, - scope: ['actions'], - }); - }; -} - export function createBulkExecutionEnqueuerFunction({ taskManager, actionTypeRegistry, diff --git a/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts b/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts index b44b109e9265f..09d3ae3b532d9 100644 --- a/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts +++ b/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts @@ -14,6 +14,7 @@ export const searchConnectorsSo = async ({ }: SearchConnectorsSoParams) => { return scopedClusterClient.asInternalUser.search({ index: kibanaIndices, + ignore_unavailable: true, body: { aggs, size: 0, diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 14ae64391177f..70a2cfd9f8e85 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -12,7 +12,7 @@ import { } from '@kbn/core/server/mocks'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { Logger } from '@kbn/core/server'; -import { actionsClientMock } from './actions_client.mock'; +import { actionsClientMock } from './actions_client/actions_client.mock'; import { PluginSetupContract, PluginStartContract, renderActionParameterTemplates } from './plugin'; import { Services } from './types'; import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; @@ -31,6 +31,7 @@ const createSetupMock = () => { getCaseConnectorClass: jest.fn(), getActionsHealth: jest.fn(), getActionsConfigurationUtilities: jest.fn(), + setEnabledConnectorTypes: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 66d75da5fd7cf..d3bc3be1a9deb 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -348,6 +348,163 @@ describe('Actions Plugin', () => { expect(pluginSetup.isPreconfiguredConnector('anotherConnectorId')).toEqual(false); }); }); + + describe('setEnabledConnectorTypes (works only on serverless)', () => { + function setup(config: ActionsConfig) { + context = coreMock.createPluginInitializerContext(config); + plugin = new ActionsPlugin(context); + coreSetup = coreMock.createSetup(); + pluginsSetup = { + taskManager: taskManagerMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(), + licensing: licensingMock.createSetup(), + eventLog: eventLogMock.createSetup(), + usageCollection: usageCollectionPluginMock.createSetupContract(), + features: featuresPluginMock.createSetup(), + serverless: {}, + }; + } + + it('should set connector type enabled', async () => { + setup(getConfig()); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + const coreStart = coreMock.createStart(); + const pluginsStart = { + licensing: licensingMock.createStart(), + taskManager: taskManagerMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + eventLog: eventLogMock.createStart(), + }; + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginSetup.registerType({ + id: '.server-log', + name: 'Server log', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + pluginSetup.registerType({ + id: '.slack', + name: 'Slack', + minimumLicenseRequired: 'gold', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + pluginSetup.setEnabledConnectorTypes(['.server-log']); + expect(pluginStart.isActionTypeEnabled('.server-log')).toBeTruthy(); + expect(pluginStart.isActionTypeEnabled('.slack')).toBeFalsy(); + }); + + it('should set all the connector types enabled when null or ["*"] passed', async () => { + setup(getConfig()); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + const coreStart = coreMock.createStart(); + const pluginsStart = { + licensing: licensingMock.createStart(), + taskManager: taskManagerMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + eventLog: eventLogMock.createStart(), + }; + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginSetup.registerType({ + id: '.server-log', + name: 'Server log', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + pluginSetup.registerType({ + id: '.index', + name: 'Index', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + pluginSetup.setEnabledConnectorTypes(['*']); + expect(pluginStart.isActionTypeEnabled('.server-log')).toBeTruthy(); + expect(pluginStart.isActionTypeEnabled('.index')).toBeTruthy(); + }); + + it('should set all the connector types disabled when [] passed', async () => { + setup(getConfig()); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + const coreStart = coreMock.createStart(); + const pluginsStart = { + licensing: licensingMock.createStart(), + taskManager: taskManagerMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + eventLog: eventLogMock.createStart(), + }; + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginSetup.registerType({ + id: '.server-log', + name: 'Server log', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + pluginSetup.registerType({ + id: '.index', + name: 'Index', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + pluginSetup.setEnabledConnectorTypes([]); + expect(pluginStart.isActionTypeEnabled('.server-log')).toBeFalsy(); + expect(pluginStart.isActionTypeEnabled('.index')).toBeFalsy(); + }); + + it('should throw if the enabledActionTypes is already set by the config', async () => { + setup({ ...getConfig(), enabledActionTypes: ['.email'] }); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + + expect(() => pluginSetup.setEnabledConnectorTypes(['.index'])).toThrow( + "Enabled connector types can be set only if they haven't already been set in the config" + ); + }); + }); }); describe('start()', () => { @@ -396,6 +553,38 @@ describe('Actions Plugin', () => { }; }); + it('should throw when there is an invalid connector type in enabledActionTypes', async () => { + const pluginSetup = await plugin.setup(coreSetup, { + ...pluginsSetup, + encryptedSavedObjects: { + ...pluginsSetup.encryptedSavedObjects, + canEncrypt: true, + }, + serverless: {}, + }); + + pluginSetup.registerType({ + id: '.server-log', + name: 'Server log', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + pluginSetup.setEnabledConnectorTypes(['.server-log', 'non-existing']); + + await expect(async () => + plugin.start(coreStart, { ...pluginsStart, serverless: {} }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Action type \\"non-existing\\" is not registered."` + ); + }); + describe('getActionsClientWithRequest()', () => { it('should not throw error when ESO plugin has encryption key', async () => { await plugin.setup(coreSetup, { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 122e3075ac0ac..415a9e36a1c01 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -40,12 +40,12 @@ import { } from '@kbn/event-log-plugin/server'; import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; -import { ActionsConfig, getValidatedConfig } from './config'; +import { ServerlessPluginSetup } from '@kbn/serverless/server'; +import { ActionsConfig, AllowedHosts, EnabledConnectorTypes, getValidatedConfig } from './config'; import { resolveCustomHosts } from './lib/custom_host_settings'; -import { ActionsClient } from './actions_client'; +import { ActionsClient } from './actions_client/actions_client'; import { ActionTypeRegistry } from './action_type_registry'; import { - createExecutionEnqueuerFunction, createEphemeralExecutionEnqueuerFunction, createBulkExecutionEnqueuerFunction, } from './create_execute_function'; @@ -101,10 +101,8 @@ import { createSubActionConnectorFramework } from './sub_action_framework'; import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework/types'; import { SubActionConnector } from './sub_action_framework/sub_action_connector'; import { CaseConnector } from './sub_action_framework/case'; -import { - type IUnsecuredActionsClient, - UnsecuredActionsClient, -} from './unsecured_actions_client/unsecured_actions_client'; +import type { IUnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client'; +import { UnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client'; import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function'; import { createSystemConnectors } from './create_system_actions'; @@ -131,6 +129,7 @@ export interface PluginSetupContract { getCaseConnectorClass: () => IServiceAbstract; getActionsHealth: () => { hasPermanentEncryptionKey: boolean }; getActionsConfigurationUtilities: () => ActionsConfigurationUtilities; + setEnabledConnectorTypes: (connectorTypes: EnabledConnectorTypes) => void; } export interface PluginStartContract { @@ -170,6 +169,7 @@ export interface ActionsPluginsSetup { features: FeaturesPluginSetup; spaces?: SpacesPluginSetup; monitoringCollection?: MonitoringCollectionSetup; + serverless?: ServerlessPluginSetup; } export interface ActionsPluginsStart { @@ -179,6 +179,7 @@ export interface ActionsPluginsStart { eventLog: IEventLogClientService; spaces?: SpacesPluginStart; security?: SecurityPluginStart; + serverless?: ServerlessPluginSetup; } const includedHiddenTypes = [ @@ -376,6 +377,20 @@ export class ActionsPlugin implements Plugin actionsConfigUtils, + setEnabledConnectorTypes: (connectorTypes) => { + if ( + !!plugins.serverless && + this.actionsConfig.enabledActionTypes.length === 1 && + this.actionsConfig.enabledActionTypes[0] === AllowedHosts.Any + ) { + this.actionsConfig.enabledActionTypes.pop(); + this.actionsConfig.enabledActionTypes.push(...connectorTypes); + } else { + throw new Error( + "Enabled connector types can be set only if they haven't already been set in the config" + ); + } + }, }; } @@ -443,12 +458,6 @@ export class ActionsPlugin implements Plugin { return this.actionTypeRegistry!.isActionTypeEnabled(id, options); @@ -677,12 +688,6 @@ export class ActionsPlugin implements Plugin { + if ( + !!plugins.serverless && + this.actionsConfig.enabledActionTypes.length > 0 && + this.actionsConfig.enabledActionTypes[0] !== AllowedHosts.Any + ) { + this.actionsConfig.enabledActionTypes.forEach((connectorType) => { + // Throws error if action type doesn't exist + this.actionTypeRegistry?.get(connectorType); + }); + } + }; + public stop() { if (this.licenseState) { this.licenseState.clean(); diff --git a/x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts b/x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts index 06491cfeb8ef2..223a2d56c0843 100644 --- a/x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts +++ b/x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts @@ -10,7 +10,7 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../../../lib/license_state.mock'; import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments'; import { verifyAccessAndContext } from '../../verify_access_and_context'; -import { actionsClientMock } from '../../../actions_client.mock'; +import { actionsClientMock } from '../../../actions_client/actions_client.mock'; jest.mock('../../verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), diff --git a/x-pack/plugins/actions/server/routes/connector/list_types/index.ts b/x-pack/plugins/actions/server/routes/connector/list_types/index.ts new file mode 100644 index 0000000000000..668942c40ae90 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/list_types/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 { listTypesRoute } from './list_types'; diff --git a/x-pack/plugins/actions/server/routes/connector_types.test.ts b/x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts similarity index 91% rename from x-pack/plugins/actions/server/routes/connector_types.test.ts rename to x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts index 4c5a7089b75c4..91419ac562324 100644 --- a/x-pack/plugins/actions/server/routes/connector_types.test.ts +++ b/x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { connectorTypesRoute } from './connector_types'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; import { LicenseType } from '@kbn/licensing-plugin/server'; -import { actionsClientMock } from '../mocks'; -import { verifyAccessAndContext } from './verify_access_and_context'; +import { licenseStateMock } from '../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments'; +import { listTypesRoute } from './list_types'; +import { verifyAccessAndContext } from '../../verify_access_and_context'; +import { actionsClientMock } from '../../../mocks'; -jest.mock('./verify_access_and_context', () => ({ +jest.mock('../../verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), })); @@ -22,12 +22,12 @@ beforeEach(() => { (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); -describe('connectorTypesRoute', () => { +describe('listTypesRoute', () => { it('lists action types with proper parameters', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - connectorTypesRoute(router, licenseState); + listTypesRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; @@ -89,7 +89,7 @@ describe('connectorTypesRoute', () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - connectorTypesRoute(router, licenseState); + listTypesRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; @@ -168,7 +168,7 @@ describe('connectorTypesRoute', () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - connectorTypesRoute(router, licenseState); + listTypesRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; @@ -211,7 +211,7 @@ describe('connectorTypesRoute', () => { throw new Error('OMG'); }); - connectorTypesRoute(router, licenseState); + listTypesRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; diff --git a/x-pack/plugins/actions/server/routes/connector/list_types/list_types.ts b/x-pack/plugins/actions/server/routes/connector/list_types/list_types.ts new file mode 100644 index 0000000000000..078c51743c4d9 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/list_types/list_types.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '@kbn/core/server'; +import { ConnectorTypesResponseV1 } from '../../../../common/routes/connector/response'; +import { + connectorTypesQuerySchemaV1, + ConnectorTypesRequestQueryV1, +} from '../../../../common/routes/connector/apis/connector_types'; +import { transformListTypesResponseV1 } from './transforms'; +import { ActionsRequestHandlerContext } from '../../../types'; +import { BASE_ACTION_API_PATH } from '../../../../common'; +import { ILicenseState } from '../../../lib'; +import { verifyAccessAndContext } from '../../verify_access_and_context'; + +export const listTypesRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${BASE_ACTION_API_PATH}/connector_types`, + validate: { + query: connectorTypesQuerySchemaV1, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = (await context.actions).getActionsClient(); + + // Assert versioned inputs + const query: ConnectorTypesRequestQueryV1 = req.query; + + const connectorTypes = await actionsClient.listTypes({ featureId: query?.feature_id }); + + const responseBody: ConnectorTypesResponseV1[] = + transformListTypesResponseV1(connectorTypes); + + return res.ok({ body: responseBody }); + }) + ) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/connector/list_types/transforms/index.ts b/x-pack/plugins/actions/server/routes/connector/list_types/transforms/index.ts new file mode 100644 index 0000000000000..35e5f1db443c2 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/list_types/transforms/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { transformListTypesResponse } from './transform_list_types_response/latest'; +export { transformListTypesResponse as transformListTypesResponseV1 } from './transform_list_types_response/v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/list_types/transforms/transform_list_types_response/latest.ts b/x-pack/plugins/actions/server/routes/connector/list_types/transforms/transform_list_types_response/latest.ts new file mode 100644 index 0000000000000..5fd887263233c --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/list_types/transforms/transform_list_types_response/latest.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 { transformListTypesResponse } from './v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/list_types/transforms/transform_list_types_response/v1.ts b/x-pack/plugins/actions/server/routes/connector/list_types/transforms/transform_list_types_response/v1.ts new file mode 100644 index 0000000000000..e32bec2f9e1a1 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/list_types/transforms/transform_list_types_response/v1.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorType } from '../../../../../application/connector/types'; +import { ConnectorTypesResponseV1 } from '../../../../../../common/routes/connector/response'; + +export const transformListTypesResponse = ( + results: ConnectorType[] +): ConnectorTypesResponseV1[] => { + return results.map( + ({ + id, + name, + enabled, + enabledInConfig, + enabledInLicense, + minimumLicenseRequired, + supportedFeatureIds, + isSystemActionType, + }) => ({ + id, + name, + enabled, + enabled_in_config: enabledInConfig, + enabled_in_license: enabledInLicense, + minimum_license_required: minimumLicenseRequired, + supported_feature_ids: supportedFeatureIds, + is_system_action_type: isSystemActionType, + }) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/connector_types.ts b/x-pack/plugins/actions/server/routes/connector_types.ts deleted file mode 100644 index d54b35a7a99df..0000000000000 --- a/x-pack/plugins/actions/server/routes/connector_types.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IRouter } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; -import { ILicenseState } from '../lib'; -import { ActionType, BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common'; -import { ActionsRequestHandlerContext } from '../types'; -import { verifyAccessAndContext } from './verify_access_and_context'; - -const querySchema = schema.object({ - feature_id: schema.maybe(schema.string()), -}); - -const rewriteBodyRes: RewriteResponseCase = (results) => { - return results.map( - ({ - enabledInConfig, - enabledInLicense, - minimumLicenseRequired, - supportedFeatureIds, - isSystemActionType, - ...res - }) => ({ - ...res, - enabled_in_config: enabledInConfig, - enabled_in_license: enabledInLicense, - minimum_license_required: minimumLicenseRequired, - supported_feature_ids: supportedFeatureIds, - is_system_action_type: isSystemActionType, - }) - ); -}; - -export const connectorTypesRoute = ( - router: IRouter, - licenseState: ILicenseState -) => { - router.get( - { - path: `${BASE_ACTION_API_PATH}/connector_types`, - validate: { - query: querySchema, - }, - }, - router.handleLegacyErrors( - verifyAccessAndContext(licenseState, async function (context, req, res) { - const actionsClient = (await context.actions).getActionsClient(); - return res.ok({ - body: rewriteBodyRes(await actionsClient.listTypes({ featureId: req.query?.feature_id })), - }); - }) - ) - ); -}; diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index 2161e68e3706b..dd1317f57cd27 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -9,9 +9,9 @@ import { createActionRoute, bodySchema } from './create'; import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { actionsClientMock } from '../actions_client.mock'; import { verifyAccessAndContext } from './verify_access_and_context'; import { omit } from 'lodash'; +import { actionsClientMock } from '../actions_client/actions_client.mock'; jest.mock('./verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 6bcab72fbc869..12960aeae47e6 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -10,7 +10,7 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; import { asHttpRequestExecutionSource } from '../lib'; -import { actionsClientMock } from '../actions_client.mock'; +import { actionsClientMock } from '../actions_client/actions_client.mock'; import { ActionTypeExecutorResult } from '../types'; import { verifyAccessAndContext } from './verify_access_and_context'; diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index 76dda602945c9..a52cea1d49f6d 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -9,7 +9,7 @@ import { getActionRoute } from './get'; import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { actionsClientMock } from '../actions_client.mock'; +import { actionsClientMock } from '../actions_client/actions_client.mock'; import { verifyAccessAndContext } from './verify_access_and_context'; jest.mock('./verify_access_and_context', () => ({ diff --git a/x-pack/plugins/actions/server/routes/get_global_execution_kpi.test.ts b/x-pack/plugins/actions/server/routes/get_global_execution_kpi.test.ts index 838a1bf69789f..066d558bcfd59 100644 --- a/x-pack/plugins/actions/server/routes/get_global_execution_kpi.test.ts +++ b/x-pack/plugins/actions/server/routes/get_global_execution_kpi.test.ts @@ -8,7 +8,7 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { actionsClientMock } from '../actions_client.mock'; +import { actionsClientMock } from '../actions_client/actions_client.mock'; import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi'; import { verifyAccessAndContext } from './verify_access_and_context'; diff --git a/x-pack/plugins/actions/server/routes/get_global_execution_logs.test.ts b/x-pack/plugins/actions/server/routes/get_global_execution_logs.test.ts index 1c309e14dae09..4654885a49bcb 100644 --- a/x-pack/plugins/actions/server/routes/get_global_execution_logs.test.ts +++ b/x-pack/plugins/actions/server/routes/get_global_execution_logs.test.ts @@ -9,7 +9,7 @@ import { getGlobalExecutionLogRoute } from './get_global_execution_logs'; import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { actionsClientMock } from '../actions_client.mock'; +import { actionsClientMock } from '../actions_client/actions_client.mock'; import { IExecutionLogResult } from '../../common'; import { verifyAccessAndContext } from './verify_access_and_context'; diff --git a/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts index ff13b021a209f..ae06068273ca3 100644 --- a/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts +++ b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts @@ -11,7 +11,7 @@ import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; import { verifyAccessAndContext } from './verify_access_and_context'; import { actionsConfigMock } from '../actions_config.mock'; -import { actionsClientMock } from '../actions_client.mock'; +import { actionsClientMock } from '../actions_client/actions_client.mock'; jest.mock('./verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index d46ce9dd5cc51..e5dce0ac3b6c7 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -8,13 +8,13 @@ import { IRouter } from '@kbn/core/server'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { getAllConnectorsRoute } from './connector/get_all'; +import { listTypesRoute } from './connector/list_types'; import { ILicenseState } from '../lib'; import { ActionsRequestHandlerContext } from '../types'; import { createActionRoute } from './create'; import { deleteActionRoute } from './delete'; import { executeActionRoute } from './execute'; import { getActionRoute } from './get'; -import { connectorTypesRoute } from './connector_types'; import { updateActionRoute } from './update'; import { getOAuthAccessToken } from './get_oauth_access_token'; import { defineLegacyRoutes } from './legacy'; @@ -39,7 +39,7 @@ export function defineRoutes(opts: RouteOptions) { getActionRoute(router, licenseState); getAllConnectorsRoute(router, licenseState); updateActionRoute(router, licenseState); - connectorTypesRoute(router, licenseState); + listTypesRoute(router, licenseState); executeActionRoute(router, licenseState); getGlobalExecutionLogRoute(router, licenseState); getGlobalExecutionKPIRoute(router, licenseState); diff --git a/x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts b/x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts index 07ac40b7e52e1..73906fa0a63e3 100644 --- a/x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts +++ b/x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts @@ -9,15 +9,16 @@ import { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; import { identity } from 'lodash'; import type { MethodKeysOf } from '@kbn/utility-types'; import { httpServerMock } from '@kbn/core/server/mocks'; -import { ActionType } from '../../../common'; -import { ActionsClientMock, actionsClientMock } from '../../actions_client.mock'; import { ActionsRequestHandlerContext } from '../../types'; +import { actionsClientMock } from '../../mocks'; +import { ActionsClientMock } from '../../actions_client/actions_client.mock'; +import { ConnectorType } from '../../application/connector/types'; export function mockHandlerArguments( { actionsClient = actionsClientMock.create(), listTypes: listTypesRes = [], - }: { actionsClient?: ActionsClientMock; listTypes?: ActionType[] }, + }: { actionsClient?: ActionsClientMock; listTypes?: ConnectorType[] }, request: unknown, response?: Array> ): [ActionsRequestHandlerContext, KibanaRequest, KibanaResponseFactory] { diff --git a/x-pack/plugins/actions/server/routes/legacy/create.test.ts b/x-pack/plugins/actions/server/routes/legacy/create.test.ts index 0d285244342a6..e711f73265f53 100644 --- a/x-pack/plugins/actions/server/routes/legacy/create.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/create.test.ts @@ -9,7 +9,7 @@ import { createActionRoute } from './create'; import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { actionsClientMock } from '../../actions_client.mock'; +import { actionsClientMock } from '../../actions_client/actions_client.mock'; import { verifyAccessAndContext } from '../verify_access_and_context'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts index 1f9e7bb7566da..53e7d038dfea5 100644 --- a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts @@ -10,7 +10,7 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { verifyApiAccess, ActionTypeDisabledError, asHttpRequestExecutionSource } from '../../lib'; -import { actionsClientMock } from '../../actions_client.mock'; +import { actionsClientMock } from '../../actions_client/actions_client.mock'; import { ActionTypeExecutorResult } from '../../types'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; diff --git a/x-pack/plugins/actions/server/routes/legacy/get.test.ts b/x-pack/plugins/actions/server/routes/legacy/get.test.ts index be5cc1c819e3a..6a19400da0fb5 100644 --- a/x-pack/plugins/actions/server/routes/legacy/get.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/get.test.ts @@ -10,7 +10,7 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../../lib/license_state.mock'; import { verifyApiAccess } from '../../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { actionsClientMock } from '../../actions_client.mock'; +import { actionsClientMock } from '../../actions_client/actions_client.mock'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; diff --git a/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts b/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts index f7cdb56082a10..e999c769f3dbb 100644 --- a/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts @@ -10,7 +10,7 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../../lib/license_state.mock'; import { verifyApiAccess } from '../../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { actionsClientMock } from '../../actions_client.mock'; +import { actionsClientMock } from '../../actions_client/actions_client.mock'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; diff --git a/x-pack/plugins/actions/server/routes/legacy/update.test.ts b/x-pack/plugins/actions/server/routes/legacy/update.test.ts index 9158b4165a660..304b504636c91 100644 --- a/x-pack/plugins/actions/server/routes/legacy/update.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/update.test.ts @@ -10,7 +10,7 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { actionsClientMock } from '../../actions_client.mock'; +import { actionsClientMock } from '../../actions_client/actions_client.mock'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index ac9f3dc45f660..aef179264ac48 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -9,7 +9,7 @@ import { bodySchema, updateActionRoute } from './update'; import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { actionsClientMock } from '../actions_client.mock'; +import { actionsClientMock } from '../actions_client/actions_client.mock'; import { verifyAccessAndContext } from './verify_access_and_context'; jest.mock('./verify_access_and_context', () => ({ diff --git a/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts b/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts index 4b07216172473..e079634fbfeff 100644 --- a/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts +++ b/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts @@ -8,7 +8,7 @@ import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { actionsClientMock } from '../actions_client.mock'; +import { actionsClientMock } from '../actions_client/actions_client.mock'; import { verifyAccessAndContext } from './verify_access_and_context'; jest.mock('../lib/verify_api_access', () => ({ diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index 0f4d2faf03e1a..3beeddef429b2 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -43,6 +43,7 @@ "@kbn/core-saved-objects-api-server-mocks", "@kbn/core-elasticsearch-server-mocks", "@kbn/core-logging-server-mocks", + "@kbn/serverless" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/alerting/kibana.jsonc b/x-pack/plugins/alerting/kibana.jsonc index 86dc1a393085e..f37b7c7d72676 100644 --- a/x-pack/plugins/alerting/kibana.jsonc +++ b/x-pack/plugins/alerting/kibana.jsonc @@ -30,7 +30,8 @@ "usageCollection", "security", "monitoringCollection", - "spaces" + "spaces", + "serverless", ], "extraPublicDirs": [ "common", diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts index 24503e4951e56..78b2e41431c22 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts @@ -31,6 +31,7 @@ import { getParamsByTimeQuery, mockAAD, } from './alerts_client_fixtures'; +import { getDataStreamAdapter } from '../alerts_service/lib/data_stream_adapter'; const date = '2023-03-28T22:27:28.159Z'; const maxAlerts = 1000; @@ -82,404 +83,526 @@ const mockSetContext = jest.fn(); describe('Alerts Client', () => { let alertsClientParams: AlertsClientParams; let processAndLogAlertsOpts: ProcessAndLogAlertsOpts; + beforeAll(() => { jest.useFakeTimers(); jest.setSystemTime(new Date(date)); }); - beforeEach(() => { - jest.clearAllMocks(); - logger = loggingSystemMock.createLogger(); - alertsClientParams = { - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - kibanaVersion: '8.9.0', - }; - processAndLogAlertsOpts = { - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }; - }); - afterAll(() => { jest.useRealTimers(); }); - describe('initializeExecution()', () => { - test('should initialize LegacyAlertsClient', async () => { - mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ - active: {}, - recovered: {}, - })); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - - const alertsClient = new AlertsClient(alertsClientParams); - - const opts = { - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }; - await alertsClient.initializeExecution(opts); - expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); - - // no alerts to query for - expect(clusterClient.search).not.toHaveBeenCalled(); - - spy.mockRestore(); - }); + for (const useDataStreamForAlerts of [false, true]) { + const label = useDataStreamForAlerts ? 'data streams' : 'aliases'; - test('should skip track alerts ruleType shouldWrite is false', async () => { - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - - const alertsClient = new AlertsClient({ - ...alertsClientParams, - ruleType: { - ...alertsClientParams.ruleType, - alerts: { - context: 'test', - mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, - shouldWrite: false, - }, - }, - }); + describe(`using ${label} for alert indices`, () => { + beforeEach(() => { + jest.clearAllMocks(); + logger = loggingSystemMock.createLogger(); + alertsClientParams = { + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + ruleType, + namespace: 'default', + rule: alertRuleData, + kibanaVersion: '8.9.0', + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts }), + }; + processAndLogAlertsOpts = { + eventLogger: alertingEventLogger, + ruleRunMetricsStore, + shouldLogAlerts: false, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + notifyWhen: RuleNotifyWhen.CHANGE, + maintenanceWindowIds: [], + }; + }); + + describe('initializeExecution()', () => { + test('should initialize LegacyAlertsClient', async () => { + mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ + active: {}, + recovered: {}, + })); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + + const alertsClient = new AlertsClient(alertsClientParams); + + const opts = { + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }; + await alertsClient.initializeExecution(opts); + expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); + + // no alerts to query for + expect(clusterClient.search).not.toHaveBeenCalled(); + + spy.mockRestore(); + }); - const opts = { - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }; - await alertsClient.initializeExecution(opts); - expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); - expect(mockLegacyAlertsClient.getTrackedAlerts).not.toHaveBeenCalled(); - spy.mockRestore(); - }); + test('should skip track alerts ruleType shouldWrite is false', async () => { + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); - test('should query for alert UUIDs if they exist', async () => { - mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ - active: { - '1': new Alert('1', { - state: { foo: true }, - meta: { - flapping: false, - flappingHistory: [true, false], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }), - '2': new Alert('2', { - state: { foo: false }, - meta: { - flapping: false, - flappingHistory: [true, false, false], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'def', - }, - }), - }, - recovered: { - '3': new Alert('3', { - state: { foo: false }, - meta: { - flapping: false, - flappingHistory: [true, false, false], - uuid: 'xyz', - }, - }), - }, - })); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - - const alertsClient = new AlertsClient(alertsClientParams); - - const opts = { - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }; - await alertsClient.initializeExecution(opts); - expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); - - expect(clusterClient.search).toHaveBeenCalledWith({ - body: { - query: { - bool: { - filter: [ - { term: { 'kibana.alert.rule.uuid': '1' } }, - { terms: { 'kibana.alert.uuid': ['abc', 'def', 'xyz'] } }, - ], + const alertsClient = new AlertsClient({ + ...alertsClientParams, + ruleType: { + ...alertsClientParams.ruleType, + alerts: { + context: 'test', + mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, + shouldWrite: false, + }, }, - }, - size: 3, - }, - index: '.internal.alerts-test.alerts-default-*', - }); + }); - spy.mockRestore(); - }); + const opts = { + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }; + await alertsClient.initializeExecution(opts); + expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); + expect(mockLegacyAlertsClient.getTrackedAlerts).not.toHaveBeenCalled(); + spy.mockRestore(); + }); - test('should split queries into chunks when there are greater than 10,000 alert UUIDs', async () => { - mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ - active: range(15000).reduce((acc: Record>, value: number) => { - const id: string = `${value}`; - acc[id] = new Alert(id, { - state: { foo: true }, - meta: { - flapping: false, - flappingHistory: [true, false], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: id, + test('should query for alert UUIDs if they exist', async () => { + mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ + active: { + '1': new Alert('1', { + state: { foo: true }, + meta: { + flapping: false, + flappingHistory: [true, false], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, + }), + '2': new Alert('2', { + state: { foo: false }, + meta: { + flapping: false, + flappingHistory: [true, false, false], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'def', + }, + }), + }, + recovered: { + '3': new Alert('3', { + state: { foo: false }, + meta: { + flapping: false, + flappingHistory: [true, false, false], + uuid: 'xyz', + }, + }), + }, + })); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + + const alertsClient = new AlertsClient(alertsClientParams); + + const opts = { + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }; + await alertsClient.initializeExecution(opts); + expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); + + expect(clusterClient.search).toHaveBeenCalledWith({ + body: { + query: { + bool: { + filter: [ + { term: { 'kibana.alert.rule.uuid': '1' } }, + { terms: { 'kibana.alert.uuid': ['abc', 'def', 'xyz'] } }, + ], + }, + }, + seq_no_primary_term: true, + size: 3, }, + index: useDataStreamForAlerts + ? '.alerts-test.alerts-default' + : '.internal.alerts-test.alerts-default-*', + ignore_unavailable: true, }); - return acc; - }, {}), - recovered: {}, - })); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - - const alertsClient = new AlertsClient(alertsClientParams); - - const opts = { - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }; - await alertsClient.initializeExecution(opts); - expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); - - expect(clusterClient.search).toHaveBeenCalledTimes(2); - - spy.mockRestore(); - }); - test('should log but not throw if query returns error', async () => { - clusterClient.search.mockImplementation(() => { - throw new Error('search failed!'); - }); - mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ - active: { - '1': new Alert('1', { - state: { foo: true }, - meta: { - flapping: false, - flappingHistory: [true, false], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }), - }, - recovered: {}, - })); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - - const alertsClient = new AlertsClient(alertsClientParams); - - const opts = { - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }; - await alertsClient.initializeExecution(opts); - expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); - - expect(clusterClient.search).toHaveBeenCalledWith({ - body: { - query: { - bool: { - filter: [ - { term: { 'kibana.alert.rule.uuid': '1' } }, - { terms: { 'kibana.alert.uuid': ['abc'] } }, - ], - }, - }, - size: 1, - }, - index: '.internal.alerts-test.alerts-default-*', - }); + spy.mockRestore(); + }); - expect(logger.error).toHaveBeenCalledWith( - `Error searching for tracked alerts by UUID - search failed!` - ); + test('should split queries into chunks when there are greater than 10,000 alert UUIDs', async () => { + mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ + active: range(15000).reduce((acc: Record>, value: number) => { + const id: string = `${value}`; + acc[id] = new Alert(id, { + state: { foo: true }, + meta: { + flapping: false, + flappingHistory: [true, false], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: id, + }, + }); + return acc; + }, {}), + recovered: {}, + })); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + + const alertsClient = new AlertsClient(alertsClientParams); + + const opts = { + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }; + await alertsClient.initializeExecution(opts); + expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); + + expect(clusterClient.search).toHaveBeenCalledTimes(2); + + spy.mockRestore(); + }); - spy.mockRestore(); - }); - }); + test('should log but not throw if query returns error', async () => { + clusterClient.search.mockImplementation(() => { + throw new Error('search failed!'); + }); + mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ + active: { + '1': new Alert('1', { + state: { foo: true }, + meta: { + flapping: false, + flappingHistory: [true, false], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, + }), + }, + recovered: {}, + })); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + + const alertsClient = new AlertsClient(alertsClientParams); + + const opts = { + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }; + await alertsClient.initializeExecution(opts); + expect(mockLegacyAlertsClient.initializeExecution).toHaveBeenCalledWith(opts); + + expect(clusterClient.search).toHaveBeenCalledWith({ + body: { + query: { + bool: { + filter: [ + { term: { 'kibana.alert.rule.uuid': '1' } }, + { terms: { 'kibana.alert.uuid': ['abc'] } }, + ], + }, + }, + size: 1, + seq_no_primary_term: true, + }, + index: useDataStreamForAlerts + ? '.alerts-test.alerts-default' + : '.internal.alerts-test.alerts-default-*', + ignore_unavailable: true, + }); - describe('persistAlerts()', () => { - test('should index new alerts', async () => { - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); + expect(logger.error).toHaveBeenCalledWith( + `Error searching for tracked alerts by UUID - search failed!` + ); - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, + spy.mockRestore(); + }); }); - // Report 2 new alerts - const alertExecutorService = alertsClient.factory(); - alertExecutorService.create('1').scheduleActions('default'); - alertExecutorService.create('2').scheduleActions('default'); + describe('persistAlerts()', () => { + test('should index new alerts', async () => { + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - await alertsClient.persistAlerts(); + // Report 2 new alerts + const alertExecutorService = alertsClient.factory(); + alertExecutorService.create('1').scheduleActions('default'); + alertExecutorService.create('2').scheduleActions('default'); - const { alertsToReturn } = alertsClient.getAlertsToSerialize(); - const uuid1 = alertsToReturn['1'].meta?.uuid; - const uuid2 = alertsToReturn['2'].meta?.uuid; + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { index: { _id: uuid1 } }, - // new alert doc - { - '@timestamp': date, - event: { - action: 'open', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true], - instance: { - id: '1', + await alertsClient.persistAlerts(); + + const { alertsToReturn } = alertsClient.getAlertsToSerialize(); + const uuid1 = alertsToReturn['1'].meta?.uuid; + const uuid2 = alertsToReturn['2'].meta?.uuid; + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + create: { _id: uuid1, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, + }, + // new alert doc + { + '@timestamp': date, + event: { + action: 'open', + kind: 'signal', }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + maintenance_window_ids: [], + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: date, + status: 'active', + time_range: { + gte: date, + }, + uuid: uuid1, + workflow_status: 'open', }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: date, - status: 'active', - time_range: { - gte: date, + space_ids: ['default'], + version: '8.9.0', }, - uuid: uuid1, - workflow_status: 'open', + tags: ['rule-', '-tags'], }, - space_ids: ['default'], - version: '8.9.0', - }, - tags: ['rule-', '-tags'], - }, - { index: { _id: uuid2 } }, - // new alert doc - { - '@timestamp': date, - event: { - action: 'open', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true], - instance: { - id: '2', + { + create: { _id: uuid2, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, + }, + // new alert doc + { + '@timestamp': date, + event: { + action: 'open', + kind: 'signal', }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '2', + }, + maintenance_window_ids: [], + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: date, + status: 'active', + time_range: { + gte: date, + }, + uuid: uuid2, + workflow_status: 'open', }, - name: 'rule-name', - parameters: { - bar: true, + space_ids: ['default'], + version: '8.9.0', + }, + tags: ['rule-', '-tags'], + }, + ], + }); + }); + + test('should update ongoing alerts in existing index', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 1, + }, + hits: [ + { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + _seq_no: 41, + _primary_term: 665, + _source: { + '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abc', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', }, - start: date, - status: 'active', - time_range: { - gte: date, + ], + }, + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', }, - uuid: uuid2, - workflow_status: 'open', }, - space_ids: ['default'], - version: '8.9.0', }, - tags: ['rule-', '-tags'], - }, - ], - }); - }); + recoveredAlertsFromState: {}, + }); - test('should update ongoing alerts in existing index', async () => { - clusterClient.search.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, - hits: { - total: { - relation: 'eq', - value: 1, - }, - hits: [ - { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - _source: { - '@timestamp': '2023-03-28T12:27:28.159Z', + // Report 1 new alert and 1 active alert + const alertExecutorService = alertsClient.factory(); + alertExecutorService.create('1').scheduleActions('default'); + alertExecutorService.create('2').scheduleActions('default'); + + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + + await alertsClient.persistAlerts(); + + const { alertsToReturn } = alertsClient.getAlertsToSerialize(); + const uuid2 = alertsToReturn['2'].meta?.uuid; + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + index: { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + if_seq_no: 41, + if_primary_term: 665, + require_alias: false, + }, + }, + // ongoing alert doc + { + '@timestamp': date, event: { action: 'active', kind: 'signal', @@ -488,13 +611,14 @@ describe('Alerts Client', () => { alert: { action_group: 'default', duration: { - us: '0', + us: '36000000000000', }, flapping: false, - flapping_history: [true], + flapping_history: [true, false], instance: { id: '1', }, + maintenance_window_ids: [], rule: { category: 'My test rule', consumer: 'bar', @@ -520,197 +644,195 @@ describe('Alerts Client', () => { workflow_status: 'open', }, space_ids: ['default'], - version: '8.8.0', + version: '8.9.0', }, tags: ['rule-', '-tags'], }, - }, - ], - }, - }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }, - }, - recoveredAlertsFromState: {}, - }); - - // Report 1 new alert and 1 active alert - const alertExecutorService = alertsClient.factory(); - alertExecutorService.create('1').scheduleActions('default'); - alertExecutorService.create('2').scheduleActions('default'); - - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - - await alertsClient.persistAlerts(); - - const { alertsToReturn } = alertsClient.getAlertsToSerialize(); - const uuid2 = alertsToReturn['2'].meta?.uuid; - - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { - index: { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - require_alias: false, - }, - }, - // ongoing alert doc - { - '@timestamp': date, - event: { - action: 'active', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '36000000000000', - }, - flapping: false, - flapping_history: [true, false], - instance: { - id: '1', + { + create: { _id: uuid2, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, + }, + // new alert doc + { + '@timestamp': date, + event: { + action: 'open', + kind: 'signal', }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '2', + }, + maintenance_window_ids: [], + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: date, + status: 'active', + time_range: { + gte: date, + }, + uuid: uuid2, + workflow_status: 'open', }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: '2023-03-28T12:27:28.159Z', - status: 'active', - time_range: { - gte: '2023-03-28T12:27:28.159Z', + space_ids: ['default'], + version: '8.9.0', }, - uuid: 'abc', - workflow_status: 'open', + tags: ['rule-', '-tags'], }, - space_ids: ['default'], - version: '8.9.0', - }, - tags: ['rule-', '-tags'], - }, - { index: { _id: uuid2 } }, - // new alert doc - { - '@timestamp': date, - event: { - action: 'open', - kind: 'signal', + ], + }); + }); + + test('should not update ongoing alerts in existing index when they are not in the processed alerts', async () => { + const activeAlert = { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true, false], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true], - instance: { - id: '2', - }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, + }; + + const activeAlertObj = new Alert<{}, {}, 'default'>('1', activeAlert); + activeAlertObj.scheduleActions('default', {}); + const spy = jest + .spyOn(LegacyAlertsClient.prototype, 'getProcessedAlerts') + .mockReturnValueOnce({ + '1': activeAlertObj, // return only the first (tracked) alert + }) + .mockReturnValueOnce({}); + + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 1, + }, + hits: [ + { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + _source: { + '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abc', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: date, - status: 'active', - time_range: { - gte: date, }, - uuid: uuid2, - workflow_status: 'open', - }, - space_ids: ['default'], - version: '8.9.0', + ], }, - tags: ['rule-', '-tags'], - }, - ], - }); - }); + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': activeAlert, + }, + recoveredAlertsFromState: {}, + }); - test('should not update ongoing alerts in existing index when they are not in the processed alerts', async () => { - const activeAlert = { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true, false], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date() }, - uuid: 'abc', - }, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const activeAlertObj = new Alert<{}, {}, 'default'>('1', activeAlert as any); - activeAlertObj.scheduleActions('default', {}); - const spy = jest - .spyOn(LegacyAlertsClient.prototype, 'getProcessedAlerts') - .mockReturnValueOnce({ - '1': activeAlertObj, // return only the first (tracked) alert - }) - .mockReturnValueOnce({}); - - clusterClient.search.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, - hits: { - total: { - relation: 'eq', - value: 1, - }, - hits: [ - { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - _source: { - '@timestamp': '2023-03-28T12:27:28.159Z', + // Report 1 new alert and 1 active alert + const alertExecutorService = alertsClient.factory(); + alertExecutorService.create('1').scheduleActions('default'); + alertExecutorService.create('2').scheduleActions('default'); // will be skipped as getProcessedAlerts does not return it + + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + + await alertsClient.persistAlerts(); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenNthCalledWith(1, 'active'); + expect(spy).toHaveBeenNthCalledWith(2, 'recovered'); + + expect(logger.error).toHaveBeenCalledWith( + "Error writing alert(2) to .alerts-test.alerts-default - alert(2) doesn't exist in active alerts" + ); + spy.mockRestore(); + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + create: { + _id: 'abc', + ...(useDataStreamForAlerts ? {} : { require_alias: true }), + }, + }, + // ongoing alert doc + { + '@timestamp': date, event: { action: 'active', kind: 'signal', @@ -722,10 +844,11 @@ describe('Alerts Client', () => { us: '0', }, flapping: false, - flapping_history: [true], + flapping_history: [true, false], instance: { id: '1', }, + maintenance_window_ids: [], rule: { category: 'My test rule', consumer: 'bar', @@ -751,140 +874,210 @@ describe('Alerts Client', () => { workflow_status: 'open', }, space_ids: ['default'], - version: '8.8.0', + version: '8.9.0', }, tags: ['rule-', '-tags'], }, - }, - ], - }, - }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - '1': activeAlert as any, - }, - recoveredAlertsFromState: {}, - }); + ], + }); + }); - // Report 1 new alert and 1 active alert - const alertExecutorService = alertsClient.factory(); - alertExecutorService.create('1').scheduleActions('default'); - alertExecutorService.create('2').scheduleActions('default'); // will be skipped as getProcessedAlerts does not return it - - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - - await alertsClient.persistAlerts(); - - expect(spy).toHaveBeenCalledTimes(2); - expect(spy).toHaveBeenNthCalledWith(1, 'active'); - expect(spy).toHaveBeenNthCalledWith(2, 'recovered'); - - expect(logger.error).toHaveBeenCalledWith( - "Error writing alert(2) to .alerts-test.alerts-default - alert(2) doesn't exist in active alerts" - ); - spy.mockRestore(); - - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { - index: { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - require_alias: false, - }, - }, - // ongoing alert doc - { - '@timestamp': date, - event: { - action: 'active', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true, false], - instance: { - id: '1', - }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + test('should recover recovered alerts in existing index', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 1, + }, + hits: [ + { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + _seq_no: 41, + _primary_term: 665, + _source: { + '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'open', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abc', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], }, - name: 'rule-name', - parameters: { - bar: true, + }, + { + _id: 'def', + _index: '.internal.alerts-test.alerts-default-000002', + _seq_no: 42, + _primary_term: 666, + _source: { + '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '36000000000000', + }, + flapping: false, + flapping_history: [true, false], + instance: { + id: '2', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T02:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T02:27:28.159Z', + }, + uuid: 'def', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', }, - start: '2023-03-28T12:27:28.159Z', - status: 'active', - time_range: { - gte: '2023-03-28T12:27:28.159Z', + ], + }, + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, + }, + '2': { + state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, + meta: { + flapping: false, + flappingHistory: [true, false], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'def', }, - uuid: 'abc', - workflow_status: 'open', }, - space_ids: ['default'], - version: '8.9.0', }, - tags: ['rule-', '-tags'], - }, - ], - }); - }); + recoveredAlertsFromState: {}, + }); - test('should recover recovered alerts in existing index', async () => { - clusterClient.search.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, - hits: { - total: { - relation: 'eq', - value: 1, - }, - hits: [ - { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - _source: { - '@timestamp': '2023-03-28T12:27:28.159Z', + // Report 1 new alert and 1 active alert, recover 1 alert + const alertExecutorService = alertsClient.factory(); + alertExecutorService.create('2').scheduleActions('default'); + alertExecutorService.create('3').scheduleActions('default'); + + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + + await alertsClient.persistAlerts(); + + const { alertsToReturn } = alertsClient.getAlertsToSerialize(); + const uuid3 = alertsToReturn['3'].meta?.uuid; + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + index: { + _id: 'def', + _index: '.internal.alerts-test.alerts-default-000002', + if_seq_no: 42, + if_primary_term: 666, + require_alias: false, + }, + }, + // ongoing alert doc + { + '@timestamp': date, event: { - action: 'open', + action: 'active', kind: 'signal', }, kibana: { alert: { action_group: 'default', duration: { - us: '0', + us: '72000000000000', }, flapping: false, - flapping_history: [true], + flapping_history: [true, false, false], instance: { - id: '1', + id: '2', }, + maintenance_window_ids: [], rule: { category: 'My test rule', consumer: 'bar', @@ -901,40 +1094,41 @@ describe('Alerts Client', () => { tags: ['rule-', '-tags'], uuid: '1', }, - start: '2023-03-28T12:27:28.159Z', + start: '2023-03-28T02:27:28.159Z', status: 'active', time_range: { - gte: '2023-03-28T12:27:28.159Z', + gte: '2023-03-28T02:27:28.159Z', }, - uuid: 'abc', + uuid: 'def', workflow_status: 'open', }, space_ids: ['default'], - version: '8.8.0', + version: '8.9.0', }, tags: ['rule-', '-tags'], }, - }, - { - _id: 'def', - _index: '.internal.alerts-test.alerts-default-000002', - _source: { - '@timestamp': '2023-03-28T12:27:28.159Z', + { + create: { _id: uuid3, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, + }, + // new alert doc + { + '@timestamp': date, event: { - action: 'active', + action: 'open', kind: 'signal', }, kibana: { alert: { action_group: 'default', duration: { - us: '36000000000000', + us: '0', }, flapping: false, - flapping_history: [true, false], + flapping_history: [true], instance: { - id: '2', + id: '3', }, + maintenance_window_ids: [], rule: { category: 'My test rule', consumer: 'bar', @@ -951,1219 +1145,1218 @@ describe('Alerts Client', () => { tags: ['rule-', '-tags'], uuid: '1', }, - start: '2023-03-28T02:27:28.159Z', + start: date, status: 'active', time_range: { - gte: '2023-03-28T02:27:28.159Z', + gte: date, }, - uuid: 'def', + uuid: uuid3, workflow_status: 'open', }, space_ids: ['default'], - version: '8.8.0', + version: '8.9.0', }, tags: ['rule-', '-tags'], }, - }, - ], - }, - }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }, - '2': { - state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, - meta: { - flapping: false, - flappingHistory: [true, false], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'def', - }, - }, - }, - recoveredAlertsFromState: {}, - }); - - // Report 1 new alert and 1 active alert, recover 1 alert - const alertExecutorService = alertsClient.factory(); - alertExecutorService.create('2').scheduleActions('default'); - alertExecutorService.create('3').scheduleActions('default'); - - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - - await alertsClient.persistAlerts(); - - const { alertsToReturn } = alertsClient.getAlertsToSerialize(); - const uuid3 = alertsToReturn['3'].meta?.uuid; - - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { - index: { - _id: 'def', - _index: '.internal.alerts-test.alerts-default-000002', - require_alias: false, - }, - }, - // ongoing alert doc - { - '@timestamp': date, - event: { - action: 'active', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '72000000000000', - }, - flapping: false, - flapping_history: [true, false, false], - instance: { - id: '2', - }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, - }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: '2023-03-28T02:27:28.159Z', - status: 'active', - time_range: { - gte: '2023-03-28T02:27:28.159Z', - }, - uuid: 'def', - workflow_status: 'open', - }, - space_ids: ['default'], - version: '8.9.0', - }, - tags: ['rule-', '-tags'], - }, - { index: { _id: uuid3 } }, - // new alert doc - { - '@timestamp': date, - event: { - action: 'open', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true], - instance: { - id: '3', - }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, - }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: date, - status: 'active', - time_range: { - gte: date, + { + index: { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + if_seq_no: 41, + if_primary_term: 665, + require_alias: false, }, - uuid: uuid3, - workflow_status: 'open', }, - space_ids: ['default'], - version: '8.9.0', - }, - tags: ['rule-', '-tags'], - }, - { - index: { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - require_alias: false, - }, - }, - // recovered alert doc - { - '@timestamp': date, - event: { - action: 'close', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'recovered', - duration: { - us: '36000000000000', - }, - end: date, - flapping: false, - flapping_history: [true, true], - instance: { - id: '1', + // recovered alert doc + { + '@timestamp': date, + event: { + action: 'close', + kind: 'signal', }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, + kibana: { + alert: { + action_group: 'recovered', + duration: { + us: '36000000000000', + }, + end: date, + flapping: false, + flapping_history: [true, true], + instance: { + id: '1', + }, + maintenance_window_ids: [], + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'recovered', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + lte: date, + }, + uuid: 'abc', + workflow_status: 'open', }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: '2023-03-28T12:27:28.159Z', - status: 'recovered', - time_range: { - gte: '2023-03-28T12:27:28.159Z', - lte: date, + space_ids: ['default'], + version: '8.9.0', }, - uuid: 'abc', - workflow_status: 'open', + tags: ['rule-', '-tags'], }, - space_ids: ['default'], - version: '8.9.0', - }, - tags: ['rule-', '-tags'], - }, - ], - }); - }); - - test('should not try to index if no alerts', async () => { - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); + ], + }); + }); - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); + test('should not try to index if no alerts', async () => { + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - // Report no alerts + // Report no alerts - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - await alertsClient.persistAlerts(); + await alertsClient.persistAlerts(); - expect(clusterClient.bulk).not.toHaveBeenCalled(); - }); + expect(clusterClient.bulk).not.toHaveBeenCalled(); + }); - test('should log if bulk indexing fails for some alerts', async () => { - clusterClient.bulk.mockResponseOnce({ - took: 1, - errors: true, - items: [ - { - index: { - _index: '.internal.alerts-test.alerts-default-000001', - status: 400, - error: { - type: 'action_request_validation_exception', - reason: 'Validation Failed: 1: index is missing;2: type is missing;', + test('should log if bulk indexing fails for some alerts', async () => { + clusterClient.bulk.mockResponseOnce({ + took: 1, + errors: true, + items: [ + { + index: { + _index: '.internal.alerts-test.alerts-default-000001', + status: 400, + error: { + type: 'action_request_validation_exception', + reason: 'Validation Failed: 1: index is missing;2: type is missing;', + }, + }, }, - }, - }, - { - index: { - _index: '.internal.alerts-test.alerts-default-000002', - _id: '1', - _version: 1, - result: 'created', - _shards: { - total: 2, - successful: 1, - failed: 0, + { + index: { + _index: '.internal.alerts-test.alerts-default-000002', + _id: '1', + _version: 1, + result: 'created', + _shards: { + total: 2, + successful: 1, + failed: 0, + }, + status: 201, + _seq_no: 0, + _primary_term: 1, + }, }, - status: 201, - _seq_no: 0, - _primary_term: 1, - }, - }, - ], - }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); - - // Report 2 new alerts - const alertExecutorService = alertsClient.factory(); - alertExecutorService.create('1').scheduleActions('default'); - alertExecutorService.create('2').scheduleActions('default'); + ], + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + // Report 2 new alerts + const alertExecutorService = alertsClient.factory(); + alertExecutorService.create('1').scheduleActions('default'); + alertExecutorService.create('2').scheduleActions('default'); - await alertsClient.persistAlerts(); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - expect(clusterClient.bulk).toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith( - `Error writing 1 out of 2 alerts - [{\"type\":\"action_request_validation_exception\",\"reason\":\"Validation Failed: 1: index is missing;2: type is missing;\"}]` - ); - }); + await alertsClient.persistAlerts(); - test('should log and swallow error if bulk indexing throws error', async () => { - clusterClient.bulk.mockImplementation(() => { - throw new Error('fail'); - }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); + expect(clusterClient.bulk).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + `Error writing 1 out of 2 alerts - [{\"type\":\"action_request_validation_exception\",\"reason\":\"Validation Failed: 1: index is missing;2: type is missing;\"}]` + ); + }); - // Report 2 new alerts - const alertExecutorService = alertsClient.factory(); - alertExecutorService.create('1').scheduleActions('default'); - alertExecutorService.create('2').scheduleActions('default'); + test('should log and swallow error if bulk indexing throws error', async () => { + clusterClient.bulk.mockImplementation(() => { + throw new Error('fail'); + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + // Report 2 new alerts + const alertExecutorService = alertsClient.factory(); + alertExecutorService.create('1').scheduleActions('default'); + alertExecutorService.create('2').scheduleActions('default'); - await alertsClient.persistAlerts(); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - expect(clusterClient.bulk).toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith( - `Error writing 2 alerts to .alerts-test.alerts-default - fail` - ); - }); + await alertsClient.persistAlerts(); - test('should not persist alerts if shouldWrite is false', async () => { - alertsClientParams = { - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: { - ...ruleType, - alerts: { - ...ruleType.alerts!, - shouldWrite: false, - }, - }, - namespace: 'default', - rule: alertRuleData, - kibanaVersion: '8.9.0', - }; - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - expect(await alertsClient.persistAlerts()).toBe(void 0); - - expect(logger.debug).toHaveBeenCalledWith( - `Resources registered and installed for test context but "shouldWrite" is set to false.` - ); - expect(clusterClient.bulk).not.toHaveBeenCalled(); - }); - }); + expect(clusterClient.bulk).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + `Error writing 2 alerts to .alerts-test.alerts-default - fail` + ); + }); - // FLAKY: https://github.com/elastic/kibana/issues/163192 - // FLAKY: https://github.com/elastic/kibana/issues/163193 - // FLAKY: https://github.com/elastic/kibana/issues/163194 - // FLAKY: https://github.com/elastic/kibana/issues/163195 - describe.skip('getSummarizedAlerts', () => { - beforeEach(() => { - clusterClient.search.mockReturnValue({ - // @ts-ignore - hits: { total: { value: 0 }, hits: [] }, + test('should not persist alerts if shouldWrite is false', async () => { + alertsClientParams = { + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + ruleType: { + ...ruleType, + alerts: { + ...ruleType.alerts!, + shouldWrite: false, + }, + }, + namespace: 'default', + rule: alertRuleData, + kibanaVersion: '8.9.0', + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts }), + }; + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + expect(await alertsClient.persistAlerts()).toBe(void 0); + + expect(logger.debug).toHaveBeenCalledWith( + `Resources registered and installed for test context but "shouldWrite" is set to false.` + ); + expect(clusterClient.bulk).not.toHaveBeenCalled(); + }); }); - }); - const excludedAlertInstanceIds = ['1', '2']; - const alertsFilter: AlertsFilter = { - query: { - kql: 'kibana.alert.rule.name:test', - dsl: '{"bool":{"minimum_should_match":1,"should":[{"match":{"kibana.alert.rule.name":"test"}}]}}', - filters: [], - }, - timeframe: { - days: [1, 2, 3, 4, 5, 6, 7], - hours: { start: '08:00', end: '17:00' }, - timezone: 'UTC', - }, - }; - - test('should get the persistent LifeCycle Alerts successfully', async () => { - clusterClient.search - .mockReturnValueOnce({ - // @ts-ignore - hits: { total: { value: 1 }, hits: [mockAAD] }, - }) - .mockReturnValueOnce({ - // @ts-ignore - hits: { total: { value: 1 }, hits: [mockAAD, mockAAD] }, - }) - .mockReturnValueOnce({ - // @ts-ignore - hits: { total: { value: 0 }, hits: [] }, + // FLAKY: https://github.com/elastic/kibana/issues/163192 + // FLAKY: https://github.com/elastic/kibana/issues/163193 + // FLAKY: https://github.com/elastic/kibana/issues/163194 + // FLAKY: https://github.com/elastic/kibana/issues/163195 + describe('getSummarizedAlerts', () => { + beforeEach(() => { + clusterClient.search.mockReturnValue({ + // @ts-ignore + hits: { total: { value: 0 }, hits: [] }, + }); }); - const alertsClient = new AlertsClient(alertsClientParams); - const result = await alertsClient.getSummarizedAlerts(getParamsByExecutionUuid); - - expect(clusterClient.search).toHaveBeenCalledTimes(3); - - expect(result).toEqual({ - new: { - count: 1, - data: [ - { - _id: mockAAD._id, - _index: mockAAD._index, - ...expandFlattenedAlert(mockAAD._source), - }, - ], - }, - ongoing: { - count: 1, - data: [ - { - _id: mockAAD._id, - _index: mockAAD._index, - ...expandFlattenedAlert(mockAAD._source), + const excludedAlertInstanceIds = ['1', '2']; + const alertsFilter: AlertsFilter = { + query: { + kql: 'kibana.alert.rule.name:test', + dsl: '{"bool":{"minimum_should_match":1,"should":[{"match":{"kibana.alert.rule.name":"test"}}]}}', + filters: [], + }, + timeframe: { + days: [1, 2, 3, 4, 5, 6, 7], + hours: { start: '08:00', end: '17:00' }, + timezone: 'UTC', + }, + }; + + test('should get the persistent LifeCycle Alerts successfully', async () => { + clusterClient.search + .mockReturnValueOnce({ + // @ts-ignore + hits: { total: { value: 1 }, hits: [mockAAD] }, + }) + .mockReturnValueOnce({ + // @ts-ignore + hits: { total: { value: 1 }, hits: [mockAAD, mockAAD] }, + }) + .mockReturnValueOnce({ + // @ts-ignore + hits: { total: { value: 0 }, hits: [] }, + }); + + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.getSummarizedAlerts(getParamsByExecutionUuid); + + expect(clusterClient.search).toHaveBeenCalledTimes(3); + + expect(result).toEqual({ + new: { + count: 1, + data: [ + { + _id: mockAAD._id, + _index: mockAAD._index, + ...expandFlattenedAlert(mockAAD._source), + }, + ], }, - { - _id: mockAAD._id, - _index: mockAAD._index, - ...expandFlattenedAlert(mockAAD._source), + ongoing: { + count: 1, + data: [ + { + _id: mockAAD._id, + _index: mockAAD._index, + ...expandFlattenedAlert(mockAAD._source), + }, + { + _id: mockAAD._id, + _index: mockAAD._index, + ...expandFlattenedAlert(mockAAD._source), + }, + ], }, - ], - }, - recovered: { count: 0, data: [] }, - }); - }); + recovered: { count: 0, data: [] }, + }); + }); - test('should get the persistent Continual Alerts successfully', async () => { - clusterClient.search.mockReturnValueOnce({ - // @ts-ignore - hits: { total: { value: 1 }, hits: [mockAAD] }, - }); - const alertsClient = new AlertsClient({ - ...alertsClientParams, - ruleType: { - ...alertsClientParams.ruleType, - autoRecoverAlerts: false, - }, - }); + test('should get the persistent Continual Alerts successfully', async () => { + clusterClient.search.mockReturnValueOnce({ + // @ts-ignore + hits: { total: { value: 1 }, hits: [mockAAD] }, + }); + const alertsClient = new AlertsClient({ + ...alertsClientParams, + ruleType: { + ...alertsClientParams.ruleType, + autoRecoverAlerts: false, + }, + }); - const result = await alertsClient.getSummarizedAlerts(getParamsByExecutionUuid); + const result = await alertsClient.getSummarizedAlerts(getParamsByExecutionUuid); - expect(clusterClient.search).toHaveBeenCalledTimes(1); + expect(clusterClient.search).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - new: { - count: 1, - data: [ - { - _id: mockAAD._id, - _index: mockAAD._index, - ...expandFlattenedAlert(mockAAD._source), + expect(result).toEqual({ + new: { + count: 1, + data: [ + { + _id: mockAAD._id, + _index: mockAAD._index, + ...expandFlattenedAlert(mockAAD._source), + }, + ], }, - ], - }, - ongoing: { count: 0, data: [] }, - recovered: { count: 0, data: [] }, - }); - }); + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }); + }); - test('formats alerts with formatAlert when provided', async () => { - interface AlertData extends RuleAlertData { - 'signal.rule.consumer': string; - } - const alertsClient = new AlertsClient({ - ...alertsClientParams, - ruleType: { - ...alertsClientParams.ruleType, - autoRecoverAlerts: false, - alerts: { - context: 'test', - mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, - shouldWrite: true, - formatAlert: (alert) => { - const alertCopy = { ...alert } as Partial; - alertCopy['kibana.alert.rule.consumer'] = alert['signal.rule.consumer']; - delete alertCopy['signal.rule.consumer']; - return alertCopy; + test('formats alerts with formatAlert when provided', async () => { + interface AlertData extends RuleAlertData { + 'signal.rule.consumer': string; + } + const alertsClient = new AlertsClient({ + ...alertsClientParams, + ruleType: { + ...alertsClientParams.ruleType, + autoRecoverAlerts: false, + alerts: { + context: 'test', + mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, + shouldWrite: true, + formatAlert: (alert) => { + const alertCopy = { ...alert } as Partial; + alertCopy['kibana.alert.rule.consumer'] = alert['signal.rule.consumer']; + delete alertCopy['signal.rule.consumer']; + return alertCopy; + }, + }, }, - }, - }, - }); + }); - clusterClient.search.mockReturnValueOnce({ - // @ts-ignore - hits: { - total: { value: 1 }, - hits: [ - { - ...mockAAD, - _source: { ...mockAAD._source, 'signal.rule.consumer': 'signalConsumer' }, + clusterClient.search.mockReturnValueOnce({ + // @ts-ignore + hits: { + total: { value: 1 }, + hits: [ + { + ...mockAAD, + _source: { ...mockAAD._source, 'signal.rule.consumer': 'signalConsumer' }, + }, + ], }, - ], - }, - }); + }); - const result = await alertsClient.getSummarizedAlerts(getParamsByExecutionUuid); + const result = await alertsClient.getSummarizedAlerts(getParamsByExecutionUuid); - expect(clusterClient.search).toHaveBeenCalledTimes(1); + expect(clusterClient.search).toHaveBeenCalledTimes(1); - const expectedResult = { ...mockAAD._source }; - expectedResult['kibana.alert.rule.consumer'] = 'signalConsumer'; + const expectedResult = { ...mockAAD._source }; + expectedResult['kibana.alert.rule.consumer'] = 'signalConsumer'; - expect(result).toEqual({ - new: { - count: 1, - data: [ - { - _id: mockAAD._id, - _index: mockAAD._index, - ...expandFlattenedAlert(expectedResult), + expect(result).toEqual({ + new: { + count: 1, + data: [ + { + _id: mockAAD._id, + _index: mockAAD._index, + ...expandFlattenedAlert(expectedResult), + }, + ], }, - ], - }, - ongoing: { count: 0, data: [] }, - recovered: { count: 0, data: [] }, - }); - }); + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }); + }); - describe.each([ - { alertType: 'LifeCycle Alerts Query', isLifecycleAlert: true }, - { alertType: 'Continual Alerts Query', isLifecycleAlert: false }, - ])('$alertType', ({ isLifecycleAlert }) => { - describe.each([ - { - queryByType: 'ByExecutionUuid', - baseParams: getParamsByExecutionUuid, - getQuery: getExpectedQueryByExecutionUuid, - }, - { - queryByType: 'ByTimeRange', - baseParams: getParamsByTimeQuery, - getQuery: getExpectedQueryByTimeRange, - }, - ])('$queryByType', ({ baseParams, getQuery }) => { - test.each([ - { - text: 'should generate the correct query', - params: baseParams, - call1: getQuery({ - alertType: 'new', - isLifecycleAlert, - }), - call2: getQuery({ - alertType: 'ongoing', - }), - call3: getQuery({ - alertType: 'recovered', - }), - }, - { - text: 'should filter by excludedAlertInstanceIds', - params: { - ...baseParams, - excludedAlertInstanceIds, - }, - call1: getQuery({ - alertType: 'new', - isLifecycleAlert, - excludedAlertInstanceIds, - }), - call2: getQuery({ - alertType: 'ongoing', - excludedAlertInstanceIds, - }), - call3: getQuery({ - alertType: 'recovered', - excludedAlertInstanceIds, - }), - }, - { - text: 'should filter by alertsFilter', - params: { - ...baseParams, - alertsFilter, + describe.each([ + { alertType: 'LifeCycle Alerts Query', isLifecycleAlert: true }, + { alertType: 'Continual Alerts Query', isLifecycleAlert: false }, + ])('$alertType', ({ isLifecycleAlert }) => { + describe.each([ + { + queryByType: 'ByExecutionUuid', + baseParams: getParamsByExecutionUuid, + getQuery: getExpectedQueryByExecutionUuid, }, - call1: getQuery({ - alertType: 'new', - isLifecycleAlert, - alertsFilter, - }), - call2: getQuery({ - alertType: 'ongoing', - alertsFilter, - }), - call3: getQuery({ - alertType: 'recovered', - alertsFilter, - }), - }, - { - text: 'alertsFilter uses the all the days (ISO_WEEKDAYS) when no day is selected', - params: { - ...baseParams, - alertsFilter: { - ...alertsFilter, - timeframe: { - ...alertsFilter.timeframe!, - days: [], + { + queryByType: 'ByTimeRange', + baseParams: getParamsByTimeQuery, + getQuery: getExpectedQueryByTimeRange, + }, + ])('$queryByType', ({ baseParams, getQuery }) => { + const indexName = useDataStreamForAlerts + ? '.alerts-test.alerts-default' + : '.internal.alerts-test.alerts-default-*'; + test.each([ + { + text: 'should generate the correct query', + params: baseParams, + call1: getQuery({ + indexName, + alertType: 'new', + isLifecycleAlert, + }), + call2: getQuery({ + indexName, + alertType: 'ongoing', + }), + call3: getQuery({ + indexName, + alertType: 'recovered', + }), + }, + { + text: 'should filter by excludedAlertInstanceIds', + params: { + ...baseParams, + excludedAlertInstanceIds, + }, + call1: getQuery({ + indexName, + alertType: 'new', + isLifecycleAlert, + excludedAlertInstanceIds, + }), + call2: getQuery({ + indexName, + alertType: 'ongoing', + excludedAlertInstanceIds, + }), + call3: getQuery({ + indexName, + alertType: 'recovered', + excludedAlertInstanceIds, + }), + }, + { + text: 'should filter by alertsFilter', + params: { + ...baseParams, + alertsFilter, + }, + call1: getQuery({ + indexName, + alertType: 'new', + isLifecycleAlert, + alertsFilter, + }), + call2: getQuery({ + indexName, + alertType: 'ongoing', + alertsFilter, + }), + call3: getQuery({ + indexName, + alertType: 'recovered', + alertsFilter, + }), + }, + { + text: 'alertsFilter uses the all the days (ISO_WEEKDAYS) when no day is selected', + params: { + ...baseParams, + alertsFilter: { + ...alertsFilter, + timeframe: { + ...alertsFilter.timeframe!, + days: [], + }, + }, }, + call1: getQuery({ + indexName, + alertType: 'new', + isLifecycleAlert, + alertsFilter, + }), + call2: getQuery({ + indexName, + alertType: 'ongoing', + alertsFilter, + }), + call3: getQuery({ + indexName, + alertType: 'recovered', + alertsFilter, + }), }, - }, - call1: getQuery({ - alertType: 'new', - isLifecycleAlert, - alertsFilter, - }), - call2: getQuery({ - alertType: 'ongoing', - alertsFilter, - }), - call3: getQuery({ - alertType: 'recovered', - alertsFilter, - }), - }, - ])('$text', async ({ params, call1, call2, call3 }) => { - const alertsClient = new AlertsClient({ - ...alertsClientParams, - ruleType: { - ...alertsClientParams.ruleType, - autoRecoverAlerts: isLifecycleAlert, - }, + ])('$text', async ({ params, call1, call2, call3 }) => { + const alertsClient = new AlertsClient({ + ...alertsClientParams, + ruleType: { + ...alertsClientParams.ruleType, + autoRecoverAlerts: isLifecycleAlert, + }, + }); + await alertsClient.getSummarizedAlerts(params); + expect(clusterClient.search).toHaveBeenCalledTimes(isLifecycleAlert ? 3 : 1); + expect(clusterClient.search).toHaveBeenNthCalledWith(1, call1); + if (isLifecycleAlert) { + expect(clusterClient.search).toHaveBeenNthCalledWith(2, call2); + expect(clusterClient.search).toHaveBeenNthCalledWith(3, call3); + } + }); }); - await alertsClient.getSummarizedAlerts(params); - expect(clusterClient.search).toHaveBeenCalledTimes(isLifecycleAlert ? 3 : 1); - expect(clusterClient.search).toHaveBeenNthCalledWith(1, call1); - if (isLifecycleAlert) { - expect(clusterClient.search).toHaveBeenNthCalledWith(2, call2); - expect(clusterClient.search).toHaveBeenNthCalledWith(3, call3); - } }); - }); - }); - describe('throws error', () => { - let alertsClient: AlertsClient<{}, {}, {}, 'default', 'recovered'>; + describe('throws error', () => { + let alertsClient: AlertsClient<{}, {}, {}, 'default', 'recovered'>; - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - }); - test('if ruleId is not specified', async () => { - const { ruleId, ...paramsWithoutRuleId } = getParamsByExecutionUuid; + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + test('if ruleId is not specified', async () => { + const { ruleId, ...paramsWithoutRuleId } = getParamsByExecutionUuid; - await expect( - alertsClient.getSummarizedAlerts(paramsWithoutRuleId as GetSummarizedAlertsParams) - ).rejects.toThrowError(`Must specify both rule ID and space ID for AAD alert query.`); - }); + await expect( + alertsClient.getSummarizedAlerts(paramsWithoutRuleId as GetSummarizedAlertsParams) + ).rejects.toThrowError(`Must specify both rule ID and space ID for AAD alert query.`); + }); - test('if spaceId is not specified', async () => { - const { spaceId, ...paramsWithoutSpaceId } = getParamsByExecutionUuid; + test('if spaceId is not specified', async () => { + const { spaceId, ...paramsWithoutSpaceId } = getParamsByExecutionUuid; - await expect( - alertsClient.getSummarizedAlerts(paramsWithoutSpaceId as GetSummarizedAlertsParams) - ).rejects.toThrowError(`Must specify both rule ID and space ID for AAD alert query.`); - }); + await expect( + alertsClient.getSummarizedAlerts(paramsWithoutSpaceId as GetSummarizedAlertsParams) + ).rejects.toThrowError(`Must specify both rule ID and space ID for AAD alert query.`); + }); - test('if executionUuid or start date are not specified', async () => { - const { executionUuid, ...paramsWithoutExecutionUuid } = getParamsByExecutionUuid; + test('if executionUuid or start date are not specified', async () => { + const { executionUuid, ...paramsWithoutExecutionUuid } = getParamsByExecutionUuid; - await expect( - alertsClient.getSummarizedAlerts(paramsWithoutExecutionUuid as GetSummarizedAlertsParams) - ).rejects.toThrowError( - 'Must specify either execution UUID or time range for AAD alert query.' - ); - }); + await expect( + alertsClient.getSummarizedAlerts( + paramsWithoutExecutionUuid as GetSummarizedAlertsParams + ) + ).rejects.toThrowError( + 'Must specify either execution UUID or time range for AAD alert query.' + ); + }); - test('if start date is not specified for a TimeRange query', async () => { - const { start, ...paramsWithoutStart } = getParamsByTimeQuery; + test('if start date is not specified for a TimeRange query', async () => { + const { start, ...paramsWithoutStart } = getParamsByTimeQuery; - await expect( - alertsClient.getSummarizedAlerts(paramsWithoutStart as GetSummarizedAlertsParams) - ).rejects.toThrowError( - 'Must specify either execution UUID or time range for AAD alert query.' - ); - }); + await expect( + alertsClient.getSummarizedAlerts(paramsWithoutStart as GetSummarizedAlertsParams) + ).rejects.toThrowError( + 'Must specify either execution UUID or time range for AAD alert query.' + ); + }); - test('if end date is not specified for a TimeRange query', async () => { - const { end, ...paramsWithoutEnd } = getParamsByTimeQuery; + test('if end date is not specified for a TimeRange query', async () => { + const { end, ...paramsWithoutEnd } = getParamsByTimeQuery; - await expect( - alertsClient.getSummarizedAlerts(paramsWithoutEnd as GetSummarizedAlertsParams) - ).rejects.toThrowError( - 'Must specify either execution UUID or time range for AAD alert query.' - ); + await expect( + alertsClient.getSummarizedAlerts(paramsWithoutEnd as GetSummarizedAlertsParams) + ).rejects.toThrowError( + 'Must specify either execution UUID or time range for AAD alert query.' + ); + }); + }); }); - }); - }); - describe('report()', () => { - test('should create legacy alert with id, action group', async () => { - const mockGetUuidCurrent = jest - .fn() - .mockReturnValueOnce('uuid1') - .mockReturnValueOnce('uuid2'); - const mockGetStartCurrent = jest.fn().mockReturnValue(null); - const mockScheduleActionsCurrent = jest.fn().mockImplementation(() => ({ - replaceState: mockReplaceState, - getUuid: mockGetUuidCurrent, - getStart: mockGetStartCurrent, - })); - const mockCreateCurrent = jest.fn().mockImplementation(() => ({ - scheduleActions: mockScheduleActionsCurrent, - })); - mockLegacyAlertsClient.factory.mockImplementation(() => ({ create: mockCreateCurrent })); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); + describe('report()', () => { + test('should create legacy alert with id, action group', async () => { + const mockGetUuidCurrent = jest + .fn() + .mockReturnValueOnce('uuid1') + .mockReturnValueOnce('uuid2'); + const mockGetStartCurrent = jest.fn().mockReturnValue(null); + const mockScheduleActionsCurrent = jest.fn().mockImplementation(() => ({ + replaceState: mockReplaceState, + getUuid: mockGetUuidCurrent, + getStart: mockGetStartCurrent, + })); + const mockCreateCurrent = jest.fn().mockImplementation(() => ({ + scheduleActions: mockScheduleActionsCurrent, + })); + mockLegacyAlertsClient.factory.mockImplementation(() => ({ create: mockCreateCurrent })); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - // Report 2 new alerts - const { uuid: uuid1, start: start1 } = alertsClient.report({ - id: '1', - actionGroup: 'default', - state: {}, - context: {}, - }); - const { uuid: uuid2, start: start2 } = alertsClient.report({ - id: '2', - actionGroup: 'default', - state: {}, - context: {}, - }); + // Report 2 new alerts + const { uuid: uuid1, start: start1 } = alertsClient.report({ + id: '1', + actionGroup: 'default', + state: {}, + context: {}, + }); + const { uuid: uuid2, start: start2 } = alertsClient.report({ + id: '2', + actionGroup: 'default', + state: {}, + context: {}, + }); - expect(mockCreateCurrent).toHaveBeenCalledTimes(2); - expect(mockCreateCurrent).toHaveBeenNthCalledWith(1, '1'); - expect(mockCreateCurrent).toHaveBeenNthCalledWith(2, '2'); - expect(mockScheduleActionsCurrent).toHaveBeenCalledTimes(2); - expect(mockScheduleActionsCurrent).toHaveBeenNthCalledWith(1, 'default', {}); - expect(mockScheduleActionsCurrent).toHaveBeenNthCalledWith(2, 'default', {}); + expect(mockCreateCurrent).toHaveBeenCalledTimes(2); + expect(mockCreateCurrent).toHaveBeenNthCalledWith(1, '1'); + expect(mockCreateCurrent).toHaveBeenNthCalledWith(2, '2'); + expect(mockScheduleActionsCurrent).toHaveBeenCalledTimes(2); + expect(mockScheduleActionsCurrent).toHaveBeenNthCalledWith(1, 'default', {}); + expect(mockScheduleActionsCurrent).toHaveBeenNthCalledWith(2, 'default', {}); - expect(mockReplaceState).not.toHaveBeenCalled(); - spy.mockRestore(); + expect(mockReplaceState).not.toHaveBeenCalled(); + spy.mockRestore(); - expect(uuid1).toEqual('uuid1'); - expect(uuid2).toEqual('uuid2'); - expect(start1).toBeNull(); - expect(start2).toBeNull(); - }); + expect(uuid1).toEqual('uuid1'); + expect(uuid2).toEqual('uuid2'); + expect(start1).toBeNull(); + expect(start2).toBeNull(); + }); - test('should set context if defined', async () => { - mockLegacyAlertsClient.factory.mockImplementation(() => ({ create: mockCreate })); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, {}, { foo?: string }, 'default', 'recovered'>( - alertsClientParams - ); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); + test('should set context if defined', async () => { + mockLegacyAlertsClient.factory.mockImplementation(() => ({ create: mockCreate })); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + const alertsClient = new AlertsClient<{}, {}, { foo?: string }, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - // Report 2 new alerts - alertsClient.report({ - id: '1', - actionGroup: 'default', - state: {}, - context: { foo: 'cheese' }, - }); - alertsClient.report({ id: '2', actionGroup: 'default', state: {}, context: {} }); + // Report 2 new alerts + alertsClient.report({ + id: '1', + actionGroup: 'default', + state: {}, + context: { foo: 'cheese' }, + }); + alertsClient.report({ id: '2', actionGroup: 'default', state: {}, context: {} }); - expect(mockCreate).toHaveBeenCalledTimes(2); - expect(mockCreate).toHaveBeenNthCalledWith(1, '1'); - expect(mockCreate).toHaveBeenNthCalledWith(2, '2'); - expect(mockScheduleActions).toHaveBeenCalledTimes(2); - expect(mockScheduleActions).toHaveBeenNthCalledWith(1, 'default', { foo: 'cheese' }); - expect(mockScheduleActions).toHaveBeenNthCalledWith(2, 'default', {}); + expect(mockCreate).toHaveBeenCalledTimes(2); + expect(mockCreate).toHaveBeenNthCalledWith(1, '1'); + expect(mockCreate).toHaveBeenNthCalledWith(2, '2'); + expect(mockScheduleActions).toHaveBeenCalledTimes(2); + expect(mockScheduleActions).toHaveBeenNthCalledWith(1, 'default', { foo: 'cheese' }); + expect(mockScheduleActions).toHaveBeenNthCalledWith(2, 'default', {}); - expect(mockReplaceState).not.toHaveBeenCalled(); - spy.mockRestore(); - }); + expect(mockReplaceState).not.toHaveBeenCalled(); + spy.mockRestore(); + }); - test('should set state if defined', async () => { - mockLegacyAlertsClient.factory.mockImplementation(() => ({ create: mockCreate })); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, { count: number }, {}, 'default', 'recovered'>( - alertsClientParams - ); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); + test('should set state if defined', async () => { + mockLegacyAlertsClient.factory.mockImplementation(() => ({ create: mockCreate })); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + const alertsClient = new AlertsClient<{}, { count: number }, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - // Report 2 new alerts - alertsClient.report({ id: '1', actionGroup: 'default', state: { count: 1 }, context: {} }); - alertsClient.report({ id: '2', actionGroup: 'default', state: { count: 2 }, context: {} }); - - expect(mockCreate).toHaveBeenCalledTimes(2); - expect(mockCreate).toHaveBeenNthCalledWith(1, '1'); - expect(mockCreate).toHaveBeenNthCalledWith(2, '2'); - expect(mockScheduleActions).toHaveBeenCalledTimes(2); - expect(mockScheduleActions).toHaveBeenNthCalledWith(1, 'default', {}); - expect(mockScheduleActions).toHaveBeenNthCalledWith(2, 'default', {}); - expect(mockReplaceState).toHaveBeenCalledTimes(2); - expect(mockReplaceState).toHaveBeenNthCalledWith(1, { count: 1 }); - expect(mockReplaceState).toHaveBeenNthCalledWith(2, { count: 2 }); - spy.mockRestore(); - }); + // Report 2 new alerts + alertsClient.report({ + id: '1', + actionGroup: 'default', + state: { count: 1 }, + context: {}, + }); + alertsClient.report({ + id: '2', + actionGroup: 'default', + state: { count: 2 }, + context: {}, + }); - test('should set payload if defined and write out to alert doc', async () => { - const alertsClient = new AlertsClient< - { count: number; url: string }, - {}, - {}, - 'default', - 'recovered' - >(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); + expect(mockCreate).toHaveBeenCalledTimes(2); + expect(mockCreate).toHaveBeenNthCalledWith(1, '1'); + expect(mockCreate).toHaveBeenNthCalledWith(2, '2'); + expect(mockScheduleActions).toHaveBeenCalledTimes(2); + expect(mockScheduleActions).toHaveBeenNthCalledWith(1, 'default', {}); + expect(mockScheduleActions).toHaveBeenNthCalledWith(2, 'default', {}); + expect(mockReplaceState).toHaveBeenCalledTimes(2); + expect(mockReplaceState).toHaveBeenNthCalledWith(1, { count: 1 }); + expect(mockReplaceState).toHaveBeenNthCalledWith(2, { count: 2 }); + spy.mockRestore(); + }); - // Report 2 new alerts - alertsClient.report({ - id: '1', - actionGroup: 'default', - state: {}, - context: {}, - payload: { count: 1, url: `https://url1` }, - }); - alertsClient.report({ - id: '2', - actionGroup: 'default', - state: {}, - context: {}, - payload: { count: 2, url: `https://url2` }, - }); + test('should set payload if defined and write out to alert doc', async () => { + const alertsClient = new AlertsClient< + { count: number; url: string }, + {}, + {}, + 'default', + 'recovered' + >(alertsClientParams); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + // Report 2 new alerts + alertsClient.report({ + id: '1', + actionGroup: 'default', + state: {}, + context: {}, + payload: { count: 1, url: `https://url1` }, + }); + alertsClient.report({ + id: '2', + actionGroup: 'default', + state: {}, + context: {}, + payload: { count: 2, url: `https://url2` }, + }); - await alertsClient.persistAlerts(); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - const { alertsToReturn } = alertsClient.getAlertsToSerialize(); - const uuid1 = alertsToReturn['1'].meta?.uuid; - const uuid2 = alertsToReturn['2'].meta?.uuid; + await alertsClient.persistAlerts(); - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { index: { _id: uuid1 } }, - // new alert doc - { - '@timestamp': date, - count: 1, - event: { - action: 'open', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true], - instance: { - id: '1', + const { alertsToReturn } = alertsClient.getAlertsToSerialize(); + const uuid1 = alertsToReturn['1'].meta?.uuid; + const uuid2 = alertsToReturn['2'].meta?.uuid; + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + create: { _id: uuid1, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, + }, + // new alert doc + { + '@timestamp': date, + count: 1, + event: { + action: 'open', + kind: 'signal', }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + maintenance_window_ids: [], + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: date, + status: 'active', + time_range: { + gte: date, + }, + uuid: uuid1, + workflow_status: 'open', }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: date, - status: 'active', - time_range: { - gte: date, + space_ids: ['default'], + version: '8.9.0', }, - uuid: uuid1, - workflow_status: 'open', + tags: ['rule-', '-tags'], + url: `https://url1`, }, - space_ids: ['default'], - version: '8.9.0', - }, - tags: ['rule-', '-tags'], - url: `https://url1`, - }, - { index: { _id: uuid2 } }, - // new alert doc - { - '@timestamp': date, - count: 2, - event: { - action: 'open', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true], - instance: { - id: '2', + { + create: { _id: uuid2, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, + }, + // new alert doc + { + '@timestamp': date, + count: 2, + event: { + action: 'open', + kind: 'signal', }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '2', + }, + maintenance_window_ids: [], + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: date, + status: 'active', + time_range: { + gte: date, + }, + uuid: uuid2, + workflow_status: 'open', }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: date, - status: 'active', - time_range: { - gte: date, + space_ids: ['default'], + version: '8.9.0', }, - uuid: uuid2, - workflow_status: 'open', + tags: ['rule-', '-tags'], + url: `https://url2`, }, - space_ids: ['default'], - version: '8.9.0', - }, - tags: ['rule-', '-tags'], - url: `https://url2`, - }, - ], + ], + }); + }); }); - }); - }); - describe('setAlertData()', () => { - test('should call setContext on legacy alert', async () => { - mockLegacyAlertsClient.getAlert.mockReturnValueOnce({ - getId: jest.fn().mockReturnValue('1'), - setContext: mockSetContext, - }); - mockLegacyAlertsClient.getAlert.mockReturnValueOnce({ - getId: jest.fn().mockReturnValue('1'), - setContext: mockSetContext, - }); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }, - '2': { - state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, - meta: { - flapping: false, - flappingHistory: [true, false], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'def', + describe('setAlertData()', () => { + test('should call setContext on legacy alert', async () => { + mockLegacyAlertsClient.getAlert.mockReturnValueOnce({ + getId: jest.fn().mockReturnValue('1'), + setContext: mockSetContext, + }); + mockLegacyAlertsClient.getAlert.mockReturnValueOnce({ + getId: jest.fn().mockReturnValue('1'), + setContext: mockSetContext, + }); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, + }, + '2': { + state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, + meta: { + flapping: false, + flappingHistory: [true, false], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'def', + }, + }, }, - }, - }, - recoveredAlertsFromState: {}, - }); + recoveredAlertsFromState: {}, + }); - // Set context on 2 recovered alerts - alertsClient.setAlertData({ id: '1', context: { foo: 'bar' } }); - alertsClient.setAlertData({ id: '2' }); + // Set context on 2 recovered alerts + alertsClient.setAlertData({ id: '1', context: { foo: 'bar' } }); + alertsClient.setAlertData({ id: '2' }); - expect(mockSetContext).toHaveBeenCalledTimes(2); - expect(mockSetContext).toHaveBeenNthCalledWith(1, { foo: 'bar' }); - expect(mockSetContext).toHaveBeenNthCalledWith(2, {}); - spy.mockRestore(); - }); + expect(mockSetContext).toHaveBeenCalledTimes(2); + expect(mockSetContext).toHaveBeenNthCalledWith(1, { foo: 'bar' }); + expect(mockSetContext).toHaveBeenNthCalledWith(2, {}); + spy.mockRestore(); + }); - test('should throw error if called on unknown alert id', async () => { - mockLegacyAlertsClient.getAlert.mockReturnValueOnce(null); - const spy = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, {}, { foo?: string }, 'default', 'recovered'>( - alertsClientParams - ); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }, - '2': { - state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, - meta: { - flapping: false, - flappingHistory: [true, false], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'def', + test('should throw error if called on unknown alert id', async () => { + mockLegacyAlertsClient.getAlert.mockReturnValueOnce(null); + const spy = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + const alertsClient = new AlertsClient<{}, {}, { foo?: string }, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, + }, + '2': { + state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, + meta: { + flapping: false, + flappingHistory: [true, false], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'def', + }, + }, }, - }, - }, - recoveredAlertsFromState: {}, - }); + recoveredAlertsFromState: {}, + }); - // Set context on 2 recovered alerts - expect(() => { - alertsClient.setAlertData({ id: '1', context: { foo: 'bar' } }); - }).toThrowErrorMatchingInlineSnapshot( - `"Cannot set alert data for alert 1 because it has not been reported and it is not recovered."` - ); - spy.mockRestore(); - }); + // Set context on 2 recovered alerts + expect(() => { + alertsClient.setAlertData({ id: '1', context: { foo: 'bar' } }); + }).toThrowErrorMatchingInlineSnapshot( + `"Cannot set alert data for alert 1 because it has not been reported and it is not recovered."` + ); + spy.mockRestore(); + }); - test('should successfully update context and payload for new alert', async () => { - clusterClient.search.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, - hits: { - total: { - relation: 'eq', - value: 0, - }, - hits: [], - }, - }); - const alertsClient = new AlertsClient< - { count: number; url: string }, - {}, - {}, - 'default', - 'recovered' - >(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); + test('should successfully update context and payload for new alert', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 0, + }, + hits: [], + }, + }); + const alertsClient = new AlertsClient< + { count: number; url: string }, + {}, + {}, + 'default', + 'recovered' + >(alertsClientParams); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - // Report new alert - alertsClient.report({ - id: '1', - actionGroup: 'default', - context: { foo: 'bar' }, - payload: { count: 1, url: `http://localhost:5601` }, - }); + // Report new alert + alertsClient.report({ + id: '1', + actionGroup: 'default', + context: { foo: 'bar' }, + payload: { count: 1, url: `http://localhost:5601` }, + }); - // Update context and payload on the new alert - alertsClient.setAlertData({ - id: '1', - context: { foo: 'notbar' }, - payload: { count: 100, url: `https://elastic.co` }, - }); + // Update context and payload on the new alert + alertsClient.setAlertData({ + id: '1', + context: { foo: 'notbar' }, + payload: { count: 100, url: `https://elastic.co` }, + }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - await alertsClient.persistAlerts(); + await alertsClient.persistAlerts(); - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { - index: { - _id: expect.any(String), - }, - }, - { - '@timestamp': date, - count: 100, - event: { - action: 'open', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + create: { + _id: expect.any(String), + ...(useDataStreamForAlerts ? {} : { require_alias: true }), }, - flapping: false, - flapping_history: [true], - instance: { - id: '1', + }, + { + '@timestamp': date, + count: 100, + event: { + action: 'open', + kind: 'signal', }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + maintenance_window_ids: [], + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: date, + status: 'active', + time_range: { + gte: date, + }, + uuid: expect.any(String), + workflow_status: 'open', }, - name: 'rule-name', - parameters: { - bar: true, + space_ids: ['default'], + version: '8.9.0', + }, + tags: ['rule-', '-tags'], + url: 'https://elastic.co', + }, + ], + }); + }); + + test('should successfully update context and payload for ongoing alert', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 1, + }, + hits: [ + { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + _source: { + '@timestamp': '2023-03-28T12:27:28.159Z', + count: 1, + url: 'https://localhost:5601/abc', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abc', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', }, - start: date, - status: 'active', - time_range: { - gte: date, + ], + }, + }); + const alertsClient = new AlertsClient< + { count: number; url: string }, + {}, + {}, + 'default', + 'recovered' + >(alertsClientParams); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', }, - uuid: expect.any(String), - workflow_status: 'open', }, - space_ids: ['default'], - version: '8.9.0', }, - tags: ['rule-', '-tags'], - url: 'https://elastic.co', - }, - ], - }); - }); + recoveredAlertsFromState: {}, + }); - test('should successfully update context and payload for ongoing alert', async () => { - clusterClient.search.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, - hits: { - total: { - relation: 'eq', - value: 1, - }, - hits: [ - { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - _source: { - '@timestamp': '2023-03-28T12:27:28.159Z', - count: 1, - url: 'https://localhost:5601/abc', + // Report ongoing alert + alertsClient.report({ + id: '1', + actionGroup: 'default', + context: { foo: 'bar' }, + payload: { count: 1, url: `http://localhost:5601` }, + }); + + // Update context and payload on the new alert + alertsClient.setAlertData({ + id: '1', + context: { foo: 'notbar' }, + payload: { count: 100, url: `https://elastic.co` }, + }); + + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + + await alertsClient.persistAlerts(); + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + create: { + _id: 'abc', + ...(useDataStreamForAlerts ? {} : { require_alias: true }), + }, + }, + { + '@timestamp': date, + count: 100, event: { action: 'active', kind: 'signal', @@ -2172,13 +2365,14 @@ describe('Alerts Client', () => { alert: { action_group: 'default', duration: { - us: '0', + us: '36000000000000', }, flapping: false, - flapping_history: [true], + flapping_history: [true, false], instance: { id: '1', }, + maintenance_window_ids: [], rule: { category: 'My test rule', consumer: 'bar', @@ -2204,158 +2398,157 @@ describe('Alerts Client', () => { workflow_status: 'open', }, space_ids: ['default'], - version: '8.8.0', + version: '8.9.0', }, tags: ['rule-', '-tags'], + url: 'https://elastic.co', + }, + ], + }); + }); + + test('should successfully update context and payload for recovered alert', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 1, }, + hits: [ + { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + _seq_no: 42, + _primary_term: 666, + _source: { + '@timestamp': '2023-03-28T12:27:28.159Z', + count: 1, + url: 'https://localhost:5601/abc', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T11:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T11:27:28.159Z', + }, + uuid: 'abc', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], + }, + }, + ], }, - ], - }, - }); - const alertsClient = new AlertsClient< - { count: number; url: string }, - {}, - {}, - 'default', - 'recovered' - >(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', + }); + const alertsClient = new AlertsClient< + { count: number; url: string }, + {}, + {}, + 'default', + 'recovered' + >(alertsClientParams); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T11:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, + }, }, - }, - }, - recoveredAlertsFromState: {}, - }); + recoveredAlertsFromState: {}, + }); - // Report ongoing alert - alertsClient.report({ - id: '1', - actionGroup: 'default', - context: { foo: 'bar' }, - payload: { count: 1, url: `http://localhost:5601` }, - }); + // Don't report any alerts so existing alert recovers - // Update context and payload on the new alert - alertsClient.setAlertData({ - id: '1', - context: { foo: 'notbar' }, - payload: { count: 100, url: `https://elastic.co` }, - }); + // Update context and payload on the new alert + alertsClient.setAlertData({ + id: '1', + context: { foo: 'notbar' }, + payload: { count: 100, url: `https://elastic.co` }, + }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); - await alertsClient.persistAlerts(); + await alertsClient.persistAlerts(); - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { - index: { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - require_alias: false, - }, - }, - { - '@timestamp': date, - count: 100, - event: { - action: 'active', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '36000000000000', - }, - flapping: false, - flapping_history: [true, false], - instance: { - id: '1', - }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, - }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: '2023-03-28T12:27:28.159Z', - status: 'active', - time_range: { - gte: '2023-03-28T12:27:28.159Z', + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + index: { + _id: 'abc', + if_primary_term: 666, + if_seq_no: 42, + _index: '.internal.alerts-test.alerts-default-000001', + require_alias: false, }, - uuid: 'abc', - workflow_status: 'open', }, - space_ids: ['default'], - version: '8.9.0', - }, - tags: ['rule-', '-tags'], - url: 'https://elastic.co', - }, - ], - }); - }); - - test('should successfully update context and payload for recovered alert', async () => { - clusterClient.search.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, - hits: { - total: { - relation: 'eq', - value: 1, - }, - hits: [ - { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - _source: { - '@timestamp': '2023-03-28T12:27:28.159Z', - count: 1, - url: 'https://localhost:5601/abc', + { + '@timestamp': date, + count: 100, event: { - action: 'active', + action: 'close', kind: 'signal', }, kibana: { alert: { - action_group: 'default', + action_group: 'recovered', duration: { - us: '0', + us: '39600000000000', }, + end: date, flapping: false, - flapping_history: [true], + flapping_history: [true, true], instance: { id: '1', }, + maintenance_window_ids: [], rule: { category: 'My test rule', consumer: 'bar', @@ -2373,94 +2566,162 @@ describe('Alerts Client', () => { uuid: '1', }, start: '2023-03-28T11:27:28.159Z', - status: 'active', + status: 'recovered', time_range: { gte: '2023-03-28T11:27:28.159Z', + lte: date, }, uuid: 'abc', workflow_status: 'open', }, space_ids: ['default'], - version: '8.8.0', + version: '8.9.0', }, tags: ['rule-', '-tags'], + url: 'https://elastic.co', }, - }, - ], - }, - }); - const alertsClient = new AlertsClient< - { count: number; url: string }, - {}, - {}, - 'default', - 'recovered' - >(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T11:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }, - }, - recoveredAlertsFromState: {}, + ], + }); + }); }); - // Don't report any alerts so existing alert recovers + describe('client()', () => { + test('only returns subset of functionality', async () => { + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); - // Update context and payload on the new alert - alertsClient.setAlertData({ - id: '1', - context: { foo: 'notbar' }, - payload: { count: 100, url: `https://elastic.co` }, - }); + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: {}, + recoveredAlertsFromState: {}, + }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + const publicAlertsClient = alertsClient.client(); - await alertsClient.persistAlerts(); + expect(keys(publicAlertsClient)).toEqual([ + 'report', + 'setAlertData', + 'getAlertLimitValue', + 'setAlertLimitReached', + 'getRecoveredAlerts', + ]); + }); - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { - index: { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - require_alias: false, + test('should return recovered alert document with recovered alert, if it exists', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 1, + }, + hits: [ + { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + _source: { + '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abc', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], + }, + }, + ], }, - }, - { - '@timestamp': date, - count: 100, + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, + }, + }, + recoveredAlertsFromState: {}, + }); + + // report no alerts to allow existing alert to recover + + const publicAlertsClient = alertsClient.client(); + const recoveredAlerts = publicAlertsClient.getRecoveredAlerts(); + expect(recoveredAlerts.length).toEqual(1); + const recoveredAlert = recoveredAlerts[0]; + expect(recoveredAlert.alert.getId()).toEqual('1'); + expect(recoveredAlert.alert.getUuid()).toEqual('abc'); + expect(recoveredAlert.alert.getStart()).toEqual('2023-03-28T12:27:28.159Z'); + expect(recoveredAlert.hit).toEqual({ + '@timestamp': '2023-03-28T12:27:28.159Z', event: { - action: 'close', + action: 'active', kind: 'signal', }, kibana: { alert: { - action_group: 'recovered', + action_group: 'default', duration: { - us: '39600000000000', + us: '0', }, - end: date, flapping: false, - flapping_history: [true, true], + flapping_history: [true], instance: { id: '1', }, - maintenance_window_ids: [], rule: { category: 'My test rule', consumer: 'bar', @@ -2477,233 +2738,68 @@ describe('Alerts Client', () => { tags: ['rule-', '-tags'], uuid: '1', }, - start: '2023-03-28T11:27:28.159Z', - status: 'recovered', + start: '2023-03-28T12:27:28.159Z', + status: 'active', time_range: { - gte: '2023-03-28T11:27:28.159Z', - lte: date, + gte: '2023-03-28T12:27:28.159Z', }, uuid: 'abc', workflow_status: 'open', }, space_ids: ['default'], - version: '8.9.0', + version: '8.8.0', }, tags: ['rule-', '-tags'], - url: 'https://elastic.co', - }, - ], - }); - }); - }); - - describe('client()', () => { - test('only returns subset of functionality', async () => { - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: {}, - recoveredAlertsFromState: {}, - }); - - const publicAlertsClient = alertsClient.client(); - - expect(keys(publicAlertsClient)).toEqual([ - 'report', - 'setAlertData', - 'getAlertLimitValue', - 'setAlertLimitReached', - 'getRecoveredAlerts', - ]); - }); + }); + }); - test('should return recovered alert document with recovered alert, if it exists', async () => { - clusterClient.search.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, - hits: { - total: { - relation: 'eq', - value: 1, - }, - hits: [ - { - _id: 'abc', - _index: '.internal.alerts-test.alerts-default-000001', - _source: { - '@timestamp': '2023-03-28T12:27:28.159Z', - event: { - action: 'active', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true], - instance: { - id: '1', - }, - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, - }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: '2023-03-28T12:27:28.159Z', - status: 'active', - time_range: { - gte: '2023-03-28T12:27:28.159Z', - }, - uuid: 'abc', - workflow_status: 'open', - }, - space_ids: ['default'], - version: '8.8.0', - }, - tags: ['rule-', '-tags'], + test('should return undefined document with recovered alert, if it does not exists', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 0, }, + hits: [], }, - ], - }, - }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }, - }, - recoveredAlertsFromState: {}, - }); - - // report no alerts to allow existing alert to recover - - const publicAlertsClient = alertsClient.client(); - const recoveredAlerts = publicAlertsClient.getRecoveredAlerts(); - expect(recoveredAlerts.length).toEqual(1); - const recoveredAlert = recoveredAlerts[0]; - expect(recoveredAlert.alert.getId()).toEqual('1'); - expect(recoveredAlert.alert.getUuid()).toEqual('abc'); - expect(recoveredAlert.alert.getStart()).toEqual('2023-03-28T12:27:28.159Z'); - expect(recoveredAlert.hit).toEqual({ - '@timestamp': '2023-03-28T12:27:28.159Z', - event: { - action: 'active', - kind: 'signal', - }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', - }, - flapping: false, - flapping_history: [true], - instance: { - id: '1', - }, - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - name: 'rule-name', - parameters: { - bar: true, + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test.rule-type', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: '2023-03-28T12:27:28.159Z', - status: 'active', - time_range: { - gte: '2023-03-28T12:27:28.159Z', - }, - uuid: 'abc', - workflow_status: 'open', - }, - space_ids: ['default'], - version: '8.8.0', - }, - tags: ['rule-', '-tags'], - }); - }); - - test('should return undefined document with recovered alert, if it does not exists', async () => { - clusterClient.search.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, - hits: { - total: { - relation: 'eq', - value: 0, - }, - hits: [], - }, - }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); - await alertsClient.initializeExecution({ - maxAlerts, - ruleLabel: `test: rule-name`, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', }, - }, - }, - recoveredAlertsFromState: {}, - }); + recoveredAlertsFromState: {}, + }); - // report no alerts to allow existing alert to recover + // report no alerts to allow existing alert to recover - const publicAlertsClient = alertsClient.client(); - const recoveredAlerts = publicAlertsClient.getRecoveredAlerts(); - expect(recoveredAlerts.length).toEqual(1); - const recoveredAlert = recoveredAlerts[0]; - expect(recoveredAlert.alert.getId()).toEqual('1'); - expect(recoveredAlert.alert.getUuid()).toEqual('abc'); - expect(recoveredAlert.alert.getStart()).toEqual('2023-03-28T12:27:28.159Z'); - expect(recoveredAlert.hit).toBeUndefined(); + const publicAlertsClient = alertsClient.client(); + const recoveredAlerts = publicAlertsClient.getRecoveredAlerts(); + expect(recoveredAlerts.length).toEqual(1); + const recoveredAlert = recoveredAlerts[0]; + expect(recoveredAlert.alert.getId()).toEqual('1'); + expect(recoveredAlert.alert.getUuid()).toEqual('abc'); + expect(recoveredAlert.alert.getStart()).toEqual('2023-03-28T12:27:28.159Z'); + expect(recoveredAlert.hit).toBeUndefined(); + }); + }); }); - }); + } }); diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 1153f871a60ec..03500c8b94575 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -19,6 +19,7 @@ import { AlertInstanceState, RuleAlertData, WithoutReservedActionGroups, + DataStreamAdapter, } from '../types'; import { LegacyAlertsClient } from './legacy_alerts_client'; import { @@ -54,6 +55,7 @@ const CHUNK_SIZE = 10000; export interface AlertsClientParams extends CreateAlertsClientParams { elasticsearchClientPromise: Promise; kibanaVersion: string; + dataStreamAdapter: DataStreamAdapter; } export class AlertsClient< @@ -78,6 +80,8 @@ export class AlertsClient< private fetchedAlerts: { indices: Record; data: Record; + seqNo: Record; + primaryTerm: Record; }; private rule: AlertRule = {}; @@ -86,6 +90,7 @@ export class AlertsClient< private indexTemplateAndPattern: IIndexPatternString; private reportedAlerts: Record> = {}; + private _isUsingDataStreams: boolean; constructor(private readonly options: AlertsClientParams) { this.legacyAlertsClient = new LegacyAlertsClient< @@ -100,9 +105,10 @@ export class AlertsClient< ? this.options.namespace : DEFAULT_NAMESPACE_STRING, }); - this.fetchedAlerts = { indices: {}, data: {} }; + this.fetchedAlerts = { indices: {}, data: {}, seqNo: {}, primaryTerm: {} }; this.rule = formatRule({ rule: this.options.rule, ruleType: this.options.ruleType }); this.ruleType = options.ruleType; + this._isUsingDataStreams = this.options.dataStreamAdapter.isUsingDataStreams(); } public async initializeExecution(opts: InitializeExecutionOpts) { @@ -131,6 +137,7 @@ export class AlertsClient< const queryByUuid = async (uuids: string[]) => { const result = await this.search({ size: uuids.length, + seq_no_primary_term: true, query: { bool: { filter: [ @@ -166,6 +173,8 @@ export class AlertsClient< // Keep track of index so we can update the correct document this.fetchedAlerts.indices[alertUuid] = hit._index; + this.fetchedAlerts.seqNo[alertUuid] = hit._seq_no; + this.fetchedAlerts.primaryTerm[alertUuid] = hit._primary_term; } } catch (err) { this.options.logger.error(`Error searching for tracked alerts by UUID - ${err.message}`); @@ -174,11 +183,15 @@ export class AlertsClient< public async search(queryBody: SearchRequest['body']): Promise> { const esClient = await this.options.elasticsearchClientPromise; + const index = this.isUsingDataStreams() + ? this.indexTemplateAndPattern.alias + : this.indexTemplateAndPattern.pattern; const { hits: { hits, total }, } = await esClient.search({ - index: this.indexTemplateAndPattern.pattern, + index, body: queryBody, + ignore_unavailable: true, }); return { hits, total }; @@ -366,34 +379,31 @@ export class AlertsClient< const alertsToIndex = [...activeAlertsToIndex, ...recoveredAlertsToIndex]; if (alertsToIndex.length > 0) { + const bulkBody = flatMap( + [...activeAlertsToIndex, ...recoveredAlertsToIndex].map((alert: Alert & AlertData) => [ + getBulkMeta( + alert.kibana.alert.uuid, + this.fetchedAlerts.indices[alert.kibana.alert.uuid], + this.fetchedAlerts.seqNo[alert.kibana.alert.uuid], + this.fetchedAlerts.primaryTerm[alert.kibana.alert.uuid], + this.isUsingDataStreams() + ), + alert, + ]) + ); + try { const response = await esClient.bulk({ refresh: 'wait_for', index: this.indexTemplateAndPattern.alias, - require_alias: true, - body: flatMap( - [...activeAlertsToIndex, ...recoveredAlertsToIndex].map((alert: Alert & AlertData) => [ - { - index: { - _id: alert.kibana.alert.uuid, - // If we know the concrete index for this alert, specify it - ...(this.fetchedAlerts.indices[alert.kibana.alert.uuid] - ? { - _index: this.fetchedAlerts.indices[alert.kibana.alert.uuid], - require_alias: false, - } - : {}), - }, - }, - alert, - ]) - ), + require_alias: !this.isUsingDataStreams(), + body: bulkBody, }); // If there were individual indexing errors, they will be returned in the success response if (response && response.errors) { const errorsInResponse = (response.items ?? []) - .map((item) => (item && item.index && item.index.error ? item.index.error : null)) + .map((item) => item?.index?.error || item?.create?.error) .filter((item) => item != null); this.options.logger.error( @@ -408,6 +418,33 @@ export class AlertsClient< ); } } + + function getBulkMeta( + uuid: string, + index: string | undefined, + seqNo: number | undefined, + primaryTerm: number | undefined, + isUsingDataStreams: boolean + ) { + if (index && seqNo != null && primaryTerm != null) { + return { + index: { + _id: uuid, + _index: index, + if_seq_no: seqNo, + if_primary_term: primaryTerm, + require_alias: false, + }, + }; + } + + return { + create: { + _id: uuid, + ...(isUsingDataStreams ? {} : { require_alias: true }), + }, + }; + } } public getAlertsToSerialize() { @@ -506,4 +543,8 @@ export class AlertsClient< }, }; } + + public isUsingDataStreams(): boolean { + return this._isUsingDataStreams; + } } diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client_fixtures.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client_fixtures.ts index 4ff3c14120d14..aa513588b83f8 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client_fixtures.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client_fixtures.ts @@ -77,6 +77,7 @@ export const getParamsByTimeQuery: GetSummarizedAlertsParams = { }; export const getExpectedQueryByExecutionUuid = ({ + indexName, uuid = getParamsByExecutionUuid.executionUuid, ruleId = getParamsByExecutionUuid.ruleId, alertType, @@ -84,6 +85,7 @@ export const getExpectedQueryByExecutionUuid = ({ excludedAlertInstanceIds, alertsFilter, }: { + indexName: string; uuid?: string; ruleId?: string; alertType: keyof typeof alertTypes; @@ -184,10 +186,12 @@ export const getExpectedQueryByExecutionUuid = ({ size: 100, track_total_hits: true, }, - index: '.internal.alerts-test.alerts-default-*', + ignore_unavailable: true, + index: indexName, }); export const getExpectedQueryByTimeRange = ({ + indexName, end = getParamsByTimeQuery.end.toISOString(), start = getParamsByTimeQuery.start.toISOString(), ruleId = getParamsByTimeQuery.ruleId, @@ -196,6 +200,7 @@ export const getExpectedQueryByTimeRange = ({ excludedAlertInstanceIds, alertsFilter, }: { + indexName: string; end?: string; start?: string; ruleId?: string; @@ -344,6 +349,7 @@ export const getExpectedQueryByTimeRange = ({ size: 100, track_total_hits: true, }, - index: '.internal.alerts-test.alerts-default-*', + ignore_unavailable: true, + index: indexName, }; }; diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts index e3942b26ee6fa..90552e1d5b0ac 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; +import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { ReplaySubject, Subject } from 'rxjs'; import { AlertsService } from './alerts_service'; @@ -16,6 +17,7 @@ import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { AlertsClient } from '../alerts_client'; import { alertsClientMock } from '../alerts_client/alerts_client.mock'; +import { getDataStreamAdapter } from './lib/data_stream_adapter'; jest.mock('../alerts_client'); @@ -63,6 +65,20 @@ const GetAliasResponse = { }, }; +const GetDataStreamResponse: IndicesGetDataStreamResponse = { + data_streams: [ + { + name: 'ignored', + generation: 1, + timestamp_field: { name: 'ignored' }, + hidden: true, + indices: [{ index_name: 'ignored', index_uuid: 'ignored' }], + status: 'green', + template: 'ignored', + }, + ], +}; + const IlmPutBody = { policy: { _meta: { @@ -88,6 +104,7 @@ interface GetIndexTemplatePutBodyOpts { useLegacyAlerts?: boolean; useEcs?: boolean; secondaryAlias?: string; + useDataStream?: boolean; } const getIndexTemplatePutBody = (opts?: GetIndexTemplatePutBodyOpts) => { const context = opts ? opts.context : undefined; @@ -95,25 +112,35 @@ const getIndexTemplatePutBody = (opts?: GetIndexTemplatePutBodyOpts) => { const useLegacyAlerts = opts ? opts.useLegacyAlerts : undefined; const useEcs = opts ? opts.useEcs : undefined; const secondaryAlias = opts ? opts.secondaryAlias : undefined; + const useDataStream = opts?.useDataStream ?? false; + + const indexPatterns = useDataStream + ? [`.alerts-${context ? context : 'test'}.alerts-${namespace}`] + : [`.internal.alerts-${context ? context : 'test'}.alerts-${namespace}-*`]; return { name: `.alerts-${context ? context : 'test'}.alerts-${namespace}-index-template`, body: { - index_patterns: [`.internal.alerts-${context ? context : 'test'}.alerts-${namespace}-*`], + index_patterns: indexPatterns, composed_of: [ ...(useEcs ? ['.alerts-ecs-mappings'] : []), `.alerts-${context ? `${context}.alerts` : 'test.alerts'}-mappings`, ...(useLegacyAlerts ? ['.alerts-legacy-alert-mappings'] : []), '.alerts-framework-mappings', ], + ...(useDataStream ? { data_stream: { hidden: true } } : {}), priority: namespace.length, template: { settings: { auto_expand_replicas: '0-1', hidden: true, - 'index.lifecycle': { - name: '.alerts-ilm-policy', - rollover_alias: `.alerts-${context ? context : 'test'}.alerts-${namespace}`, - }, + ...(useDataStream + ? {} + : { + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-${context ? context : 'test'}.alerts-${namespace}`, + }, + }), 'index.mapping.total_fields.limit': 2500, }, mappings: { @@ -186,7 +213,7 @@ describe('Alerts Service', () => { let pluginStop$: Subject; beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); logger = loggingSystemMock.createLogger(); pluginStop$ = new ReplaySubject(1); jest.spyOn(global.Math, 'random').mockReturnValue(0.01); @@ -195,1809 +222,2145 @@ describe('Alerts Service', () => { async () => SimulateTemplateResponse ); clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); }); afterEach(() => { pluginStop$.next(); pluginStop$.complete(); }); - describe('AlertsService()', () => { - test('should correctly initialize common resources', async () => { - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - expect(alertsService.isInitialized()).toEqual(true); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(3); - - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - }); - test('should log error and set initialized to false if adding ILM policy throws error', async () => { - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + for (const useDataStreamForAlerts of [false, true]) { + const label = useDataStreamForAlerts ? 'data streams' : 'aliases'; + const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts }); + + describe(`using ${label} for alert indices`, () => { + describe('AlertsService()', () => { + test('should correctly initialize common resources', async () => { + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + expect(alertsService.isInitialized()).toEqual(true); + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + if (!useDataStreamForAlerts) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(3); + + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + }); - expect(alertsService.isInitialized()).toEqual(false); + test('should log error and set initialized to false if adding ILM policy throws error', async () => { + if (useDataStreamForAlerts) return; - expect(logger.error).toHaveBeenCalledWith( - `Error installing ILM policy .alerts-ilm-policy - fail` - ); + clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - }); + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - test('should log error and set initialized to false if creating/updating common component template throws error', async () => { - clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(new Error('fail')); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + expect(alertsService.isInitialized()).toEqual(false); - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + expect(logger.error).toHaveBeenCalledWith( + `Error installing ILM policy .alerts-ilm-policy - fail` + ); - expect(alertsService.isInitialized()).toEqual(false); - expect(logger.error).toHaveBeenCalledWith( - `Error installing component template .alerts-framework-mappings - fail` - ); + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); + }); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - }); + test('should log error and set initialized to false if creating/updating common component template throws error', async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(new Error('fail')); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + expect(logger.error).toHaveBeenCalledWith( + `Error installing component template .alerts-framework-mappings - fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + }); - test('should update index template field limit and retry initialization if creating/updating common component template fails with field limit error', async () => { - clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce( - new EsErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: 400, - body: { - error: { - root_cause: [ - { + test('should update index template field limit and retry initialization if creating/updating common component template fails with field limit error', async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce( + new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 400, + body: { + error: { + root_cause: [ + { + type: 'illegal_argument_exception', + reason: + 'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged', + }, + ], type: 'illegal_argument_exception', reason: 'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged', - }, - ], - type: 'illegal_argument_exception', - reason: - 'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged', - caused_by: { - type: 'illegal_argument_exception', - reason: - 'composable template [.alerts-security.alerts-default-index-template] template after composition with component templates [.alerts-ecs-mappings, .alerts-security.alerts-mappings, .alerts-technical-mappings] is invalid', - caused_by: { - type: 'illegal_argument_exception', - reason: - 'invalid composite mappings for [.alerts-security.alerts-default-index-template]', caused_by: { type: 'illegal_argument_exception', - reason: 'Limit of total fields [1900] has been exceeded', + reason: + 'composable template [.alerts-security.alerts-default-index-template] template after composition with component templates [.alerts-ecs-mappings, .alerts-security.alerts-mappings, .alerts-technical-mappings] is invalid', + caused_by: { + type: 'illegal_argument_exception', + reason: + 'invalid composite mappings for [.alerts-security.alerts-default-index-template]', + caused_by: { + type: 'illegal_argument_exception', + reason: 'Limit of total fields [1900] has been exceeded', + }, + }, }, }, }, + }) + ) + ); + const existingIndexTemplate = { + name: 'test-template', + index_template: { + index_patterns: ['test*'], + composed_of: ['.alerts-framework-mappings'], + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-empty-default`, + }, + 'index.mapping.total_fields.limit': 1800, + }, + mappings: { + dynamic: false, + }, }, }, - }) - ) - ); - const existingIndexTemplate = { - name: 'test-template', - index_template: { - index_patterns: ['test*'], - composed_of: ['.alerts-framework-mappings'], - template: { - settings: { - auto_expand_replicas: '0-1', - hidden: true, - 'index.lifecycle': { - name: '.alerts-ilm-policy', - rollover_alias: `.alerts-empty-default`, + }; + clusterClient.indices.getIndexTemplate.mockResolvedValueOnce({ + index_templates: [existingIndexTemplate], + }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + expect(clusterClient.indices.getIndexTemplate).toHaveBeenCalledTimes(1); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(1); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({ + name: existingIndexTemplate.name, + body: { + ...existingIndexTemplate.index_template, + template: { + ...existingIndexTemplate.index_template.template, + settings: { + ...existingIndexTemplate.index_template.template?.settings, + 'index.mapping.total_fields.limit': 2500, + }, }, - 'index.mapping.total_fields.limit': 1800, - }, - mappings: { - dynamic: false, - }, - }, - }, - }; - clusterClient.indices.getIndexTemplate.mockResolvedValueOnce({ - index_templates: [existingIndexTemplate], - }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - expect(clusterClient.indices.getIndexTemplate).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({ - name: existingIndexTemplate.name, - body: { - ...existingIndexTemplate.index_template, - template: { - ...existingIndexTemplate.index_template.template, - settings: { - ...existingIndexTemplate.index_template.template?.settings, - 'index.mapping.total_fields.limit': 2500, }, - }, - }, - }); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - // 3x for framework, legacy-alert and ecs mappings, then 1 extra time to update component template - // after updating index template field limit - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - }); - }); - - describe('register()', () => { - let alertsService: AlertsService; - beforeEach(async () => { - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', + }); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + // 3x for framework, legacy-alert and ecs mappings, then 1 extra time to update component template + // after updating index template field limit + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + }); }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - }); - - test('should correctly install resources for context when common initialization is complete', async () => { - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; - expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - getIndexTemplatePutBody() - ); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); - }); + describe('register()', () => { + let alertsService: AlertsService; + beforeEach(async () => { + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + }); - test('should correctly install resources for context when useLegacyAlerts is true', async () => { - alertsService.register({ ...TestRegistrationContext, useLegacyAlerts: true }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; - expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - getIndexTemplatePutBody({ useLegacyAlerts: true }) - ); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); - }); + test('should correctly install resources for context when common initialization is complete', async () => { + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (!useDataStreamForAlerts) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; + expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + getIndexTemplatePutBody({ useDataStream: useDataStreamForAlerts }) + ); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalledWith({ + expand_wildcards: 'all', + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + } + }); - test('should correctly install resources for context when useEcs is true', async () => { - alertsService.register({ ...TestRegistrationContext, useEcs: true }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; - expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - getIndexTemplatePutBody({ useEcs: true }) - ); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); - }); + test('should correctly install resources for context when useLegacyAlerts is true', async () => { + alertsService.register({ ...TestRegistrationContext, useLegacyAlerts: true }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (!useDataStreamForAlerts) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; + expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + getIndexTemplatePutBody({ + useLegacyAlerts: true, + useDataStream: useDataStreamForAlerts, + }) + ); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalledWith({ + expand_wildcards: 'all', + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + } + }); - test('should correctly install resources for custom namespace on demand when isSpaceAware is true', async () => { - alertsService.register({ ...TestRegistrationContext, isSpaceAware: true }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( - 1, - getIndexTemplatePutBody() - ); - expect(clusterClient.indices.getAlias).toHaveBeenNthCalledWith(1, { - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenNthCalledWith(1, { - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); + test('should correctly install resources for context when useEcs is true', async () => { + alertsService.register({ ...TestRegistrationContext, useEcs: true }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (!useDataStreamForAlerts) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; + expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + getIndexTemplatePutBody({ useEcs: true, useDataStream: useDataStreamForAlerts }) + ); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenNthCalledWith(1, { + expand_wildcards: 'all', + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + } + }); - await retryUntil( - 'context in namespace initialized', - async () => - (await getContextInitialized( - alertsService, - TestRegistrationContext.context, - 'another-namespace' - )) === true - ); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( - 2, - getIndexTemplatePutBody({ namespace: 'another-namespace' }) - ); - expect(clusterClient.indices.getAlias).toHaveBeenNthCalledWith(2, { - index: '.internal.alerts-test.alerts-another-namespace-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.create).toHaveBeenNthCalledWith(2, { - index: '.internal.alerts-test.alerts-another-namespace-000001', - body: { - aliases: { - '.alerts-test.alerts-another-namespace': { - is_write_index: true, - }, - }, - }, - }); - }); + test('should correctly install resources for custom namespace on demand when isSpaceAware is true', async () => { + alertsService.register({ ...TestRegistrationContext, isSpaceAware: true }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (!useDataStreamForAlerts) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( + 1, + getIndexTemplatePutBody({ useDataStream: useDataStreamForAlerts }) + ); + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenNthCalledWith(1, { + expand_wildcards: 'all', + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenNthCalledWith(1, { + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenNthCalledWith(1, { + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + } + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + + clusterClient.indices.getDataStream.mockImplementationOnce(async () => ({ + data_streams: [], + })); + + await retryUntil( + 'context in namespace initialized', + async () => + (await getContextInitialized( + alertsService, + TestRegistrationContext.context, + 'another-namespace' + )) === true + ); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( + 2, + getIndexTemplatePutBody({ + namespace: 'another-namespace', + useDataStream: useDataStreamForAlerts, + }) + ); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 4 + ); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 4 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 4 + ); + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).toHaveBeenNthCalledWith(1, { + name: '.alerts-test.alerts-another-namespace', + }); + expect(clusterClient.indices.getDataStream).toHaveBeenNthCalledWith(2, { + expand_wildcards: 'all', + name: '.alerts-test.alerts-another-namespace', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenNthCalledWith(2, { + index: '.internal.alerts-test.alerts-another-namespace-000001', + body: { + aliases: { + '.alerts-test.alerts-another-namespace': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenNthCalledWith(2, { + index: '.internal.alerts-test.alerts-another-namespace-*', + name: '.alerts-test.alerts-*', + }); + } + }); - test('should correctly install resources for context when secondaryAlias is defined', async () => { - alertsService.register({ ...TestRegistrationContext, secondaryAlias: 'another.alias' }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; - expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - getIndexTemplatePutBody({ secondaryAlias: 'another.alias' }) - ); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-*', - name: '.alerts-test.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, + test('should correctly install resources for context when secondaryAlias is defined', async () => { + if (useDataStreamForAlerts) return; + + alertsService.register({ ...TestRegistrationContext, secondaryAlias: 'another.alias' }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + const componentTemplate4 = clusterClient.cluster.putComponentTemplate.mock.calls[3][0]; + expect(componentTemplate4.name).toEqual('.alerts-test.alerts-mappings'); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + getIndexTemplatePutBody({ + secondaryAlias: 'another.alias', + useDataStream: useDataStreamForAlerts, + }) + ); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-*', + name: '.alerts-test.alerts-*', + }); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, }, - }, - }, - }); - }); + }); + }); - test('should not install component template for context if fieldMap is empty', async () => { - alertsService.register({ - context: 'empty', - mappings: { fieldMap: {} }, - }); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService, 'empty')) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); - - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(3); - const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; - expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); - const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; - expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); - const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; - expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); - - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({ - name: `.alerts-empty.alerts-default-index-template`, - body: { - index_patterns: [`.internal.alerts-empty.alerts-default-*`], - composed_of: ['.alerts-framework-mappings'], - priority: 7, - template: { - settings: { - auto_expand_replicas: '0-1', - hidden: true, - 'index.lifecycle': { - name: '.alerts-ilm-policy', - rollover_alias: `.alerts-empty.alerts-default`, + test('should not install component template for context if fieldMap is empty', async () => { + alertsService.register({ + context: 'empty', + mappings: { fieldMap: {} }, + }); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService, 'empty')) === true + ); + + if (!useDataStreamForAlerts) { + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith(IlmPutBody); + } else { + expect(clusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + } + + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(3); + const componentTemplate1 = clusterClient.cluster.putComponentTemplate.mock.calls[0][0]; + expect(componentTemplate1.name).toEqual('.alerts-framework-mappings'); + const componentTemplate2 = clusterClient.cluster.putComponentTemplate.mock.calls[1][0]; + expect(componentTemplate2.name).toEqual('.alerts-legacy-alert-mappings'); + const componentTemplate3 = clusterClient.cluster.putComponentTemplate.mock.calls[2][0]; + expect(componentTemplate3.name).toEqual('.alerts-ecs-mappings'); + + const template = { + name: `.alerts-empty.alerts-default-index-template`, + body: { + index_patterns: [ + useDataStreamForAlerts + ? `.alerts-empty.alerts-default` + : `.internal.alerts-empty.alerts-default-*`, + ], + composed_of: ['.alerts-framework-mappings'], + ...(useDataStreamForAlerts ? { data_stream: { hidden: true } } : {}), + priority: 7, + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + ...(useDataStreamForAlerts + ? {} + : { + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-empty.alerts-default`, + }, + }), + 'index.mapping.total_fields.limit': 2500, + }, + mappings: { + _meta: { + kibana: { version: '8.8.0' }, + managed: true, + namespace: 'default', + }, + dynamic: false, + }, }, - 'index.mapping.total_fields.limit': 2500, - }, - mappings: { _meta: { kibana: { version: '8.8.0' }, managed: true, namespace: 'default', }, - dynamic: false, - }, - }, - _meta: { - kibana: { version: '8.8.0' }, - managed: true, - namespace: 'default', - }, - }, - }); - expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ - index: '.internal.alerts-empty.alerts-default-*', - name: '.alerts-empty.alerts-*', - }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-empty.alerts-default-000001', - body: { - aliases: { - '.alerts-empty.alerts-default': { - is_write_index: true, }, - }, - }, - }); - }); + }; + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith(template); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalledWith({}); + expect(clusterClient.indices.getDataStream).toHaveBeenCalledWith({ + expand_wildcards: 'all', + name: '.alerts-empty.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-empty.alerts-default-000001', + body: { + aliases: { + '.alerts-empty.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + expect(clusterClient.indices.getAlias).toHaveBeenCalledWith({ + index: '.internal.alerts-empty.alerts-default-*', + name: '.alerts-empty.alerts-*', + }); + } + + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 1 : 2 + ); + }); - test('should skip initialization if context already exists', async () => { - alertsService.register(TestRegistrationContext); - alertsService.register(TestRegistrationContext); + test('should skip initialization if context already exists', async () => { + alertsService.register(TestRegistrationContext); + alertsService.register(TestRegistrationContext); - expect(logger.debug).toHaveBeenCalledWith( - `Resources for context "test" have already been registered.` - ); - }); + expect(logger.debug).toHaveBeenCalledWith( + `Resources for context "test" have already been registered.` + ); + }); - test('should throw error if context already exists and has been registered with a different field map', async () => { - alertsService.register(TestRegistrationContext); - expect(() => { - alertsService.register({ - ...TestRegistrationContext, - mappings: { fieldMap: { anotherField: { type: 'keyword', required: false } } }, + test('should throw error if context already exists and has been registered with a different field map', async () => { + alertsService.register(TestRegistrationContext); + expect(() => { + alertsService.register({ + ...TestRegistrationContext, + mappings: { fieldMap: { anotherField: { type: 'keyword', required: false } } }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"test has already been registered with different options"` + ); }); - }).toThrowErrorMatchingInlineSnapshot( - `"test has already been registered with different options"` - ); - }); - test('should throw error if context already exists and has been registered with a different options', async () => { - alertsService.register(TestRegistrationContext); - expect(() => { - alertsService.register({ - ...TestRegistrationContext, - useEcs: true, + test('should throw error if context already exists and has been registered with a different options', async () => { + alertsService.register(TestRegistrationContext); + expect(() => { + alertsService.register({ + ...TestRegistrationContext, + useEcs: true, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"test has already been registered with different options"` + ); }); - }).toThrowErrorMatchingInlineSnapshot( - `"test has already been registered with different options"` - ); - }); - test('should not update index template if simulating template throws error', async () => { - clusterClient.indices.simulateTemplate.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(logger.error).toHaveBeenCalledWith( - `Failed to simulate index template mappings for .alerts-test.alerts-default-index-template; not applying mappings - fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - // putIndexTemplate is skipped but other operations are called as expected - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); + test('should not update index template if simulating template throws error', async () => { + clusterClient.indices.simulateTemplate.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(logger.error).toHaveBeenCalledWith( + `Failed to simulate index template mappings for .alerts-test.alerts-default-index-template; not applying mappings - fail`, + expect.any(Error) + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + // putIndexTemplate is skipped but other operations are called as expected + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('should log error and set initialized to false if simulating template returns empty mappings', async () => { - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ - result: false, - error: - 'Failure during installation. No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping', - }); + test('should log error and set initialized to false if simulating template returns empty mappings', async () => { + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ + result: false, + error: + 'Failure during installation. No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping', + }); + + expect(logger.error).toHaveBeenCalledWith( + new Error( + `No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping` + ) + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + }); - expect(logger.error).toHaveBeenCalledWith( - new Error( - `No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping` - ) - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('should log error and set initialized to false if updating index template throws error', async () => { + clusterClient.indices.putIndexTemplate.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + expect(logger.error).toHaveBeenCalledWith( + `Error installing index template .alerts-test.alerts-default-index-template - fail`, + expect.any(Error) + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + }); - test('should log error and set initialized to false if updating index template throws error', async () => { - clusterClient.indices.putIndexTemplate.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith( - `Error installing index template .alerts-test.alerts-default-index-template - fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('should log error and set initialized to false if checking for concrete write index throws error', async () => { + clusterClient.indices.getAlias.mockRejectedValueOnce(new Error('fail')); + clusterClient.indices.getDataStream.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + expect(logger.error).toHaveBeenCalledWith( + useDataStreamForAlerts + ? `Error fetching data stream for .alerts-test.alerts-default - fail` + : `Error fetching concrete indices for .internal.alerts-test.alerts-default-* pattern - fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + }); - test('should log error and set initialized to false if checking for concrete write index throws error', async () => { - clusterClient.indices.getAlias.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith( - `Error fetching concrete indices for .internal.alerts-test.alerts-default-* pattern - fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('should not throw error if checking for concrete write index throws 404', async () => { + const error = new Error(`index doesn't exist`) as HTTPError; + error.statusCode = 404; + clusterClient.indices.getAlias.mockRejectedValueOnce(error); + clusterClient.indices.getDataStream.mockRejectedValueOnce(error); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + } + }); - test('should not throw error if checking for concrete write index throws 404', async () => { - const error = new Error(`index doesn't exist`) as HTTPError; - error.statusCode = 404; - clusterClient.indices.getAlias.mockRejectedValueOnce(error); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); + test('should log error and set initialized to false if updating index settings for existing indices throws error', async () => { + clusterClient.indices.putSettings.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + expect(logger.error).toHaveBeenCalledWith( + useDataStreamForAlerts + ? `Failed to PUT index.mapping.total_fields.limit settings for .alerts-test.alerts-default: fail` + : `Failed to PUT index.mapping.total_fields.limit settings for alias_1: fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('should log error and set initialized to false if updating index settings for existing indices throws error', async () => { - clusterClient.indices.putSettings.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith( - `Failed to PUT index.mapping.total_fields.limit settings for alias alias_1: fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('should skip updating index mapping for existing indices if simulate index template throws error', async () => { + clusterClient.indices.simulateIndexTemplate.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(logger.error).toHaveBeenCalledWith( + useDataStreamForAlerts + ? `Ignored PUT mappings for .alerts-test.alerts-default; error generating simulated mappings: fail` + : `Ignored PUT mappings for alias_1; error generating simulated mappings: fail` + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + + // this is called to update backing indices, so not used with data streams + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + } + }); - test('should skip updating index mapping for existing indices if simulate index template throws error', async () => { - clusterClient.indices.simulateIndexTemplate.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(logger.error).toHaveBeenCalledWith( - `Ignored PUT mappings for alias alias_1; error generating simulated mappings: fail` - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); + test('should log error and set initialized to false if updating index mappings for existing indices throws error', async () => { + clusterClient.indices.putMapping.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + if (useDataStreamForAlerts) { + expect(logger.error).toHaveBeenCalledWith( + `Failed to PUT mapping for .alerts-test.alerts-default: fail` + ); + } else { + expect(logger.error).toHaveBeenCalledWith(`Failed to PUT mapping for alias_1: fail`); + } + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('should log error and set initialized to false if updating index mappings for existing indices throws error', async () => { - clusterClient.indices.putMapping.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith(`Failed to PUT mapping for alias alias_1: fail`); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('does not updating settings or mappings if no existing concrete indices', async () => { + clusterClient.indices.getAlias.mockImplementationOnce(async () => ({})); + clusterClient.indices.getDataStream.mockImplementationOnce(async () => ({ + data_streams: [], + })); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('does not updating settings or mappings if no existing concrete indices', async () => { - clusterClient.indices.getAlias.mockImplementationOnce(async () => ({})); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); + test('should log error and set initialized to false if concrete indices exist but none are write index', async () => { + // not applicable for data streams + if (useDataStreamForAlerts) return; - test('should log error and set initialized to false if concrete indices exist but none are write index', async () => { - clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-0001': { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: false, - is_hidden: true, - }, - alias_2: { - is_write_index: false, - is_hidden: true, + clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-0001': { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: false, + is_hidden: true, + }, + alias_2: { + is_write_index: false, + is_hidden: true, + }, + }, }, - }, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ - error: - 'Failure during installation. Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default', - result: false, - }); + })); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ + error: + 'Failure during installation. Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default', + result: false, + }); + + expect(logger.error).toHaveBeenCalledWith( + new Error( + `Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default` + ) + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + }); - expect(logger.error).toHaveBeenCalledWith( - new Error( - `Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default` - ) - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); + test('does not create new index if concrete write index exists', async () => { + // not applicable for data streams + if (useDataStreamForAlerts) return; - test('does not create new index if concrete write index exists', async () => { - clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-0001': { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - is_hidden: true, - }, - alias_2: { - is_write_index: false, - is_hidden: true, + clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-0001': { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + is_hidden: true, + }, + alias_2: { + is_write_index: false, + is_hidden: true, + }, + }, }, - }, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - }); - - test('should log error and set initialized to false if create concrete index throws error', async () => { - clusterClient.indices.create.mockRejectedValueOnce(new Error('fail')); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ error: 'Failure during installation. fail', result: false }); - - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); - - test('should not throw error if create concrete index throws resource_already_exists_exception error and write index already exists', async () => { - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.get).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); - - test('should log error and set initialized to false if create concrete index throws resource_already_exists_exception error and write index does not already exists', async () => { - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, - }, - })); - - alertsService.register(TestRegistrationContext); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - expect( - await alertsService.getContextInitializationPromise( - TestRegistrationContext.context, - DEFAULT_NAMESPACE_STRING - ) - ).toEqual({ - error: - 'Failure during installation. Attempted to create index: .internal.alerts-test.alerts-default-000001 as the write index for alias: .alerts-test.alerts-default, but the index already exists and is not the write index for the alias', - result: false, - }); - - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.putMapping).toHaveBeenCalled(); - expect(clusterClient.indices.get).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - }); - }); - - describe('createAlertsClient()', () => { - let alertsService: AlertsService; - beforeEach(async () => { - (AlertsClient as jest.Mock).mockImplementation(() => alertsClient); - }); - - test('should create new AlertsClient', async () => { - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', - }); - }); - - test('should return null if rule type has no alert definition', async () => { - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - const result = await alertsService.createAlertsClient({ - logger, - ruleType, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(result).toBe(null); - expect(AlertsClient).not.toHaveBeenCalled(); - }); - - test('should retry initializing common resources if common resource initialization failed', async () => { - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - - expect(alertsService.isInitialized()).toEqual(false); - - // Installing ILM policy failed so no calls to install context-specific resources - // should be made - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', - }); - - expect(result).not.toBe(null); - expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.info).toHaveBeenCalledWith( - `Resource installation for "test" succeeded after retry` - ); - }); + })); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - test('should not retry initializing common resources if common resource initialization is in progress', async () => { - // this is the initial call that fails - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); + test('should log error and set initialized to false if create concrete index throws error', async () => { + // not applicable for data streams + if (useDataStreamForAlerts) return; + + clusterClient.indices.create.mockRejectedValueOnce(new Error('fail')); + clusterClient.indices.createDataStream.mockRejectedValueOnce(new Error('fail')); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ error: 'Failure during installation. fail', result: false }); + + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + }); - // this is the retry call that we'll artificially inflate the duration of - clusterClient.ilm.putLifecycle.mockImplementationOnce(async () => { - await new Promise((r) => setTimeout(r, 1000)); - return { acknowledged: true }; - }); + test('should not throw error if create concrete index throws resource_already_exists_exception error and write index already exists', async () => { + // not applicable for data streams + if (useDataStreamForAlerts) return; - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - - expect(alertsService.isInitialized()).toEqual(false); - - // Installing ILM policy failed so no calls to install context-specific resources - // should be made - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - // call createAlertsClient at the same time which will trigger the retries - const result = await Promise.all([ - alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }), - alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }), - ]); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).toHaveBeenCalled(); - expect(clusterClient.indices.create).toHaveBeenCalled(); - - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', - }); - - expect(result[0]).not.toBe(null); - expect(result[1]).not.toBe(null); - expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.info).toHaveBeenCalledWith( - `Resource installation for "test" succeeded after retry` - ); - expect(logger.info).toHaveBeenCalledWith( - `Skipped retrying common resource initialization because it is already being retried.` - ); - }); - - test('should retry initializing context specific resources if context specific resource initialization failed', async () => { - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', - }); - - expect(result).not.toBe(null); - expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.info).toHaveBeenCalledWith( - `Resource installation for "test" succeeded after retry` - ); - }); - - test('should not retry initializing context specific resources if context specific resource initialization is in progress', async () => { - // this is the initial call that fails - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); + })); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + expect(clusterClient.indices.get).toHaveBeenCalled(); + expect(clusterClient.indices.create).toHaveBeenCalled(); + }); - // this is the retry call that we'll artificially inflate the duration of - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => { - await new Promise((r) => setTimeout(r, 1000)); - return SimulateTemplateResponse; - }); + test('should log error and set initialized to false if create concrete index throws resource_already_exists_exception error and write index does not already exists', async () => { + // not applicable for data streams + if (useDataStreamForAlerts) return; - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - const createAlertsClientWithDelay = async (delayMs: number | null) => { - if (delayMs) { - await new Promise((r) => setTimeout(r, delayMs)); - } - - return await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, + }, + })); + + alertsService.register(TestRegistrationContext); + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + expect( + await alertsService.getContextInitializationPromise( + TestRegistrationContext.context, + DEFAULT_NAMESPACE_STRING + ) + ).toEqual({ + error: + 'Failure during installation. Attempted to create index: .internal.alerts-test.alerts-default-000001 as the write index for alias: .alerts-test.alerts-default, but the index already exists and is not the write index for the alias', + result: false, + }); + + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalled(); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalled(); + expect(clusterClient.indices.get).toHaveBeenCalled(); + expect(clusterClient.indices.create).toHaveBeenCalled(); }); - }; - - const result = await Promise.all([ - createAlertsClientWithDelay(null), - createAlertsClientWithDelay(1), - ]); - - expect(AlertsClient).toHaveBeenCalledTimes(2); - expect(AlertsClient).toHaveBeenCalledWith({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - kibanaVersion: '8.8.0', }); - expect(result[0]).not.toBe(null); - expect(result[1]).not.toBe(null); - expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); - - // Should only log the retry once because the second call should - // leverage the outcome of the first retry - expect( - logger.info.mock.calls.filter( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (calls: any[]) => calls[0] === `Retrying resource initialization for context "test"` - ).length - ).toEqual(1); - expect( - logger.info.mock.calls.filter( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (calls: any[]) => calls[0] === `Resource installation for "test" succeeded after retry` - ).length - ).toEqual(1); - }); + describe('createAlertsClient()', () => { + let alertsService: AlertsService; + beforeEach(async () => { + (AlertsClient as jest.Mock).mockImplementation(() => alertsClient); + }); - test('should throttle retries of initializing context specific resources', async () => { - // this is the initial call that fails - clusterClient.indices.simulateTemplate.mockImplementation(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); + test('should create new AlertsClient', async () => { + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - const createAlertsClientWithDelay = async (delayMs: number | null) => { - if (delayMs) { - await new Promise((r) => setTimeout(r, delayMs)); - } - - return await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + dataStreamAdapter, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, + kibanaVersion: '8.8.0', + }); }); - }; - - await Promise.all([ - createAlertsClientWithDelay(null), - createAlertsClientWithDelay(1), - createAlertsClientWithDelay(2), - ]); - - expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); - - // Should only log the retry once because the second and third retries should be throttled - expect( - logger.info.mock.calls.filter( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (calls: any[]) => calls[0] === `Retrying resource initialization for context "test"` - ).length - ).toEqual(1); - }); - - test('should return null if retrying common resources initialization fails again', async () => { - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail again')); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - - expect(alertsService.isInitialized()).toEqual(false); - - // Installing ILM policy failed so no calls to install context-specific resources - // should be made - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - expect(result).toBe(null); - expect(AlertsClient).not.toHaveBeenCalled(); - expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.warn).toHaveBeenCalledWith( - `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Original error: Failure during installation. fail; Error after retry: Failure during installation. fail again` - ); - }); - - test('should return null if retrying common resources initialization fails again with same error', async () => { - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - clusterClient.ilm.putLifecycle.mockRejectedValueOnce(new Error('fail')); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); - - expect(alertsService.isInitialized()).toEqual(false); - - // Installing ILM policy failed so no calls to install context-specific resources - // should be made - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(1); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); - expect(clusterClient.indices.create).not.toHaveBeenCalled(); - - expect(result).toBe(null); - expect(AlertsClient).not.toHaveBeenCalled(); - expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.warn).toHaveBeenCalledWith( - `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Retry failed with error: Failure during installation. fail` - ); - }); - - test('should return null if retrying context specific initialization fails again', async () => { - clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { - ...SimulateTemplateResponse.template, - mappings: {}, - }, - })); - clusterClient.indices.putIndexTemplate.mockRejectedValueOnce( - new Error('fail index template') - ); - - alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); - alertsService.register(TestRegistrationContext); - - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - const result = await alertsService.createAlertsClient({ - logger, - ruleType: ruleTypeWithAlertDefinition, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, - }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], - }, - }); - expect(AlertsClient).not.toHaveBeenCalled(); - expect(result).toBe(null); - expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); - expect(logger.info).toHaveBeenCalledWith( - `Retrying resource initialization for context "test"` - ); - expect(logger.warn).toHaveBeenCalledWith( - `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Original error: Failure during installation. No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping; Error after retry: Failure during installation. fail index template` - ); - }); - }); - - describe('retries', () => { - test('should retry adding ILM policy for transient ES errors', async () => { - clusterClient.ilm.putLifecycle - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should return null if rule type has no alert definition', async () => { + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + const result = await alertsService.createAlertsClient({ + logger, + ruleType, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(3); - }); + expect(result).toBe(null); + expect(AlertsClient).not.toHaveBeenCalled(); + }); - test('should retry adding component template for transient ES errors', async () => { - clusterClient.cluster.putComponentTemplate - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should retry initializing common resources if common resource initialization failed', async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(new Error('fail')); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + + // Installing ILM policy failed so no calls to install context-specific resources + // should be made + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 2 + ); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + dataStreamAdapter, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + kibanaVersion: '8.8.0', + }); + + expect(result).not.toBe(null); + expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.info).toHaveBeenCalledWith( + `Resource installation for "test" succeeded after retry` + ); + }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(5); - }); + test('should not retry initializing common resources if common resource initialization is in progress', async () => { + // this is the initial call that fails + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce(new Error('fail')); + + // this is the retry call that we'll artificially inflate the duration of + clusterClient.cluster.putComponentTemplate.mockImplementationOnce(async () => { + await new Promise((r) => setTimeout(r, 1000)); + return { acknowledged: true }; + }); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + + // Installing ILM policy failed so no calls to install context-specific resources + // should be made + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + // call createAlertsClient at the same time which will trigger the retries + const result = await Promise.all([ + alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }), + alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }), + ]); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 2 + ); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).toHaveBeenCalled(); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + expect(clusterClient.indices.getDataStream).toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).toHaveBeenCalled(); + } + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + dataStreamAdapter, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + kibanaVersion: '8.8.0', + }); + + expect(result[0]).not.toBe(null); + expect(result[1]).not.toBe(null); + expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.info).toHaveBeenCalledWith( + `Resource installation for "test" succeeded after retry` + ); + expect(logger.info).toHaveBeenCalledWith( + `Skipped retrying common resource initialization because it is already being retried.` + ); + }); - test('should retry updating index template for transient ES errors', async () => { - clusterClient.indices.putIndexTemplate - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should retry initializing context specific resources if context specific resource initialization failed', async () => { + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - expect(alertsService.isInitialized()).toEqual(true); + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + dataStreamAdapter, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + kibanaVersion: '8.8.0', + }); + + expect(result).not.toBe(null); + expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.info).toHaveBeenCalledWith( + `Resource installation for "test" succeeded after retry` + ); + }); - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); + test('should not retry initializing context specific resources if context specific resource initialization is in progress', async () => { + // this is the initial call that fails + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + + // this is the retry call that we'll artificially inflate the duration of + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => { + await new Promise((r) => setTimeout(r, 1000)); + return SimulateTemplateResponse; + }); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + const createAlertsClientWithDelay = async (delayMs: number | null) => { + if (delayMs) { + await new Promise((r) => setTimeout(r, delayMs)); + } - expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(3); - }); + return await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + }; + + const result = await Promise.all([ + createAlertsClientWithDelay(null), + createAlertsClientWithDelay(1), + ]); + + expect(AlertsClient).toHaveBeenCalledTimes(2); + expect(AlertsClient).toHaveBeenCalledWith({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + dataStreamAdapter, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + kibanaVersion: '8.8.0', + }); + + expect(result[0]).not.toBe(null); + expect(result[1]).not.toBe(null); + expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); + + // Should only log the retry once because the second call should + // leverage the outcome of the first retry + expect( + logger.info.mock.calls.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (calls: any[]) => calls[0] === `Retrying resource initialization for context "test"` + ).length + ).toEqual(1); + expect( + logger.info.mock.calls.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (calls: any[]) => + calls[0] === `Resource installation for "test" succeeded after retry` + ).length + ).toEqual(1); + }); - test('should retry updating index settings for existing indices for transient ES errors', async () => { - clusterClient.indices.putSettings - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should throttle retries of initializing context specific resources', async () => { + // this is the initial call that fails + clusterClient.indices.simulateTemplate.mockImplementation(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + const createAlertsClientWithDelay = async (delayMs: number | null) => { + if (delayMs) { + await new Promise((r) => setTimeout(r, delayMs)); + } - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); + return await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + }; + + await Promise.all([ + createAlertsClientWithDelay(null), + createAlertsClientWithDelay(1), + createAlertsClientWithDelay(2), + ]); + + expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); + + // Should only log the retry once because the second and third retries should be throttled + expect( + logger.info.mock.calls.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (calls: any[]) => calls[0] === `Retrying resource initialization for context "test"` + ).length + ).toEqual(1); + }); - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); + test('should return null if retrying common resources initialization fails again', async () => { + let failCount = 0; + clusterClient.cluster.putComponentTemplate.mockImplementation(() => { + throw new Error(`fail ${++failCount}`); + }); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + + // Installing ILM policy failed so no calls to install context-specific resources + // should be made + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 2 + ); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + expect(result).toBe(null); + expect(AlertsClient).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringMatching( + /There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Original error: Failure during installation\. fail \d+; Error after retry: Failure during installation\. fail \d+/ + ) + ); + }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(4); - }); + test('should return null if retrying common resources initialization fails again with same error', async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValue(new Error('fail')); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil('error log called', async () => logger.error.mock.calls.length > 0); + + expect(alertsService.isInitialized()).toEqual(false); + + // Installing component template failed so no calls to install context-specific resources + // should be made + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 2 + ); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(clusterClient.indices.putSettings).not.toHaveBeenCalled(); + expect(clusterClient.indices.create).not.toHaveBeenCalled(); + + expect(result).toBe(null); + expect(AlertsClient).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.warn).toHaveBeenCalledWith( + `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Retry failed with error: Failure during installation. fail` + ); + }); - test('should retry updating index mappings for existing indices for transient ES errors', async () => { - clusterClient.indices.putMapping - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', + test('should return null if retrying context specific initialization fails again', async () => { + clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { + ...SimulateTemplateResponse.template, + mappings: {}, + }, + })); + clusterClient.indices.putIndexTemplate.mockRejectedValueOnce( + new Error('fail index template') + ); + + alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + alertsService.register(TestRegistrationContext); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + const result = await alertsService.createAlertsClient({ + logger, + ruleType: ruleTypeWithAlertDefinition, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + + expect(AlertsClient).not.toHaveBeenCalled(); + expect(result).toBe(null); + expect(logger.info).not.toHaveBeenCalledWith(`Retrying common resource initialization`); + expect(logger.info).toHaveBeenCalledWith( + `Retrying resource initialization for context "test"` + ); + expect(logger.warn).toHaveBeenCalledWith( + `There was an error in the framework installing namespace-level resources and creating concrete indices for context "test" - Original error: Failure during installation. No mappings would be generated for .alerts-test.alerts-default-index-template, possibly due to failed/misconfigured bootstrapping; Error after retry: Failure during installation. fail index template` + ); + }); }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); - - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); - - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(4); - }); + describe('retries', () => { + test('should retry adding ILM policy for transient ES errors', async () => { + if (useDataStreamForAlerts) return; + + clusterClient.ilm.putLifecycle + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(3); + }); - test('should retry creating concrete index for transient ES errors', async () => { - clusterClient.indices.create - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ index: 'index', shards_acknowledged: true, acknowledged: true }); - const alertsService = new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - }); + test('should retry adding component template for transient ES errors', async () => { + clusterClient.cluster.putComponentTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(5); + }); - await retryUntil( - 'alert service initialized', - async () => alertsService.isInitialized() === true - ); + test('should retry updating index template for transient ES errors', async () => { + clusterClient.indices.putIndexTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + expect(alertsService.isInitialized()).toEqual(true); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(3); + }); - alertsService.register(TestRegistrationContext); - await retryUntil( - 'context initialized', - async () => (await getContextInitialized(alertsService)) === true - ); + test('should retry updating index settings for existing indices for transient ES errors', async () => { + clusterClient.indices.putSettings + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(3); + } else { + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(4); + } + }); - expect(clusterClient.indices.create).toHaveBeenCalledTimes(3); - }); - }); + test('should retry updating index mappings for existing indices for transient ES errors', async () => { + clusterClient.indices.putMapping + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 3 : 4 + ); + }); - describe('timeout', () => { - test('should short circuit initialization if timeout exceeded', async () => { - clusterClient.ilm.putLifecycle.mockImplementationOnce(async () => { - await new Promise((resolve) => setTimeout(resolve, 20)); - return { acknowledged: true }; - }); - new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - timeoutMs: 10, + test('should retry creating concrete index for transient ES errors', async () => { + clusterClient.indices.getDataStream.mockImplementationOnce(async () => ({ + data_streams: [], + })); + clusterClient.indices.createDataStream + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + clusterClient.indices.create + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ index: 'index', shards_acknowledged: true, acknowledged: true }); + const alertsService = new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + dataStreamAdapter, + }); + + await retryUntil( + 'alert service initialized', + async () => alertsService.isInitialized() === true + ); + + alertsService.register(TestRegistrationContext); + await retryUntil( + 'context initialized', + async () => (await getContextInitialized(alertsService)) === true + ); + + if (useDataStreamForAlerts) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledTimes(3); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledTimes(3); + } + }); }); - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect(logger.error).toHaveBeenCalledWith(new Error(`Timeout: it took more than 10ms`)); - }); + describe('timeout', () => { + test('should short circuit initialization if timeout exceeded', async () => { + clusterClient.cluster.putComponentTemplate.mockImplementationOnce(async () => { + await new Promise((resolve) => setTimeout(resolve, 20)); + return { acknowledged: true }; + }); + new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + timeoutMs: 10, + dataStreamAdapter, + }); + + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect(logger.error).toHaveBeenCalledWith(new Error(`Timeout: it took more than 10ms`)); + }); - test('should short circuit initialization if pluginStop$ signal received but not throw error', async () => { - pluginStop$.next(); - new AlertsService({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - pluginStop$, - kibanaVersion: '8.8.0', - timeoutMs: 10, + test('should short circuit initialization if pluginStop$ signal received but not throw error', async () => { + pluginStop$.next(); + new AlertsService({ + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + pluginStop$, + kibanaVersion: '8.8.0', + timeoutMs: 10, + dataStreamAdapter, + }); + + await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); + + expect(logger.error).toHaveBeenCalledWith( + new Error(`Server is stopping; must stop all async operations`) + ); + }); }); - - await retryUntil('error logger called', async () => logger.error.mock.calls.length > 0); - - expect(logger.error).toHaveBeenCalledWith( - new Error(`Server is stopping; must stop all async operations`) - ); }); - }); + } }); diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts index e8ecab61e76d9..d0c9474389ef0 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts @@ -19,7 +19,13 @@ import { getComponentTemplateName, getIndexTemplateAndPattern, } from './resource_installer_utils'; -import { AlertInstanceContext, AlertInstanceState, IRuleTypeAlerts, RuleAlertData } from '../types'; +import { + AlertInstanceContext, + AlertInstanceState, + IRuleTypeAlerts, + RuleAlertData, + DataStreamAdapter, +} from '../types'; import { createResourceInstallationHelper, errorResult, @@ -49,6 +55,7 @@ interface AlertsServiceParams { kibanaVersion: string; elasticsearchClientPromise: Promise; timeoutMs?: number; + dataStreamAdapter: DataStreamAdapter; } export interface CreateAlertsClientParams extends LegacyAlertsClientParams { @@ -114,10 +121,13 @@ export class AlertsService implements IAlertsService { private resourceInitializationHelper: ResourceInstallationHelper; private registeredContexts: Map = new Map(); private commonInitPromise: Promise; + private dataStreamAdapter: DataStreamAdapter; constructor(private readonly options: AlertsServiceParams) { this.initialized = false; + this.dataStreamAdapter = options.dataStreamAdapter; + // Kick off initialization of common assets and save the promise this.commonInitPromise = this.initializeCommon(this.options.timeoutMs); @@ -221,6 +231,7 @@ export class AlertsService implements IAlertsService { namespace: opts.namespace, rule: opts.rule, kibanaVersion: this.options.kibanaVersion, + dataStreamAdapter: this.dataStreamAdapter, }); } @@ -296,6 +307,7 @@ export class AlertsService implements IAlertsService { esClient, name: DEFAULT_ALERTS_ILM_POLICY_NAME, policy: DEFAULT_ALERTS_ILM_POLICY, + dataStreamAdapter: this.dataStreamAdapter, }), () => createOrUpdateComponentTemplate({ @@ -421,6 +433,7 @@ export class AlertsService implements IAlertsService { kibanaVersion: this.options.kibanaVersion, namespace, totalFieldsLimit: TOTAL_FIELDS_LIMIT, + dataStreamAdapter: this.dataStreamAdapter, }), }), async () => @@ -429,6 +442,7 @@ export class AlertsService implements IAlertsService { esClient, totalFieldsLimit: TOTAL_FIELDS_LIMIT, indexPatterns: indexTemplateAndPattern, + dataStreamAdapter: this.dataStreamAdapter, }), ]); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts index a4cb6a26d3767..e2ee309b123f5 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts @@ -6,7 +6,9 @@ */ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { errors as EsErrors } from '@elastic/elasticsearch'; +import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { createConcreteWriteIndex } from './create_concrete_write_index'; +import { getDataStreamAdapter } from './data_stream_adapter'; const randomDelayMultiplier = 0.01; const logger = loggingSystemMock.createLogger(); @@ -36,6 +38,10 @@ const GetAliasResponse = { }, }; +const GetDataStreamResponse = { + data_streams: ['any-content-here-means-already-exists'], +} as unknown as IndicesGetDataStreamResponse; + const SimulateTemplateResponse = { template: { aliases: { @@ -60,483 +66,609 @@ const IndexPatterns = { }; describe('createConcreteWriteIndex', () => { - beforeEach(() => { - jest.resetAllMocks(); - jest.spyOn(global.Math, 'random').mockReturnValue(randomDelayMultiplier); - }); - - it(`should call esClient to put index template`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + for (const useDataStream of [false, true]) { + const label = useDataStream ? 'data streams' : 'aliases'; + const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: useDataStream }); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(global.Math, 'random').mockReturnValue(randomDelayMultiplier); }); - }); - - it(`should retry on transient ES errors`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.create - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ - index: '.internal.alerts-test.alerts-default-000001', - shards_acknowledged: true, - acknowledged: true, + + describe(`using ${label} for alert indices`, () => { + it(`should call esClient to put index template`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledWith({ + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } }); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); - expect(clusterClient.indices.create).toHaveBeenCalledTimes(3); - }); - - it(`should log and throw error if max retries exceeded`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.create.mockRejectedValue(new EsErrors.ConnectionError('foo')); - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); - - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - foo`); - expect(clusterClient.indices.create).toHaveBeenCalledTimes(4); - }); - - it(`should log and throw error if ES throws error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.create.mockRejectedValueOnce(new Error('generic error')); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); - - expect(logger.error).toHaveBeenCalledWith( - `Error creating concrete write index - generic error` - ); - }); - - it(`should log and return if ES throws resource_already_exists_exception error and existing index is already write index`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, - }, - })); + it(`should retry on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.create + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ + index: '.internal.alerts-test.alerts-default-000001', + shards_acknowledged: true, + acknowledged: true, + }); + clusterClient.indices.createDataStream + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ + acknowledged: true, + }); + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledTimes(3); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledTimes(3); + } + }); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and throw error if max retries exceeded`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.create.mockRejectedValue(new EsErrors.ConnectionError('foo')); + clusterClient.indices.createDataStream.mockRejectedValue( + new EsErrors.ConnectionError('foo') + ); + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Error creating data stream .alerts-test.alerts-default - foo` + : `Error creating concrete write index - foo` + ); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledTimes(4); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledTimes(4); + } + }); - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - }); - - it(`should retry getting index on transient ES error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, - }, - })); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and throw error if ES throws error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.create.mockRejectedValueOnce(new Error('generic error')); + clusterClient.indices.createDataStream.mockRejectedValueOnce(new Error('generic error')); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Error creating data stream .alerts-test.alerts-default - generic error` + : `Error creating concrete write index - generic error` + ); + }); - expect(clusterClient.indices.get).toHaveBeenCalledTimes(3); - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - }); - - it(`should log and throw error if ES throws resource_already_exists_exception error and existing index is not the write index`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => ({})); - const error = new Error(`fail`) as EsError; - error.meta = { - body: { - error: { - type: 'resource_already_exists_exception', - }, - }, - }; - clusterClient.indices.create.mockRejectedValueOnce(error); - clusterClient.indices.get.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, - }, - })); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Attempted to create index: .internal.alerts-test.alerts-default-000001 as the write index for alias: .alerts-test.alerts-default, but the index already exists and is not the write index for the alias"` - ); - expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); - }); - - it(`should call esClient to put index template if get alias throws 404`, async () => { - const error = new Error(`not found`) as EsError; - error.statusCode = 404; - clusterClient.indices.getAlias.mockRejectedValueOnce(error); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and return if ES throws resource_already_exists_exception error and existing index is already write index`, async () => { + if (useDataStream) return; - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, }, - }, - }, - }); - }); - - it(`should log and throw error if get alias throws non-404 error`, async () => { - const error = new Error(`fatal error`) as EsError; - error.statusCode = 500; - clusterClient.indices.getAlias.mockRejectedValueOnce(error); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"fatal error"`); - expect(logger.error).toHaveBeenCalledWith( - `Error fetching concrete indices for .internal.alerts-test.alerts-default-* pattern - fatal error` - ); - }); - - it(`should update underlying settings and mappings of existing concrete indices if they exist`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, + }, + })); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + }); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, + it(`should retry getting index on transient ES error`, async () => { + if (useDataStream) return; + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + const error = new Error(`fail`) as EsError; + error.statusCode = 404; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, }, - }, - }, - }); + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, + }, + })); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.get).toHaveBeenCalledTimes(3); + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(2); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); - }); - - it(`should retry simulateIndexTemplate on transient ES errors`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockImplementation(async () => SimulateTemplateResponse); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and throw error if ES throws resource_already_exists_exception error and existing index is not the write index`, async () => { + if (useDataStream) return; - expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(4); - }); - - it(`should retry getting alias on transient ES errors`, async () => { - clusterClient.indices.getAlias - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, + }, + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, + }, + })); - expect(clusterClient.indices.getAlias).toHaveBeenCalledTimes(3); - }); - - it(`should retry settings update on transient ES errors`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putSettings - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + const ccwiPromise = createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(4); - }); - - it(`should log and throw error on settings update if max retries exceeded`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putSettings.mockRejectedValue(new EsErrors.ConnectionError('foo')); - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); - expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(7); - expect(logger.error).toHaveBeenCalledWith( - `Failed to PUT index.mapping.total_fields.limit settings for alias alias_1: foo` - ); - }); - - it(`should log and throw error on settings update if ES throws error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putSettings.mockRejectedValue(new Error('generic error')); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); - - expect(logger.error).toHaveBeenCalledWith( - `Failed to PUT index.mapping.total_fields.limit settings for alias alias_1: generic error` - ); - }); - - it(`should retry mappings update on transient ES errors`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putMapping - .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) - .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) - .mockResolvedValue({ acknowledged: true }); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + await expect(() => ccwiPromise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Attempted to create index: .internal.alerts-test.alerts-default-000001 as the write index for alias: .alerts-test.alerts-default, but the index already exists and is not the write index for the alias"` + ); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(4); - }); - - it(`should log and throw error on mappings update if max retries exceeded`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putMapping.mockRejectedValue(new EsErrors.ConnectionError('foo')); - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); - expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(7); - expect(logger.error).toHaveBeenCalledWith(`Failed to PUT mapping for alias alias_1: foo`); - }); - - it(`should log and throw error on mappings update if ES throws error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - clusterClient.indices.putMapping.mockRejectedValue(new Error('generic error')); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); - - expect(logger.error).toHaveBeenCalledWith( - `Failed to PUT mapping for alias alias_1: generic error` - ); - }); - - it(`should log and return when simulating updated mappings throws error`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockRejectedValueOnce(new Error('fail')); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + }); - expect(logger.error).toHaveBeenCalledWith( - `Ignored PUT mappings for alias alias_1; error generating simulated mappings: fail` - ); + it(`should call esClient to put index template if get alias throws 404`, async () => { + const error = new Error(`not found`) as EsError; + error.statusCode = 404; + clusterClient.indices.getAlias.mockRejectedValueOnce(error); + clusterClient.indices.getDataStream.mockRejectedValueOnce(error); + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).toHaveBeenCalledWith({ + name: '.alerts-test.alerts-default', + }); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } + }); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, - }, - }, - }, - }); - }); - - it(`should log and return when simulating updated mappings returns null`, async () => { - clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(async () => ({ - ...SimulateTemplateResponse, - template: { ...SimulateTemplateResponse.template, mappings: null }, - })); - - await createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }); + it(`should log and throw error if get alias throws non-404 error`, async () => { + const error = new Error(`fatal error`) as EsError; + error.statusCode = 500; + clusterClient.indices.getAlias.mockRejectedValueOnce(error); + clusterClient.indices.getDataStream.mockRejectedValueOnce(error); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"fatal error"`); + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Error fetching data stream for .alerts-test.alerts-default - fatal error` + : `Error fetching concrete indices for .internal.alerts-test.alerts-default-* pattern - fatal error` + ); + }); - expect(logger.error).toHaveBeenCalledWith( - `Ignored PUT mappings for alias alias_1; simulated mappings were empty` - ); + it(`should update underlying settings and mappings of existing concrete indices if they exist`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (!useDataStream) { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } + + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 1 : 2); + }); + + it(`should retry simulateIndexTemplate on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => SimulateTemplateResponse); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes( + useDataStream ? 3 : 4 + ); + }); + + it(`should retry getting alias on transient ES errors`, async () => { + clusterClient.indices.getAlias + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + if (useDataStream) { + expect(clusterClient.indices.getDataStream).toHaveBeenCalledTimes(3); + } else { + expect(clusterClient.indices.getAlias).toHaveBeenCalledTimes(3); + } + }); + + it(`should retry settings update on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putSettings + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 3 : 4); + }); + + it(`should log and throw error on settings update if max retries exceeded`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putSettings.mockRejectedValue(new EsErrors.ConnectionError('foo')); + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); + expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(useDataStream ? 4 : 7); + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT index.mapping.total_fields.limit settings for .alerts-test.alerts-default: foo` + : `Failed to PUT index.mapping.total_fields.limit settings for alias_1: foo` + ); + }); - expect(clusterClient.indices.create).toHaveBeenCalledWith({ - index: '.internal.alerts-test.alerts-default-000001', - body: { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: true, + it(`should log and throw error on settings update if ES throws error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putSettings.mockRejectedValue(new Error('generic error')); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT index.mapping.total_fields.limit settings for .alerts-test.alerts-default: generic error` + : `Failed to PUT index.mapping.total_fields.limit settings for alias_1: generic error` + ); + }); + + it(`should retry mappings update on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putMapping + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 3 : 4); + }); + + it(`should log and throw error on mappings update if max retries exceeded`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putMapping.mockRejectedValue(new EsErrors.ConnectionError('foo')); + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(useDataStream ? 4 : 7); + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT mapping for .alerts-test.alerts-default: foo` + : `Failed to PUT mapping for alias_1: foo` + ); + }); + + it(`should log and throw error on mappings update if ES throws error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putMapping.mockRejectedValue(new Error('generic error')); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Failed to PUT mapping for .alerts-test.alerts-default: generic error` + : `Failed to PUT mapping for alias_1: generic error` + ); + }); + + it(`should log and return when simulating updated mappings throws error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockRejectedValueOnce(new Error('fail')); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Ignored PUT mappings for .alerts-test.alerts-default; error generating simulated mappings: fail` + : `Ignored PUT mappings for alias_1; error generating simulated mappings: fail` + ); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } + }); + + it(`should log and return when simulating updated mappings returns null`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(async () => ({ + ...SimulateTemplateResponse, + template: { ...SimulateTemplateResponse.template, mappings: null }, + })); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(logger.error).toHaveBeenCalledWith( + useDataStream + ? `Ignored PUT mappings for .alerts-test.alerts-default; simulated mappings were empty` + : `Ignored PUT mappings for alias_1; simulated mappings were empty` + ); + + if (useDataStream) { + expect(clusterClient.indices.createDataStream).not.toHaveBeenCalled(); + } else { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ + index: '.internal.alerts-test.alerts-default-000001', + body: { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: true, + }, + }, + }, + }); + } + }); + + it(`should throw error when there are concrete indices but none of them are the write index`, async () => { + if (useDataStream) return; + + clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-0001': { + aliases: { + '.alerts-test.alerts-default': { + is_write_index: false, + is_hidden: true, + }, + alias_2: { + is_write_index: false, + is_hidden: true, + }, + }, }, - }, - }, + })); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + await expect(() => + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default"` + ); + }); }); - }); - - it(`should throw error when there are concrete indices but none of them are the write index`, async () => { - clusterClient.indices.getAlias.mockImplementationOnce(async () => ({ - '.internal.alerts-test.alerts-default-0001': { - aliases: { - '.alerts-test.alerts-default': { - is_write_index: false, - is_hidden: true, - }, - alias_2: { - is_write_index: false, - is_hidden: true, - }, - }, - }, - })); - clusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - - await expect(() => - createConcreteWriteIndex({ - logger, - esClient: clusterClient, - indexPatterns: IndexPatterns, - totalFieldsLimit: 2500, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Indices matching pattern .internal.alerts-test.alerts-default-* exist but none are set as the write index for alias .alerts-test.alerts-default"` - ); - }); + } }); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts index 31aface312913..8ad628e1b2905 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts @@ -10,8 +10,9 @@ import { Logger, ElasticsearchClient } from '@kbn/core/server'; import { get } from 'lodash'; import { IIndexPatternString } from '../resource_installer_utils'; import { retryTransientEsErrors } from './retry_transient_es_errors'; +import { DataStreamAdapter } from './data_stream_adapter'; -interface ConcreteIndexInfo { +export interface ConcreteIndexInfo { index: string; alias: string; isWriteIndex: boolean; @@ -50,7 +51,7 @@ const updateTotalFieldLimitSetting = async ({ return; } catch (err) { logger.error( - `Failed to PUT index.mapping.total_fields.limit settings for alias ${alias}: ${err.message}` + `Failed to PUT index.mapping.total_fields.limit settings for ${alias}: ${err.message}` ); throw err; } @@ -74,7 +75,7 @@ const updateUnderlyingMapping = async ({ ); } catch (err) { logger.error( - `Ignored PUT mappings for alias ${alias}; error generating simulated mappings: ${err.message}` + `Ignored PUT mappings for ${alias}; error generating simulated mappings: ${err.message}` ); return; } @@ -82,7 +83,7 @@ const updateUnderlyingMapping = async ({ const simulatedMapping = get(simulatedIndexMapping, ['template', 'mappings']); if (simulatedMapping == null) { - logger.error(`Ignored PUT mappings for alias ${alias}; simulated mappings were empty`); + logger.error(`Ignored PUT mappings for ${alias}; simulated mappings were empty`); return; } @@ -94,20 +95,22 @@ const updateUnderlyingMapping = async ({ return; } catch (err) { - logger.error(`Failed to PUT mapping for alias ${alias}: ${err.message}`); + logger.error(`Failed to PUT mapping for ${alias}: ${err.message}`); throw err; } }; /** * Updates the underlying mapping for any existing concrete indices */ -const updateIndexMappings = async ({ +export const updateIndexMappings = async ({ logger, esClient, totalFieldsLimit, concreteIndices, }: UpdateIndexMappingsOpts) => { - logger.debug(`Updating underlying mappings for ${concreteIndices.length} indices.`); + logger.debug( + `Updating underlying mappings for ${concreteIndices.length} indices / data streams.` + ); // Update total field limit setting of found indices // Other index setting changes are not updated at this time @@ -125,11 +128,12 @@ const updateIndexMappings = async ({ ); }; -interface CreateConcreteWriteIndexOpts { +export interface CreateConcreteWriteIndexOpts { logger: Logger; esClient: ElasticsearchClient; totalFieldsLimit: number; indexPatterns: IIndexPatternString; + dataStreamAdapter: DataStreamAdapter; } /** * Installs index template that uses installed component template @@ -137,107 +141,6 @@ interface CreateConcreteWriteIndexOpts { * conflicts. Simulate should return an empty mapping if a template * conflicts with an already installed template. */ -export const createConcreteWriteIndex = async ({ - logger, - esClient, - indexPatterns, - totalFieldsLimit, -}: CreateConcreteWriteIndexOpts) => { - logger.info(`Creating concrete write index - ${indexPatterns.name}`); - - // check if a concrete write index already exists - let concreteIndices: ConcreteIndexInfo[] = []; - try { - // Specify both the index pattern for the backing indices and their aliases - // The alias prevents the request from finding other namespaces that could match the -* pattern - const response = await retryTransientEsErrors( - () => - esClient.indices.getAlias({ - index: indexPatterns.pattern, - name: indexPatterns.basePattern, - }), - { logger } - ); - - concreteIndices = Object.entries(response).flatMap(([index, { aliases }]) => - Object.entries(aliases).map(([aliasName, aliasProperties]) => ({ - index, - alias: aliasName, - isWriteIndex: aliasProperties.is_write_index ?? false, - })) - ); - - logger.debug( - `Found ${concreteIndices.length} concrete indices for ${ - indexPatterns.name - } - ${JSON.stringify(concreteIndices)}` - ); - } catch (error) { - // 404 is expected if no concrete write indices have been created - if (error.statusCode !== 404) { - logger.error( - `Error fetching concrete indices for ${indexPatterns.pattern} pattern - ${error.message}` - ); - throw error; - } - } - - let concreteWriteIndicesExist = false; - // if a concrete write index already exists, update the underlying mapping - if (concreteIndices.length > 0) { - await updateIndexMappings({ logger, esClient, totalFieldsLimit, concreteIndices }); - - const concreteIndicesExist = concreteIndices.some( - (index) => index.alias === indexPatterns.alias - ); - concreteWriteIndicesExist = concreteIndices.some( - (index) => index.alias === indexPatterns.alias && index.isWriteIndex - ); - - // If there are some concrete indices but none of them are the write index, we'll throw an error - // because one of the existing indices should have been the write target. - if (concreteIndicesExist && !concreteWriteIndicesExist) { - throw new Error( - `Indices matching pattern ${indexPatterns.pattern} exist but none are set as the write index for alias ${indexPatterns.alias}` - ); - } - } - - // check if a concrete write index already exists - if (!concreteWriteIndicesExist) { - try { - await retryTransientEsErrors( - () => - esClient.indices.create({ - index: indexPatterns.name, - body: { - aliases: { - [indexPatterns.alias]: { - is_write_index: true, - }, - }, - }, - }), - { logger } - ); - } catch (error) { - logger.error(`Error creating concrete write index - ${error.message}`); - // If the index already exists and it's the write index for the alias, - // something else created it so suppress the error. If it's not the write - // index, that's bad, throw an error. - if (error?.meta?.body?.error?.type === 'resource_already_exists_exception') { - const existingIndices = await retryTransientEsErrors( - () => esClient.indices.get({ index: indexPatterns.name }), - { logger } - ); - if (!existingIndices[indexPatterns.name]?.aliases?.[indexPatterns.alias]?.is_write_index) { - throw Error( - `Attempted to create index: ${indexPatterns.name} as the write index for alias: ${indexPatterns.alias}, but the index already exists and is not the write index for the alias` - ); - } - } else { - throw error; - } - } - } +export const createConcreteWriteIndex = async (opts: CreateConcreteWriteIndexOpts) => { + await opts.dataStreamAdapter.createStream(opts); }; diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.test.ts index e47bc92eb5ae0..8cacdb1e97563 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.test.ts @@ -7,10 +7,12 @@ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { createOrUpdateIlmPolicy } from './create_or_update_ilm_policy'; +import { getDataStreamAdapter } from './data_stream_adapter'; const randomDelayMultiplier = 0.01; const logger = loggingSystemMock.createLogger(); const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; +const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: false }); const IlmPolicy = { _meta: { @@ -40,6 +42,7 @@ describe('createOrUpdateIlmPolicy', () => { esClient: clusterClient, name: 'test-policy', policy: IlmPolicy, + dataStreamAdapter, }); expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledWith({ @@ -58,6 +61,7 @@ describe('createOrUpdateIlmPolicy', () => { esClient: clusterClient, name: 'test-policy', policy: IlmPolicy, + dataStreamAdapter, }); expect(clusterClient.ilm.putLifecycle).toHaveBeenCalledTimes(3); @@ -71,6 +75,7 @@ describe('createOrUpdateIlmPolicy', () => { esClient: clusterClient, name: 'test-policy', policy: IlmPolicy, + dataStreamAdapter, }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); @@ -87,6 +92,7 @@ describe('createOrUpdateIlmPolicy', () => { esClient: clusterClient, name: 'test-policy', policy: IlmPolicy, + dataStreamAdapter, }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts index d1c50b7474436..dfc967aa974d6 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_ilm_policy.ts @@ -8,12 +8,14 @@ import { IlmPolicy } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Logger, ElasticsearchClient } from '@kbn/core/server'; import { retryTransientEsErrors } from './retry_transient_es_errors'; +import { DataStreamAdapter } from './data_stream_adapter'; interface CreateOrUpdateIlmPolicyOpts { logger: Logger; esClient: ElasticsearchClient; name: string; policy: IlmPolicy; + dataStreamAdapter: DataStreamAdapter; } /** * Creates ILM policy if it doesn't already exist, updates it if it does @@ -23,7 +25,10 @@ export const createOrUpdateIlmPolicy = async ({ esClient, name, policy, + dataStreamAdapter, }: CreateOrUpdateIlmPolicyOpts) => { + if (dataStreamAdapter.isUsingDataStreams()) return; + logger.info(`Installing ILM policy ${name}`); try { diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts index d4ce203a0d0e3..bf0ae8797eca5 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts @@ -7,12 +7,14 @@ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { getIndexTemplate, createOrUpdateIndexTemplate } from './create_or_update_index_template'; +import { createDataStreamAdapterMock } from './data_stream_adapter.mock'; +import { DataStreamAdapter } from './data_stream_adapter'; const randomDelayMultiplier = 0.01; const logger = loggingSystemMock.createLogger(); const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; -const IndexTemplate = (namespace: string = 'default') => ({ +const IndexTemplate = (namespace: string = 'default', useDataStream: boolean = false) => ({ name: `.alerts-test.alerts-${namespace}-index-template`, body: { _meta: { @@ -38,10 +40,14 @@ const IndexTemplate = (namespace: string = 'default') => ({ settings: { auto_expand_replicas: '0-1', hidden: true, - 'index.lifecycle': { - name: 'test-ilm-policy', - rollover_alias: `.alerts-test.alerts-${namespace}`, - }, + ...(useDataStream + ? {} + : { + 'index.lifecycle': { + name: 'test-ilm-policy', + rollover_alias: `.alerts-test.alerts-${namespace}`, + }, + }), 'index.mapping.total_fields.limit': 2500, }, }, @@ -65,7 +71,20 @@ const SimulateTemplateResponse = { }; describe('getIndexTemplate', () => { + let dataStreamAdapter: DataStreamAdapter; + let useDataStream: boolean; + + beforeEach(() => { + dataStreamAdapter = createDataStreamAdapterMock(); + useDataStream = dataStreamAdapter.isUsingDataStreams(); + }); + it(`should create index template with given parameters in default namespace`, () => { + dataStreamAdapter.getIndexTemplateFields = jest.fn().mockReturnValue({ + index_patterns: ['.internal.alerts-test.alerts-default-*'], + rollover_alias: '.alerts-test.alerts-default', + }); + expect( getIndexTemplate({ kibanaVersion: '8.6.1', @@ -80,11 +99,17 @@ describe('getIndexTemplate', () => { namespace: 'default', componentTemplateRefs: ['mappings1', 'framework-mappings'], totalFieldsLimit: 2500, + dataStreamAdapter, }) ).toEqual(IndexTemplate()); }); it(`should create index template with given parameters in custom namespace`, () => { + dataStreamAdapter.getIndexTemplateFields = jest.fn().mockReturnValue({ + index_patterns: ['.internal.alerts-test.alerts-another-space-*'], + rollover_alias: '.alerts-test.alerts-another-space', + }); + expect( getIndexTemplate({ kibanaVersion: '8.6.1', @@ -99,8 +124,9 @@ describe('getIndexTemplate', () => { namespace: 'another-space', componentTemplateRefs: ['mappings1', 'framework-mappings'], totalFieldsLimit: 2500, + dataStreamAdapter, }) - ).toEqual(IndexTemplate('another-space')); + ).toEqual(IndexTemplate('another-space', useDataStream)); }); }); @@ -164,7 +190,8 @@ describe('createOrUpdateIndexTemplate', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`"foo"`); expect(logger.error).toHaveBeenCalledWith( - `Error installing index template .alerts-test.alerts-default-index-template - foo` + `Error installing index template .alerts-test.alerts-default-index-template - foo`, + expect.any(Error) ); expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(4); }); @@ -182,7 +209,8 @@ describe('createOrUpdateIndexTemplate', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`"generic error"`); expect(logger.error).toHaveBeenCalledWith( - `Error installing index template .alerts-test.alerts-default-index-template - generic error` + `Error installing index template .alerts-test.alerts-default-index-template - generic error`, + expect.any(Error) ); }); @@ -197,7 +225,8 @@ describe('createOrUpdateIndexTemplate', () => { }); expect(logger.error).toHaveBeenCalledWith( - `Failed to simulate index template mappings for .alerts-test.alerts-default-index-template; not applying mappings - simulate error` + `Failed to simulate index template mappings for .alerts-test.alerts-default-index-template; not applying mappings - simulate error`, + expect.any(Error) ); expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts index a17fad2d875ed..30ee06a1ddda0 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts @@ -14,6 +14,7 @@ import { Logger, ElasticsearchClient } from '@kbn/core/server'; import { isEmpty } from 'lodash'; import { IIndexPatternString } from '../resource_installer_utils'; import { retryTransientEsErrors } from './retry_transient_es_errors'; +import { DataStreamAdapter } from './data_stream_adapter'; interface GetIndexTemplateOpts { componentTemplateRefs: string[]; @@ -22,6 +23,7 @@ interface GetIndexTemplateOpts { kibanaVersion: string; namespace: string; totalFieldsLimit: number; + dataStreamAdapter: DataStreamAdapter; } export const getIndexTemplate = ({ @@ -31,6 +33,7 @@ export const getIndexTemplate = ({ kibanaVersion, namespace, totalFieldsLimit, + dataStreamAdapter, }: GetIndexTemplateOpts): IndicesPutIndexTemplateRequest => { const indexMetadata: Metadata = { kibana: { @@ -40,19 +43,31 @@ export const getIndexTemplate = ({ namespace, }; + const dataStreamFields = dataStreamAdapter.getIndexTemplateFields( + indexPatterns.alias, + indexPatterns.pattern + ); + + const indexLifecycle = { + name: ilmPolicyName, + rollover_alias: dataStreamFields.rollover_alias, + }; + return { name: indexPatterns.template, body: { - index_patterns: [indexPatterns.pattern], + ...(dataStreamFields.data_stream ? { data_stream: dataStreamFields.data_stream } : {}), + index_patterns: dataStreamFields.index_patterns, composed_of: componentTemplateRefs, template: { settings: { auto_expand_replicas: '0-1', hidden: true, - 'index.lifecycle': { - name: ilmPolicyName, - rollover_alias: indexPatterns.alias, - }, + ...(dataStreamAdapter.isUsingDataStreams() + ? {} + : { + 'index.lifecycle': indexLifecycle, + }), 'index.mapping.total_fields.limit': totalFieldsLimit, }, mappings: { @@ -107,7 +122,8 @@ export const createOrUpdateIndexTemplate = async ({ mappings = simulateResponse.template.mappings; } catch (err) { logger.error( - `Failed to simulate index template mappings for ${template.name}; not applying mappings - ${err.message}` + `Failed to simulate index template mappings for ${template.name}; not applying mappings - ${err.message}`, + err ); return; } @@ -123,7 +139,7 @@ export const createOrUpdateIndexTemplate = async ({ logger, }); } catch (err) { - logger.error(`Error installing index template ${template.name} - ${err.message}`); + logger.error(`Error installing index template ${template.name} - ${err.message}`, err); throw err; } }; diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.mock.ts b/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.mock.ts new file mode 100644 index 0000000000000..8de9f7bcc1731 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataStreamAdapter, GetDataStreamAdapterOpts } from './data_stream_adapter'; + +export function createDataStreamAdapterMock(opts?: GetDataStreamAdapterOpts): DataStreamAdapter { + return { + isUsingDataStreams: jest.fn().mockReturnValue(false), + getIndexTemplateFields: jest.fn().mockReturnValue({ + index_patterns: ['index-pattern'], + }), + createStream: jest.fn(), + }; +} diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts b/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts new file mode 100644 index 0000000000000..21091464a68b1 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerts_service/lib/data_stream_adapter.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 max-classes-per-file +import { + CreateConcreteWriteIndexOpts, + ConcreteIndexInfo, + updateIndexMappings, +} from './create_concrete_write_index'; +import { retryTransientEsErrors } from './retry_transient_es_errors'; + +export interface DataStreamAdapter { + isUsingDataStreams(): boolean; + getIndexTemplateFields(alias: string, pattern: string): IndexTemplateFields; + createStream(opts: CreateConcreteWriteIndexOpts): Promise; +} + +export interface BulkOpProperties { + require_alias: boolean; +} + +export interface IndexTemplateFields { + data_stream?: { hidden: true }; + index_patterns: string[]; + rollover_alias?: string; +} + +export interface GetDataStreamAdapterOpts { + useDataStreamForAlerts: boolean; +} + +export function getDataStreamAdapter(opts: GetDataStreamAdapterOpts): DataStreamAdapter { + if (opts.useDataStreamForAlerts) { + return new DataStreamImplementation(); + } else { + return new AliasImplementation(); + } +} + +// implementation using data streams +class DataStreamImplementation implements DataStreamAdapter { + isUsingDataStreams(): boolean { + return true; + } + + getIndexTemplateFields(alias: string, pattern: string): IndexTemplateFields { + return { + data_stream: { hidden: true }, + index_patterns: [alias], + }; + } + + async createStream(opts: CreateConcreteWriteIndexOpts): Promise { + return createDataStream(opts); + } +} + +// implementation using aliases and backing indices +class AliasImplementation implements DataStreamAdapter { + isUsingDataStreams(): boolean { + return false; + } + + getIndexTemplateFields(alias: string, pattern: string): IndexTemplateFields { + return { + index_patterns: [pattern], + rollover_alias: alias, + }; + } + + async createStream(opts: CreateConcreteWriteIndexOpts): Promise { + return createAliasStream(opts); + } +} + +async function createDataStream(opts: CreateConcreteWriteIndexOpts): Promise { + const { logger, esClient, indexPatterns, totalFieldsLimit } = opts; + logger.info(`Creating data stream - ${indexPatterns.alias}`); + + // check if data stream exists + let dataStreamExists = false; + try { + const response = await retryTransientEsErrors( + () => esClient.indices.getDataStream({ name: indexPatterns.alias, expand_wildcards: 'all' }), + { logger } + ); + dataStreamExists = response.data_streams.length > 0; + } catch (error) { + if (error?.statusCode !== 404) { + logger.error(`Error fetching data stream for ${indexPatterns.alias} - ${error.message}`); + throw error; + } + } + + // if a data stream exists, update the underlying mapping + if (dataStreamExists) { + await updateIndexMappings({ + logger, + esClient, + totalFieldsLimit, + concreteIndices: [ + { alias: indexPatterns.alias, index: indexPatterns.alias, isWriteIndex: true }, + ], + }); + } else { + try { + await retryTransientEsErrors( + () => + esClient.indices.createDataStream({ + name: indexPatterns.alias, + }), + { logger } + ); + } catch (error) { + if (error?.meta?.body?.error?.type !== 'resource_already_exists_exception') { + logger.error(`Error creating data stream ${indexPatterns.alias} - ${error.message}`); + throw error; + } + } + } +} + +async function createAliasStream(opts: CreateConcreteWriteIndexOpts): Promise { + const { logger, esClient, indexPatterns, totalFieldsLimit } = opts; + logger.info(`Creating concrete write index - ${indexPatterns.name}`); + + // check if a concrete write index already exists + let concreteIndices: ConcreteIndexInfo[] = []; + try { + // Specify both the index pattern for the backing indices and their aliases + // The alias prevents the request from finding other namespaces that could match the -* pattern + const response = await retryTransientEsErrors( + () => + esClient.indices.getAlias({ + index: indexPatterns.pattern, + name: indexPatterns.basePattern, + }), + { logger } + ); + + concreteIndices = Object.entries(response).flatMap(([index, { aliases }]) => + Object.entries(aliases).map(([aliasName, aliasProperties]) => ({ + index, + alias: aliasName, + isWriteIndex: aliasProperties.is_write_index ?? false, + })) + ); + + logger.debug( + `Found ${concreteIndices.length} concrete indices for ${ + indexPatterns.name + } - ${JSON.stringify(concreteIndices)}` + ); + } catch (error) { + // 404 is expected if no concrete write indices have been created + if (error.statusCode !== 404) { + logger.error( + `Error fetching concrete indices for ${indexPatterns.pattern} pattern - ${error.message}` + ); + throw error; + } + } + + let concreteWriteIndicesExist = false; + // if a concrete write index already exists, update the underlying mapping + if (concreteIndices.length > 0) { + await updateIndexMappings({ logger, esClient, totalFieldsLimit, concreteIndices }); + + const concreteIndicesExist = concreteIndices.some( + (index) => index.alias === indexPatterns.alias + ); + concreteWriteIndicesExist = concreteIndices.some( + (index) => index.alias === indexPatterns.alias && index.isWriteIndex + ); + + // If there are some concrete indices but none of them are the write index, we'll throw an error + // because one of the existing indices should have been the write target. + if (concreteIndicesExist && !concreteWriteIndicesExist) { + throw new Error( + `Indices matching pattern ${indexPatterns.pattern} exist but none are set as the write index for alias ${indexPatterns.alias}` + ); + } + } + + // check if a concrete write index already exists + if (!concreteWriteIndicesExist) { + try { + await retryTransientEsErrors( + () => + esClient.indices.create({ + index: indexPatterns.name, + body: { + aliases: { + [indexPatterns.alias]: { + is_write_index: true, + }, + }, + }, + }), + { logger } + ); + } catch (error) { + logger.error(`Error creating concrete write index - ${error.message}`); + // If the index already exists and it's the write index for the alias, + // something else created it so suppress the error. If it's not the write + // index, that's bad, throw an error. + if (error?.meta?.body?.error?.type === 'resource_already_exists_exception') { + const existingIndices = await retryTransientEsErrors( + () => esClient.indices.get({ index: indexPatterns.name }), + { logger } + ); + if (!existingIndices[indexPatterns.name]?.aliases?.[indexPatterns.alias]?.is_write_index) { + throw Error( + `Attempted to create index: ${indexPatterns.name} as the write index for alias: ${indexPatterns.alias}, but the index already exists and is not the write index for the alias` + ); + } + } else { + throw error; + } + } + } +} diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index ebfe1586031fa..ae42c665d73cf 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -32,6 +32,7 @@ export type { ExecutorType, IRuleTypeAlerts, GetViewInAppRelativeUrlFnOpts, + DataStreamAdapter, } from './types'; export { RuleNotifyWhen } from '../common'; export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config'; @@ -64,6 +65,7 @@ export { createConcreteWriteIndex, installWithTimeout, } from './alerts_service'; +export { getDataStreamAdapter } from './alerts_service/lib/data_stream_adapter'; export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext); diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index 9aa7209ea5fa1..ab81e472f938b 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -35,6 +35,7 @@ const createSetupMock = () => { enabled: jest.fn(), getContextInitializationPromise: jest.fn(), }, + getDataStreamAdapter: jest.fn(), }; return mock; }; @@ -190,3 +191,5 @@ export const alertsMock = { export const ruleMonitoringServiceMock = { create: createRuleMonitoringServiceMock }; export const ruleLastRunServiceMock = { create: createRuleLastRunServiceMock }; + +export { createDataStreamAdapterMock } from './alerts_service/lib/data_stream_adapter.mock'; diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 859e69b6da131..b355ecbf370a5 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -36,30 +36,7 @@ jest.mock('./alerts_service/alerts_service', () => ({ })); import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; - -const generateAlertingConfig = (): AlertingConfig => ({ - healthCheck: { - interval: '5m', - }, - enableFrameworkAlerts: false, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 10, - cancelAlertsOnRuleTimeout: true, - rules: { - minimumScheduleInterval: { value: '1m', enforce: false }, - run: { - actions: { - max: 1000, - }, - alerts: { - max: 1000, - }, - }, - }, -}); +import { generateAlertingConfig } from './test_utils'; const sampleRuleType: RuleType = { id: 'test', @@ -78,329 +55,344 @@ const sampleRuleType: RuleType { - describe('setup()', () => { - const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); - const setupMocks = coreMock.createSetup(); - const mockPlugins = { - licensing: licensingMock.createSetup(), - encryptedSavedObjects: encryptedSavedObjectsSetup, - taskManager: taskManagerMock.createSetup(), - eventLog: eventLogServiceMock.create(), - actions: actionsMock.createSetup(), - statusService: statusServiceMock.createSetupContract(), - monitoringCollection: monitoringCollectionMock.createSetup(), - data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, - features: featuresPluginMock.createSetup(), - unifiedSearch: autocompletePluginMock.createSetupContract(), - }; - - let plugin: AlertingPlugin; - - beforeEach(() => jest.clearAllMocks()); - - it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => { - const context = coreMock.createPluginInitializerContext( - generateAlertingConfig() - ); - plugin = new AlertingPlugin(context); - - // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() - await plugin.setup(setupMocks, mockPlugins); - - expect(setupMocks.status.set).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); - expect(context.logger.get().warn).toHaveBeenCalledWith( - 'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' - ); - }); - - it('should create usage counter if usageCollection plugin is defined', async () => { - const context = coreMock.createPluginInitializerContext( - generateAlertingConfig() - ); - plugin = new AlertingPlugin(context); + for (const useDataStreamForAlerts of [false, true]) { + const label = useDataStreamForAlerts ? 'data streams' : 'aliases'; - const usageCollectionSetup = createUsageCollectionSetupMock(); - - // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() - await plugin.setup(setupMocks, { ...mockPlugins, usageCollection: usageCollectionSetup }); + describe(`using ${label} for alert indices`, () => { + describe('setup()', () => { + const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + const setupMocks = coreMock.createSetup(); + const mockPlugins = { + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsSetup, + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + actions: actionsMock.createSetup(), + statusService: statusServiceMock.createSetupContract(), + monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, + features: featuresPluginMock.createSetup(), + unifiedSearch: autocompletePluginMock.createSetupContract(), + // serverless setup is currently empty, and there is no mock + ...(useDataStreamForAlerts ? { serverless: {} } : {}), + }; - expect(usageCollectionSetup.createUsageCounter).toHaveBeenCalled(); - expect(usageCollectionSetup.registerCollector).toHaveBeenCalled(); - }); + let plugin: AlertingPlugin; - it('should initialize AlertsService if enableFrameworkAlerts config is true', async () => { - const context = coreMock.createPluginInitializerContext({ - ...generateAlertingConfig(), - enableFrameworkAlerts: true, - }); - plugin = new AlertingPlugin(context); + beforeEach(() => jest.clearAllMocks()); - // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() - const setupContract = await plugin.setup(setupMocks, mockPlugins); + it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => { + const context = coreMock.createPluginInitializerContext( + generateAlertingConfig() + ); + plugin = new AlertingPlugin(context); - expect(AlertsService).toHaveBeenCalled(); + plugin.setup(setupMocks, mockPlugins); + await waitForSetupComplete(setupMocks); - expect(setupContract.frameworkAlerts.enabled()).toEqual(true); - }); + expect(setupMocks.status.set).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); + expect(context.logger.get().warn).toHaveBeenCalledWith( + 'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' + ); + }); - it(`exposes configured minimumScheduleInterval()`, async () => { - const context = coreMock.createPluginInitializerContext( - generateAlertingConfig() - ); - plugin = new AlertingPlugin(context); + it('should create usage counter if usageCollection plugin is defined', async () => { + const context = coreMock.createPluginInitializerContext( + generateAlertingConfig() + ); + plugin = new AlertingPlugin(context); - const setupContract = await plugin.setup(setupMocks, mockPlugins); + const usageCollectionSetup = createUsageCollectionSetupMock(); - expect(setupContract.getConfig()).toEqual({ - isUsingSecurity: false, - minimumScheduleInterval: { value: '1m', enforce: false }, - }); + // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() + plugin.setup(setupMocks, { ...mockPlugins, usageCollection: usageCollectionSetup }); + await waitForSetupComplete(setupMocks); - expect(setupContract.frameworkAlerts.enabled()).toEqual(false); - }); + expect(usageCollectionSetup.createUsageCounter).toHaveBeenCalled(); + expect(usageCollectionSetup.registerCollector).toHaveBeenCalled(); + }); - describe('registerType()', () => { - let setup: PluginSetupContract; - beforeEach(async () => { - const context = coreMock.createPluginInitializerContext( - generateAlertingConfig() - ); - plugin = new AlertingPlugin(context); - setup = await plugin.setup(setupMocks, mockPlugins); - }); + it('should initialize AlertsService if enableFrameworkAlerts config is true', async () => { + const context = coreMock.createPluginInitializerContext({ + ...generateAlertingConfig(), + enableFrameworkAlerts: true, + }); + plugin = new AlertingPlugin(context); - it('should throw error when license type is invalid', async () => { - expect(() => - setup.registerType({ - ...sampleRuleType, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - minimumLicenseRequired: 'foo' as any, - }) - ).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not a valid license type"`); - }); + // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() + const setupContract = plugin.setup(setupMocks, mockPlugins); + await waitForSetupComplete(setupMocks); - it('should not throw when license type is gold', async () => { - setup.registerType({ - ...sampleRuleType, - minimumLicenseRequired: 'gold', - }); - }); + expect(AlertsService).toHaveBeenCalled(); - it('should not throw when license type is basic', async () => { - setup.registerType({ - ...sampleRuleType, - minimumLicenseRequired: 'basic', + expect(setupContract.frameworkAlerts.enabled()).toEqual(true); }); - }); - - it('should apply default config value for ruleTaskTimeout if no value is specified', async () => { - const ruleType = { - ...sampleRuleType, - minimumLicenseRequired: 'basic', - } as RuleType; - await setup.registerType(ruleType); - expect(ruleType.ruleTaskTimeout).toBe('5m'); - }); - it('should apply value for ruleTaskTimeout if specified', async () => { - const ruleType = { - ...sampleRuleType, - minimumLicenseRequired: 'basic', - ruleTaskTimeout: '20h', - } as RuleType; - await setup.registerType(ruleType); - expect(ruleType.ruleTaskTimeout).toBe('20h'); - }); - - it('should apply default config value for cancelAlertsOnRuleTimeout if no value is specified', async () => { - const ruleType = { - ...sampleRuleType, - minimumLicenseRequired: 'basic', - } as RuleType; - await setup.registerType(ruleType); - expect(ruleType.cancelAlertsOnRuleTimeout).toBe(true); - }); + it(`exposes configured minimumScheduleInterval()`, async () => { + const context = coreMock.createPluginInitializerContext( + generateAlertingConfig() + ); + plugin = new AlertingPlugin(context); - it('should apply value for cancelAlertsOnRuleTimeout if specified', async () => { - const ruleType = { - ...sampleRuleType, - minimumLicenseRequired: 'basic', - cancelAlertsOnRuleTimeout: false, - } as RuleType; - await setup.registerType(ruleType); - expect(ruleType.cancelAlertsOnRuleTimeout).toBe(false); - }); - }); - }); + const setupContract = plugin.setup(setupMocks, mockPlugins); + await waitForSetupComplete(setupMocks); - describe('start()', () => { - describe('getRulesClientWithRequest()', () => { - it('throws error when encryptedSavedObjects plugin is missing encryption key', async () => { - const context = coreMock.createPluginInitializerContext( - generateAlertingConfig() - ); - const plugin = new AlertingPlugin(context); + expect(setupContract.getConfig()).toEqual({ + isUsingSecurity: false, + minimumScheduleInterval: { value: '1m', enforce: false }, + }); - const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); - plugin.setup(coreMock.createSetup(), { - licensing: licensingMock.createSetup(), - encryptedSavedObjects: encryptedSavedObjectsSetup, - taskManager: taskManagerMock.createSetup(), - eventLog: eventLogServiceMock.create(), - actions: actionsMock.createSetup(), - statusService: statusServiceMock.createSetupContract(), - monitoringCollection: monitoringCollectionMock.createSetup(), - data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, - features: featuresPluginMock.createSetup(), - unifiedSearch: autocompletePluginMock.createSetupContract(), + expect(setupContract.frameworkAlerts.enabled()).toEqual(false); }); - const startContract = plugin.start(coreMock.createStart(), { - actions: actionsMock.createStart(), - encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), - features: mockFeatures(), - spaces: spacesMock.createStart(), - licensing: licensingMock.createStart(), - eventLog: eventLogMock.createStart(), - taskManager: taskManagerMock.createStart(), - data: dataPluginMock.createStartContract(), - share: {} as SharePluginStart, - dataViews: { - dataViewsServiceFactory: jest - .fn() - .mockResolvedValue(dataViewPluginMocks.createStartContract()), - getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), - } as DataViewsServerPluginStart, + describe('registerType()', () => { + let setup: PluginSetupContract; + beforeEach(async () => { + const context = coreMock.createPluginInitializerContext( + generateAlertingConfig() + ); + plugin = new AlertingPlugin(context); + setup = plugin.setup(setupMocks, mockPlugins); + await waitForSetupComplete(setupMocks); + }); + + it('should throw error when license type is invalid', async () => { + expect(() => + setup.registerType({ + ...sampleRuleType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + minimumLicenseRequired: 'foo' as any, + }) + ).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not a valid license type"`); + }); + + it('should not throw when license type is gold', async () => { + setup.registerType({ + ...sampleRuleType, + minimumLicenseRequired: 'gold', + }); + }); + + it('should not throw when license type is basic', async () => { + setup.registerType({ + ...sampleRuleType, + minimumLicenseRequired: 'basic', + }); + }); + + it('should apply default config value for ruleTaskTimeout if no value is specified', async () => { + const ruleType = { + ...sampleRuleType, + minimumLicenseRequired: 'basic', + } as RuleType; + await setup.registerType(ruleType); + expect(ruleType.ruleTaskTimeout).toBe('5m'); + }); + + it('should apply value for ruleTaskTimeout if specified', async () => { + const ruleType = { + ...sampleRuleType, + minimumLicenseRequired: 'basic', + ruleTaskTimeout: '20h', + } as RuleType; + await setup.registerType(ruleType); + expect(ruleType.ruleTaskTimeout).toBe('20h'); + }); + + it('should apply default config value for cancelAlertsOnRuleTimeout if no value is specified', async () => { + const ruleType = { + ...sampleRuleType, + minimumLicenseRequired: 'basic', + } as RuleType; + await setup.registerType(ruleType); + expect(ruleType.cancelAlertsOnRuleTimeout).toBe(true); + }); + + it('should apply value for cancelAlertsOnRuleTimeout if specified', async () => { + const ruleType = { + ...sampleRuleType, + minimumLicenseRequired: 'basic', + cancelAlertsOnRuleTimeout: false, + } as RuleType; + await setup.registerType(ruleType); + expect(ruleType.cancelAlertsOnRuleTimeout).toBe(false); + }); }); - - expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); - expect(() => - startContract.getRulesClientWithRequest({} as KibanaRequest) - ).toThrowErrorMatchingInlineSnapshot( - `"Unable to create alerts client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` - ); }); - it(`doesn't throw error when encryptedSavedObjects plugin has encryption key`, async () => { - const context = coreMock.createPluginInitializerContext( - generateAlertingConfig() - ); - const plugin = new AlertingPlugin(context); - - const encryptedSavedObjectsSetup = { - ...encryptedSavedObjectsMock.createSetup(), - canEncrypt: true, - }; - plugin.setup(coreMock.createSetup(), { - licensing: licensingMock.createSetup(), - encryptedSavedObjects: encryptedSavedObjectsSetup, - taskManager: taskManagerMock.createSetup(), - eventLog: eventLogServiceMock.create(), - actions: actionsMock.createSetup(), - statusService: statusServiceMock.createSetupContract(), - monitoringCollection: monitoringCollectionMock.createSetup(), - data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, - features: featuresPluginMock.createSetup(), - unifiedSearch: autocompletePluginMock.createSetupContract(), - }); - - const startContract = plugin.start(coreMock.createStart(), { - actions: actionsMock.createStart(), - encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), - features: mockFeatures(), - spaces: spacesMock.createStart(), - licensing: licensingMock.createStart(), - eventLog: eventLogMock.createStart(), - taskManager: taskManagerMock.createStart(), - data: dataPluginMock.createStartContract(), - share: {} as SharePluginStart, - dataViews: { - dataViewsServiceFactory: jest - .fn() - .mockResolvedValue(dataViewPluginMocks.createStartContract()), - getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), - } as DataViewsServerPluginStart, + describe('start()', () => { + describe('getRulesClientWithRequest()', () => { + it('throws error when encryptedSavedObjects plugin is missing encryption key', async () => { + const context = coreMock.createPluginInitializerContext( + generateAlertingConfig() + ); + const plugin = new AlertingPlugin(context); + + const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + plugin.setup(coreMock.createSetup(), { + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsSetup, + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + actions: actionsMock.createSetup(), + statusService: statusServiceMock.createSetupContract(), + monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, + features: featuresPluginMock.createSetup(), + unifiedSearch: autocompletePluginMock.createSetupContract(), + ...(useDataStreamForAlerts ? { serverless: {} } : {}), + }); + + const startContract = plugin.start(coreMock.createStart(), { + actions: actionsMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + features: mockFeatures(), + spaces: spacesMock.createStart(), + licensing: licensingMock.createStart(), + eventLog: eventLogMock.createStart(), + taskManager: taskManagerMock.createStart(), + data: dataPluginMock.createStartContract(), + share: {} as SharePluginStart, + dataViews: { + dataViewsServiceFactory: jest + .fn() + .mockResolvedValue(dataViewPluginMocks.createStartContract()), + getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), + } as DataViewsServerPluginStart, + }); + + expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); + expect(() => + startContract.getRulesClientWithRequest({} as KibanaRequest) + ).toThrowErrorMatchingInlineSnapshot( + `"Unable to create alerts client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` + ); + }); + + it(`doesn't throw error when encryptedSavedObjects plugin has encryption key`, async () => { + const context = coreMock.createPluginInitializerContext( + generateAlertingConfig() + ); + const plugin = new AlertingPlugin(context); + + const encryptedSavedObjectsSetup = { + ...encryptedSavedObjectsMock.createSetup(), + canEncrypt: true, + }; + plugin.setup(coreMock.createSetup(), { + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsSetup, + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + actions: actionsMock.createSetup(), + statusService: statusServiceMock.createSetupContract(), + monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, + features: featuresPluginMock.createSetup(), + unifiedSearch: autocompletePluginMock.createSetupContract(), + ...(useDataStreamForAlerts ? { serverless: {} } : {}), + }); + + const startContract = plugin.start(coreMock.createStart(), { + actions: actionsMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + features: mockFeatures(), + spaces: spacesMock.createStart(), + licensing: licensingMock.createStart(), + eventLog: eventLogMock.createStart(), + taskManager: taskManagerMock.createStart(), + data: dataPluginMock.createStartContract(), + share: {} as SharePluginStart, + dataViews: { + dataViewsServiceFactory: jest + .fn() + .mockResolvedValue(dataViewPluginMocks.createStartContract()), + getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), + } as DataViewsServerPluginStart, + }); + + const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), + } as unknown as KibanaRequest; + startContract.getRulesClientWithRequest(fakeRequest); + }); }); - const fakeRequest = { - headers: {}, - getBasePath: () => '', - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', + test(`exposes getAlertingAuthorizationWithRequest()`, async () => { + const context = coreMock.createPluginInitializerContext( + generateAlertingConfig() + ); + const plugin = new AlertingPlugin(context); + + const encryptedSavedObjectsSetup = { + ...encryptedSavedObjectsMock.createSetup(), + canEncrypt: true, + }; + plugin.setup(coreMock.createSetup(), { + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsSetup, + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + actions: actionsMock.createSetup(), + statusService: statusServiceMock.createSetupContract(), + monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, + features: featuresPluginMock.createSetup(), + unifiedSearch: autocompletePluginMock.createSetupContract(), + ...(useDataStreamForAlerts ? { serverless: {} } : {}), + }); + + const startContract = plugin.start(coreMock.createStart(), { + actions: actionsMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + features: mockFeatures(), + spaces: spacesMock.createStart(), + licensing: licensingMock.createStart(), + eventLog: eventLogMock.createStart(), + taskManager: taskManagerMock.createStart(), + data: dataPluginMock.createStartContract(), + share: {} as SharePluginStart, + dataViews: { + dataViewsServiceFactory: jest + .fn() + .mockResolvedValue(dataViewPluginMocks.createStartContract()), + getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), + } as DataViewsServerPluginStart, + }); + + const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', }, - }, - getSavedObjectsClient: jest.fn(), - } as unknown as KibanaRequest; - startContract.getRulesClientWithRequest(fakeRequest); - }); - }); - - test(`exposes getAlertingAuthorizationWithRequest()`, async () => { - const context = coreMock.createPluginInitializerContext( - generateAlertingConfig() - ); - const plugin = new AlertingPlugin(context); - - const encryptedSavedObjectsSetup = { - ...encryptedSavedObjectsMock.createSetup(), - canEncrypt: true, - }; - plugin.setup(coreMock.createSetup(), { - licensing: licensingMock.createSetup(), - encryptedSavedObjects: encryptedSavedObjectsSetup, - taskManager: taskManagerMock.createSetup(), - eventLog: eventLogServiceMock.create(), - actions: actionsMock.createSetup(), - statusService: statusServiceMock.createSetupContract(), - monitoringCollection: monitoringCollectionMock.createSetup(), - data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, - features: featuresPluginMock.createSetup(), - unifiedSearch: autocompletePluginMock.createSetupContract(), - }); - - const startContract = plugin.start(coreMock.createStart(), { - actions: actionsMock.createStart(), - encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), - features: mockFeatures(), - spaces: spacesMock.createStart(), - licensing: licensingMock.createStart(), - eventLog: eventLogMock.createStart(), - taskManager: taskManagerMock.createStart(), - data: dataPluginMock.createStartContract(), - share: {} as SharePluginStart, - dataViews: { - dataViewsServiceFactory: jest - .fn() - .mockResolvedValue(dataViewPluginMocks.createStartContract()), - getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), - } as DataViewsServerPluginStart, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), + } as unknown as KibanaRequest; + startContract.getAlertingAuthorizationWithRequest(fakeRequest); + }); }); - - const fakeRequest = { - headers: {}, - getBasePath: () => '', - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - getSavedObjectsClient: jest.fn(), - } as unknown as KibanaRequest; - startContract.getAlertingAuthorizationWithRequest(fakeRequest); }); - }); + } }); function mockFeatures() { @@ -431,3 +423,22 @@ function mockFeatures() { ]); return features; } + +type CoreSetupMocks = ReturnType; + +const WaitForSetupAttempts = 10; +const WaitForSetupDelay = 200; +const WaitForSetupSeconds = (WaitForSetupAttempts * WaitForSetupDelay) / 1000; + +// wait for setup to *really* complete: waiting for calls to +// setupMocks.status.set, which needs to wait for core.getStartServices() +export async function waitForSetupComplete(setupMocks: CoreSetupMocks) { + let attempts = 0; + while (setupMocks.status.set.mock.calls.length < 1) { + attempts++; + await new Promise((resolve) => setTimeout(resolve, WaitForSetupDelay)); + if (attempts > WaitForSetupAttempts) { + throw new Error(`setupMocks.status.set was not called within ${WaitForSetupSeconds} seconds`); + } + } +} diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index dd20272b0fb68..fafd9a13925ab 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -56,6 +56,8 @@ import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; import { SharePluginStart } from '@kbn/share-plugin/server'; +import { ServerlessPluginSetup } from '@kbn/serverless/server'; + import { RuleTypeRegistry } from './rule_type_registry'; import { TaskRunnerFactory } from './task_runner'; import { RulesClientFactory } from './rules_client_factory'; @@ -97,6 +99,7 @@ import { } from './alerts_service'; import { rulesSettingsFeature } from './rules_settings_feature'; import { maintenanceWindowFeature } from './maintenance_window_feature'; +import { DataStreamAdapter, getDataStreamAdapter } from './alerts_service/lib/data_stream_adapter'; import { createGetAlertIndicesAliasFn, GetAlertIndicesAlias } from './lib'; export const EVENT_LOG_PROVIDER = 'alerting'; @@ -139,6 +142,7 @@ export interface PluginSetupContract { getSecurityHealth: () => Promise; getConfig: () => AlertingRulesConfig; frameworkAlerts: PublicFrameworkAlertsService; + getDataStreamAdapter: () => DataStreamAdapter; } export interface PluginStartContract { @@ -170,6 +174,7 @@ export interface AlertingPluginsSetup { data: DataPluginSetup; features: FeaturesPluginSetup; unifiedSearch: UnifiedSearchServerPluginSetup; + serverless?: ServerlessPluginSetup; } export interface AlertingPluginsStart { @@ -207,6 +212,7 @@ export class AlertingPlugin { private inMemoryMetrics: InMemoryMetrics; private alertsService: AlertsService | null; private pluginStop$: Subject; + private dataStreamAdapter?: DataStreamAdapter; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); @@ -231,6 +237,14 @@ export class AlertingPlugin { this.licenseState = new LicenseState(plugins.licensing.license$); this.security = plugins.security; + const useDataStreamForAlerts = !!plugins.serverless; + this.dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts }); + this.logger.info( + `using ${ + this.dataStreamAdapter.isUsingDataStreams() ? 'datastreams' : 'indexes and aliases' + } for persisting alerts` + ); + core.capabilities.registerProvider(() => { return { management: { @@ -266,6 +280,7 @@ export class AlertingPlugin { logger: this.logger, pluginStop$: this.pluginStop$, kibanaVersion: this.kibanaVersion, + dataStreamAdapter: this.dataStreamAdapter!, elasticsearchClientPromise: core .getStartServices() .then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser), @@ -417,6 +432,7 @@ export class AlertingPlugin { return Promise.resolve(errorResult(`Framework alerts service not available`)); }, }, + getDataStreamAdapter: () => this.dataStreamAdapter!, }; } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts index 58f2520ce1f4a..3ed2a63feacdc 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts @@ -68,6 +68,7 @@ import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock'; import { AlertsService } from '../alerts_service'; import { ReplaySubject } from 'rxjs'; import { IAlertsClient } from '../alerts_client/types'; +import { getDataStreamAdapter } from '../alerts_service/lib/data_stream_adapter'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -108,654 +109,683 @@ const ruleTypeWithAlerts: jest.Mocked = { }; describe('Task Runner', () => { - let mockedTaskInstance: ConcreteTaskInstance; - - beforeAll(() => { - fakeTimer = sinon.useFakeTimers(); - mockedTaskInstance = mockTaskInstance(); - }); - - afterAll(() => fakeTimer.restore()); - - const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); - const services = alertsMock.createRuleExecutorServices(); - const actionsClient = actionsClientMock.create(); - const rulesClient = rulesClientMock.create(); - const ruleTypeRegistry = ruleTypeRegistryMock.create(); - const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); - const elasticsearchService = elasticsearchServiceMock.createInternalStart(); - const dataPlugin = dataPluginMock.createStartContract(); - const uiSettingsService = uiSettingsServiceMock.createStartContract(); - const inMemoryMetrics = inMemoryMetricsMock.create(); - const dataViewsMock = { - dataViewsServiceFactory: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()), - getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), - } as DataViewsServerPluginStart; - const mockAlertsService = alertsServiceMock.create(); - const mockAlertsClient = alertsClientMock.create(); - const mockLegacyAlertsClient = legacyAlertsClientMock.create(); - const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); - const maintenanceWindowClient = maintenanceWindowClientMock.create(); - - type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { - actionsPlugin: jest.Mocked; - eventLogger: jest.Mocked; - executionContext: ReturnType; - }; - - const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { - data: dataPlugin, - dataViews: dataViewsMock, - savedObjects: savedObjectsService, - share: {} as SharePluginStart, - uiSettings: uiSettingsService, - elasticsearch: elasticsearchService, - actionsPlugin: actionsMock.createStart(), - getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), - encryptedSavedObjectsClient, - logger, - executionContext: executionContextServiceMock.createInternalStartContract(), - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - basePathService: httpServiceMock.createBasePath(), - eventLogger: eventLoggerMock.create(), - internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), - ruleTypeRegistry, - alertsService: mockAlertsService, - kibanaBaseUrl: 'https://localhost:5601', - supportsEphemeralTasks: false, - maxEphemeralActionsPerRule: 10, - maxAlerts: 1000, - cancelAlertsOnRuleTimeout: true, - usageCounter: mockUsageCounter, - actionsConfigMap: { - default: { - max: 10000, - }, - }, - getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), - getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), - }; - - beforeEach(() => { - jest.clearAllMocks(); - jest - .requireMock('../lib/wrap_scoped_cluster_client') - .createWrappedScopedClusterClientFactory.mockReturnValue({ - client: () => services.scopedClusterClient, - getMetrics: () => ({ - numSearches: 3, - esSearchDurationMs: 33, - totalSearchDurationMs: 23423, - }), - }); - savedObjectsService.getScopedClient.mockReturnValue(services.savedObjectsClient); - elasticsearchService.client.asScoped.mockReturnValue(services.scopedClusterClient); - maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValue([]); - taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mockReturnValue(rulesClient); - taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( - actionsClient - ); - taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation( - (actionTypeId, actionId, params) => params - ); - ruleTypeRegistry.get.mockReturnValue(ruleTypeWithAlerts); - taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => - fn() - ); - taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( - rulesSettingsClientMock.create() - ); - taskRunnerFactoryInitializerParams.getMaintenanceWindowClientWithRequest.mockReturnValue( - maintenanceWindowClient - ); - mockedRuleTypeSavedObject.monitoring!.run.history = []; - mockedRuleTypeSavedObject.monitoring!.run.calculated_metrics.success_ratio = 0; - - alertingEventLogger.getStartAndDuration.mockImplementation(() => ({ start: new Date() })); - (AlertingEventLogger as jest.Mock).mockImplementation(() => alertingEventLogger); - logger.get.mockImplementation(() => logger); - ruleType.executor.mockResolvedValue({ state: {} }); - }); - - test('should not use legacy alerts client if alerts client created', async () => { - const spy1 = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - const spy2 = jest - .spyOn(RuleRunMetricsStoreModule, 'RuleRunMetricsStore') - .mockImplementation(() => ruleRunMetricsStore); - mockAlertsService.createAlertsClient.mockImplementation(() => mockAlertsClient); - mockAlertsClient.getAlertsToSerialize.mockResolvedValue({ - alertsToReturn: {}, - recoveredAlertsToReturn: {}, - }); - ruleRunMetricsStore.getMetrics.mockReturnValue({ - numSearches: 3, - totalSearchDurationMs: 23423, - esSearchDurationMs: 33, - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - numberOfActiveAlerts: 0, - numberOfRecoveredAlerts: 0, - numberOfNewAlerts: 0, - hasReachedAlertLimit: false, - triggeredActionsStatus: 'complete', - }); - const taskRunner = new TaskRunner({ - ruleType: ruleTypeWithAlerts, - taskInstance: { - ...mockedTaskInstance, - state: { - ...mockedTaskInstance.state, - previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - }, - }, - context: taskRunnerFactoryInitializerParams, - inMemoryMetrics, - }); - expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + for (const useDataStreamForAlerts of [true, false]) { + const label = useDataStreamForAlerts ? 'data streams' : 'aliases'; - rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); + let mockedTaskInstance: ConcreteTaskInstance; - await taskRunner.run(); + beforeAll(() => { + fakeTimer = sinon.useFakeTimers(); + mockedTaskInstance = mockTaskInstance(); + }); - expect(mockAlertsService.createAlertsClient).toHaveBeenCalledWith({ + afterAll(() => fakeTimer.restore()); + + const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); + const services = alertsMock.createRuleExecutorServices(); + const actionsClient = actionsClientMock.create(); + const rulesClient = rulesClientMock.create(); + const ruleTypeRegistry = ruleTypeRegistryMock.create(); + const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); + const elasticsearchService = elasticsearchServiceMock.createInternalStart(); + const dataPlugin = dataPluginMock.createStartContract(); + const uiSettingsService = uiSettingsServiceMock.createStartContract(); + const inMemoryMetrics = inMemoryMetricsMock.create(); + const dataViewsMock = { + dataViewsServiceFactory: jest + .fn() + .mockResolvedValue(dataViewPluginMocks.createStartContract()), + getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), + } as DataViewsServerPluginStart; + const mockAlertsService = alertsServiceMock.create(); + const mockAlertsClient = alertsClientMock.create(); + const mockLegacyAlertsClient = legacyAlertsClientMock.create(); + const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); + const maintenanceWindowClient = maintenanceWindowClientMock.create(); + + type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { + actionsPlugin: jest.Mocked; + eventLogger: jest.Mocked; + executionContext: ReturnType; + }; + + const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { + data: dataPlugin, + dataViews: dataViewsMock, + savedObjects: savedObjectsService, + share: {} as SharePluginStart, + uiSettings: uiSettingsService, + elasticsearch: elasticsearchService, + actionsPlugin: actionsMock.createStart(), + getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), + encryptedSavedObjectsClient, logger, - ruleType: ruleTypeWithAlerts, - namespace: 'default', - rule: { - consumer: 'bar', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - id: '1', - name: 'rule-name', - parameters: { - bar: true, + executionContext: executionContextServiceMock.createInternalStartContract(), + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + basePathService: httpServiceMock.createBasePath(), + eventLogger: eventLoggerMock.create(), + internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), + ruleTypeRegistry, + alertsService: mockAlertsService, + kibanaBaseUrl: 'https://localhost:5601', + supportsEphemeralTasks: false, + maxEphemeralActionsPerRule: 10, + maxAlerts: 1000, + cancelAlertsOnRuleTimeout: true, + usageCounter: mockUsageCounter, + actionsConfigMap: { + default: { + max: 10000, }, - revision: 0, - spaceId: 'default', - tags: ['rule-', '-tags'], }, - }); - expect(LegacyAlertsClientModule.LegacyAlertsClient).not.toHaveBeenCalled(); + getRulesSettingsClientWithRequest: jest + .fn() + .mockReturnValue(rulesSettingsClientMock.create()), + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + }; + + describe(`using ${label} for alert indices`, () => { + beforeEach(() => { + jest.clearAllMocks(); + jest + .requireMock('../lib/wrap_scoped_cluster_client') + .createWrappedScopedClusterClientFactory.mockReturnValue({ + client: () => services.scopedClusterClient, + getMetrics: () => ({ + numSearches: 3, + esSearchDurationMs: 33, + totalSearchDurationMs: 23423, + }), + }); + savedObjectsService.getScopedClient.mockReturnValue(services.savedObjectsClient); + elasticsearchService.client.asScoped.mockReturnValue(services.scopedClusterClient); + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValue([]); + taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mockReturnValue(rulesClient); + taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( + actionsClient + ); + taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation( + (actionTypeId, actionId, params) => params + ); + ruleTypeRegistry.get.mockReturnValue(ruleTypeWithAlerts); + taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation( + (ctx, fn) => fn() + ); + taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( + rulesSettingsClientMock.create() + ); + taskRunnerFactoryInitializerParams.getMaintenanceWindowClientWithRequest.mockReturnValue( + maintenanceWindowClient + ); + mockedRuleTypeSavedObject.monitoring!.run.history = []; + mockedRuleTypeSavedObject.monitoring!.run.calculated_metrics.success_ratio = 0; + + alertingEventLogger.getStartAndDuration.mockImplementation(() => ({ start: new Date() })); + (AlertingEventLogger as jest.Mock).mockImplementation(() => alertingEventLogger); + logger.get.mockImplementation(() => logger); + ruleType.executor.mockResolvedValue({ state: {} }); + }); - testCorrectAlertsClientUsed({ - alertsClientToUse: mockAlertsClient, - alertsClientNotToUse: mockLegacyAlertsClient, - }); + test('should not use legacy alerts client if alerts client created', async () => { + const spy1 = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + const spy2 = jest + .spyOn(RuleRunMetricsStoreModule, 'RuleRunMetricsStore') + .mockImplementation(() => ruleRunMetricsStore); + mockAlertsService.createAlertsClient.mockImplementation(() => mockAlertsClient); + mockAlertsClient.getAlertsToSerialize.mockResolvedValue({ + alertsToReturn: {}, + recoveredAlertsToReturn: {}, + }); + ruleRunMetricsStore.getMetrics.mockReturnValue({ + numSearches: 3, + totalSearchDurationMs: 23423, + esSearchDurationMs: 33, + numberOfTriggeredActions: 0, + numberOfGeneratedActions: 0, + numberOfActiveAlerts: 0, + numberOfRecoveredAlerts: 0, + numberOfNewAlerts: 0, + hasReachedAlertLimit: false, + triggeredActionsStatus: 'complete', + }); + const taskRunner = new TaskRunner({ + ruleType: ruleTypeWithAlerts, + taskInstance: { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + }, + }, + context: taskRunnerFactoryInitializerParams, + inMemoryMetrics, + }); + expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + + rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); + + await taskRunner.run(); + + expect(mockAlertsService.createAlertsClient).toHaveBeenCalledWith({ + logger, + ruleType: ruleTypeWithAlerts, + namespace: 'default', + rule: { + consumer: 'bar', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + name: 'rule-name', + parameters: { + bar: true, + }, + revision: 0, + spaceId: 'default', + tags: ['rule-', '-tags'], + }, + }); + expect(LegacyAlertsClientModule.LegacyAlertsClient).not.toHaveBeenCalled(); - expect(ruleType.executor).toHaveBeenCalledTimes(1); - expect(logger.debug).toHaveBeenCalledTimes(5); - expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); - expect(logger.debug).nthCalledWith( - 2, - 'deprecated ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' - ); - expect(logger.debug).nthCalledWith( - 3, - 'ruleRunStatus for test:1: {"outcome":"succeeded","outcomeOrder":0,"outcomeMsg":null,"warning":null,"alertsCount":{"active":0,"new":0,"recovered":0,"ignored":0}}' - ); - expect(logger.debug).nthCalledWith( - 4, - 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"hasReachedAlertLimit":false,"triggeredActionsStatus":"complete"}' - ); - - expect( - taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update - ).toHaveBeenCalledWith(...generateSavedObjectParams({})); - - expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); - expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toHaveBeenCalledWith( - { - id: '1', - name: 'execute test', - type: 'alert', - description: 'execute [test] with name [rule-name] in [default] namespace', - }, - expect.any(Function) - ); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - expect( - jest.requireMock('../lib/wrap_scoped_cluster_client').createWrappedScopedClusterClientFactory - ).toHaveBeenCalled(); - spy1.mockRestore(); - spy2.mockRestore(); - }); - - test('should successfully execute task with alerts client', async () => { - const alertsService = new AlertsService({ - logger, - pluginStop$: new ReplaySubject(1), - kibanaVersion: '8.8.0', - elasticsearchClientPromise: Promise.resolve(clusterClient), - }); - const spy = jest - .spyOn(alertsService, 'getContextInitializationPromise') - .mockResolvedValue({ result: true }); - - const taskRunner = new TaskRunner({ - ruleType: ruleTypeWithAlerts, - taskInstance: { - ...mockedTaskInstance, - state: { - ...mockedTaskInstance.state, - previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - }, - }, - context: { - ...taskRunnerFactoryInitializerParams, - alertsService, - }, - inMemoryMetrics, - }); - expect(AlertingEventLogger).toHaveBeenCalledTimes(1); - rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); - const runnerResult = await taskRunner.run(); - expect(runnerResult).toEqual(generateRunnerResult({ state: true, history: [true] })); - - expect(ruleType.executor).toHaveBeenCalledTimes(1); - const call = ruleType.executor.mock.calls[0][0]; - expect(call.params).toEqual({ bar: true }); - expect(call.startedAt).toStrictEqual(new Date(DATE_1970)); - expect(call.previousStartedAt).toStrictEqual(new Date(DATE_1970_5_MIN)); - expect(call.state).toEqual({}); - expect(call.rule).not.toBe(null); - expect(call.rule.id).toBe('1'); - expect(call.rule.name).toBe(RULE_NAME); - expect(call.rule.tags).toEqual(['rule-', '-tags']); - expect(call.rule.consumer).toBe('bar'); - expect(call.rule.enabled).toBe(true); - expect(call.rule.schedule).toEqual({ interval: '10s' }); - expect(call.rule.createdBy).toBe('rule-creator'); - expect(call.rule.updatedBy).toBe('rule-updater'); - expect(call.rule.createdAt).toBe(mockDate); - expect(call.rule.updatedAt).toBe(mockDate); - expect(call.rule.notifyWhen).toBe('onActiveAlert'); - expect(call.rule.throttle).toBe(null); - expect(call.rule.producer).toBe('alerts'); - expect(call.rule.ruleTypeId).toBe('test'); - expect(call.rule.ruleTypeName).toBe('My test rule'); - expect(call.rule.actions).toEqual(RULE_ACTIONS); - expect(call.services.alertFactory.create).toBeTruthy(); - expect(call.services.alertsClient).not.toBe(null); - expect(call.services.alertsClient?.report).toBeTruthy(); - expect(call.services.alertsClient?.setAlertData).toBeTruthy(); - expect(call.services.scopedClusterClient).toBeTruthy(); - expect(call.services).toBeTruthy(); - expect(logger.debug).toHaveBeenCalledTimes(6); - expect(logger.debug).nthCalledWith(1, `Initializing resources for AlertsService`); - expect(logger.debug).nthCalledWith(2, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); - expect(logger.debug).nthCalledWith( - 3, - 'deprecated ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' - ); - expect(logger.debug).nthCalledWith( - 4, - 'ruleRunStatus for test:1: {"outcome":"succeeded","outcomeOrder":0,"outcomeMsg":null,"warning":null,"alertsCount":{"active":0,"new":0,"recovered":0,"ignored":0}}' - ); - expect(logger.debug).nthCalledWith( - 5, - 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"hasReachedAlertLimit":false,"triggeredActionsStatus":"complete"}' - ); - expect( - taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update - ).toHaveBeenCalledWith(...generateSavedObjectParams({})); - expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); - expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toHaveBeenCalledWith( - { - id: '1', - name: 'execute test', - type: 'alert', - description: 'execute [test] with name [rule-name] in [default] namespace', - }, - expect.any(Function) - ); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - expect( - jest.requireMock('../lib/wrap_scoped_cluster_client').createWrappedScopedClusterClientFactory - ).toHaveBeenCalled(); - spy.mockRestore(); - }); - - test('should successfully execute task and index alert documents', async () => { - const alertsService = new AlertsService({ - logger, - pluginStop$: new ReplaySubject(1), - kibanaVersion: '8.8.0', - elasticsearchClientPromise: Promise.resolve(clusterClient), - }); - const spy = jest - .spyOn(alertsService, 'getContextInitializationPromise') - .mockResolvedValue({ result: true }); - - ruleTypeWithAlerts.executor.mockImplementation( - async ({ - services: executorServices, - }: RuleExecutorOptions< - RuleTypeParams, - RuleTypeState, - AlertInstanceState, - AlertInstanceContext, - string, - RuleAlertData - >) => { - executorServices.alertsClient?.report({ - id: '1', - actionGroup: 'default', - payload: { textField: 'foo', numericField: 27 }, + testCorrectAlertsClientUsed({ + alertsClientToUse: mockAlertsClient, + alertsClientNotToUse: mockLegacyAlertsClient, }); - return { state: {} }; - } - ); - - const taskRunner = new TaskRunner({ - ruleType: ruleTypeWithAlerts, - taskInstance: mockedTaskInstance, - context: { - ...taskRunnerFactoryInitializerParams, - alertsService, - }, - inMemoryMetrics, - }); - expect(AlertingEventLogger).toHaveBeenCalledTimes(1); - rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); - await taskRunner.run(); - - expect(ruleType.executor).toHaveBeenCalledTimes(1); - - expect(clusterClient.bulk).toHaveBeenCalledWith({ - index: '.alerts-test.alerts-default', - refresh: 'wait_for', - require_alias: true, - body: [ - { index: { _id: '5f6aa57d-3e22-484e-bae8-cbed868f4d28' } }, - // new alert doc - { - '@timestamp': DATE_1970, - event: { - action: 'open', - kind: 'signal', + + expect(ruleType.executor).toHaveBeenCalledTimes(1); + expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + 'deprecated ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' + ); + expect(logger.debug).nthCalledWith( + 3, + 'ruleRunStatus for test:1: {"outcome":"succeeded","outcomeOrder":0,"outcomeMsg":null,"warning":null,"alertsCount":{"active":0,"new":0,"recovered":0,"ignored":0}}' + ); + expect(logger.debug).nthCalledWith( + 4, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"hasReachedAlertLimit":false,"triggeredActionsStatus":"complete"}' + ); + + expect( + taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update + ).toHaveBeenCalledWith(...generateSavedObjectParams({})); + + expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); + expect( + taskRunnerFactoryInitializerParams.executionContext.withContext + ).toHaveBeenCalledWith( + { + id: '1', + name: 'execute test', + type: 'alert', + description: 'execute [test] with name [rule-name] in [default] namespace', + }, + expect.any(Function) + ); + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + expect( + jest.requireMock('../lib/wrap_scoped_cluster_client') + .createWrappedScopedClusterClientFactory + ).toHaveBeenCalled(); + spy1.mockRestore(); + spy2.mockRestore(); + }); + + test('should successfully execute task with alerts client', async () => { + const alertsService = new AlertsService({ + logger, + pluginStop$: new ReplaySubject(1), + kibanaVersion: '8.8.0', + elasticsearchClientPromise: Promise.resolve(clusterClient), + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts }), + }); + const spy = jest + .spyOn(alertsService, 'getContextInitializationPromise') + .mockResolvedValue({ result: true }); + + const taskRunner = new TaskRunner({ + ruleType: ruleTypeWithAlerts, + taskInstance: { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + }, + }, + context: { + ...taskRunnerFactoryInitializerParams, + alertsService, + }, + inMemoryMetrics, + }); + expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); + const runnerResult = await taskRunner.run(); + expect(runnerResult).toEqual(generateRunnerResult({ state: true, history: [true] })); + + expect(ruleType.executor).toHaveBeenCalledTimes(1); + const call = ruleType.executor.mock.calls[0][0]; + expect(call.params).toEqual({ bar: true }); + expect(call.startedAt).toStrictEqual(new Date(DATE_1970)); + expect(call.previousStartedAt).toStrictEqual(new Date(DATE_1970_5_MIN)); + expect(call.state).toEqual({}); + expect(call.rule).not.toBe(null); + expect(call.rule.id).toBe('1'); + expect(call.rule.name).toBe(RULE_NAME); + expect(call.rule.tags).toEqual(['rule-', '-tags']); + expect(call.rule.consumer).toBe('bar'); + expect(call.rule.enabled).toBe(true); + expect(call.rule.schedule).toEqual({ interval: '10s' }); + expect(call.rule.createdBy).toBe('rule-creator'); + expect(call.rule.updatedBy).toBe('rule-updater'); + expect(call.rule.createdAt).toBe(mockDate); + expect(call.rule.updatedAt).toBe(mockDate); + expect(call.rule.notifyWhen).toBe('onActiveAlert'); + expect(call.rule.throttle).toBe(null); + expect(call.rule.producer).toBe('alerts'); + expect(call.rule.ruleTypeId).toBe('test'); + expect(call.rule.ruleTypeName).toBe('My test rule'); + expect(call.rule.actions).toEqual(RULE_ACTIONS); + expect(call.services.alertFactory.create).toBeTruthy(); + expect(call.services.alertsClient).not.toBe(null); + expect(call.services.alertsClient?.report).toBeTruthy(); + expect(call.services.alertsClient?.setAlertData).toBeTruthy(); + expect(call.services.scopedClusterClient).toBeTruthy(); + expect(call.services).toBeTruthy(); + expect(logger.debug).toHaveBeenCalledTimes(6); + expect(logger.debug).nthCalledWith(1, `Initializing resources for AlertsService`); + expect(logger.debug).nthCalledWith(2, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 3, + 'deprecated ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' + ); + expect(logger.debug).nthCalledWith( + 4, + 'ruleRunStatus for test:1: {"outcome":"succeeded","outcomeOrder":0,"outcomeMsg":null,"warning":null,"alertsCount":{"active":0,"new":0,"recovered":0,"ignored":0}}' + ); + expect(logger.debug).nthCalledWith( + 5, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"hasReachedAlertLimit":false,"triggeredActionsStatus":"complete"}' + ); + expect( + taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update + ).toHaveBeenCalledWith(...generateSavedObjectParams({})); + expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); + expect( + taskRunnerFactoryInitializerParams.executionContext.withContext + ).toHaveBeenCalledWith( + { + id: '1', + name: 'execute test', + type: 'alert', + description: 'execute [test] with name [rule-name] in [default] namespace', + }, + expect.any(Function) + ); + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + expect( + jest.requireMock('../lib/wrap_scoped_cluster_client') + .createWrappedScopedClusterClientFactory + ).toHaveBeenCalled(); + spy.mockRestore(); + }); + + test('should successfully execute task and index alert documents', async () => { + const alertsService = new AlertsService({ + logger, + pluginStop$: new ReplaySubject(1), + kibanaVersion: '8.8.0', + elasticsearchClientPromise: Promise.resolve(clusterClient), + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts }), + }); + const spy = jest + .spyOn(alertsService, 'getContextInitializationPromise') + .mockResolvedValue({ result: true }); + + ruleTypeWithAlerts.executor.mockImplementation( + async ({ + services: executorServices, + }: RuleExecutorOptions< + RuleTypeParams, + RuleTypeState, + AlertInstanceState, + AlertInstanceContext, + string, + RuleAlertData + >) => { + executorServices.alertsClient?.report({ + id: '1', + actionGroup: 'default', + payload: { textField: 'foo', numericField: 27 }, + }); + return { state: {} }; + } + ); + + const taskRunner = new TaskRunner({ + ruleType: ruleTypeWithAlerts, + taskInstance: mockedTaskInstance, + context: { + ...taskRunnerFactoryInitializerParams, + alertsService, }, - kibana: { - alert: { - action_group: 'default', - duration: { - us: '0', + inMemoryMetrics, + }); + expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); + await taskRunner.run(); + + expect(ruleType.executor).toHaveBeenCalledTimes(1); + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: 'wait_for', + require_alias: !useDataStreamForAlerts, + body: [ + { + create: { + _id: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + ...(useDataStreamForAlerts ? {} : { require_alias: true }), }, - flapping: false, - flapping_history: [true], - instance: { - id: '1', + }, + // new alert doc + { + '@timestamp': DATE_1970, + event: { + action: 'open', + kind: 'signal', }, - maintenance_window_ids: [], - rule: { - category: 'My test rule', - consumer: 'bar', - execution: { + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + maintenance_window_ids: [], + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: DATE_1970, + status: 'active', + time_range: { + gte: DATE_1970, + }, uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + workflow_status: 'open', }, - name: 'rule-name', - parameters: { - bar: true, - }, - producer: 'alerts', - revision: 0, - rule_type_id: 'test', - tags: ['rule-', '-tags'], - uuid: '1', - }, - start: DATE_1970, - status: 'active', - time_range: { - gte: DATE_1970, + space_ids: ['default'], + version: '8.8.0', }, - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - workflow_status: 'open', + numericField: 27, + textField: 'foo', + tags: ['rule-', '-tags'], + }, + ], + }); + spy.mockRestore(); + }); + + test('should default to legacy alerts client if error creating alerts client', async () => { + const spy1 = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + const spy2 = jest + .spyOn(RuleRunMetricsStoreModule, 'RuleRunMetricsStore') + .mockImplementation(() => ruleRunMetricsStore); + mockAlertsService.createAlertsClient.mockImplementation(() => { + throw new Error('Could not initialize!'); + }); + mockLegacyAlertsClient.getAlertsToSerialize.mockResolvedValue({ + alertsToReturn: {}, + recoveredAlertsToReturn: {}, + }); + ruleRunMetricsStore.getMetrics.mockReturnValue({ + numSearches: 3, + totalSearchDurationMs: 23423, + esSearchDurationMs: 33, + numberOfTriggeredActions: 0, + numberOfGeneratedActions: 0, + numberOfActiveAlerts: 0, + numberOfRecoveredAlerts: 0, + numberOfNewAlerts: 0, + hasReachedAlertLimit: false, + triggeredActionsStatus: 'complete', + }); + const taskRunner = new TaskRunner({ + ruleType: ruleTypeWithAlerts, + taskInstance: { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), }, - space_ids: ['default'], - version: '8.8.0', }, - numericField: 27, - textField: 'foo', - tags: ['rule-', '-tags'], - }, - ], - }); - spy.mockRestore(); - }); - - test('should default to legacy alerts client if error creating alerts client', async () => { - const spy1 = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - const spy2 = jest - .spyOn(RuleRunMetricsStoreModule, 'RuleRunMetricsStore') - .mockImplementation(() => ruleRunMetricsStore); - mockAlertsService.createAlertsClient.mockImplementation(() => { - throw new Error('Could not initialize!'); - }); - mockLegacyAlertsClient.getAlertsToSerialize.mockResolvedValue({ - alertsToReturn: {}, - recoveredAlertsToReturn: {}, - }); - ruleRunMetricsStore.getMetrics.mockReturnValue({ - numSearches: 3, - totalSearchDurationMs: 23423, - esSearchDurationMs: 33, - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - numberOfActiveAlerts: 0, - numberOfRecoveredAlerts: 0, - numberOfNewAlerts: 0, - hasReachedAlertLimit: false, - triggeredActionsStatus: 'complete', - }); - const taskRunner = new TaskRunner({ - ruleType: ruleTypeWithAlerts, - taskInstance: { - ...mockedTaskInstance, - state: { - ...mockedTaskInstance.state, - previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - }, - }, - context: taskRunnerFactoryInitializerParams, - inMemoryMetrics, - }); - expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + context: taskRunnerFactoryInitializerParams, + inMemoryMetrics, + }); + expect(AlertingEventLogger).toHaveBeenCalledTimes(1); - rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); + rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); - await taskRunner.run(); + await taskRunner.run(); - expect(mockAlertsService.createAlertsClient).toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith( - `Error initializing AlertsClient for context test. Using legacy alerts client instead. - Could not initialize!` - ); - expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({ - logger, - ruleType: ruleTypeWithAlerts, - }); + expect(mockAlertsService.createAlertsClient).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + `Error initializing AlertsClient for context test. Using legacy alerts client instead. - Could not initialize!` + ); + expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({ + logger, + ruleType: ruleTypeWithAlerts, + }); - testCorrectAlertsClientUsed({ - alertsClientToUse: mockLegacyAlertsClient, - alertsClientNotToUse: mockAlertsClient, - }); + testCorrectAlertsClientUsed({ + alertsClientToUse: mockLegacyAlertsClient, + alertsClientNotToUse: mockAlertsClient, + }); - expect(ruleType.executor).toHaveBeenCalledTimes(1); + expect(ruleType.executor).toHaveBeenCalledTimes(1); - expect(logger.debug).toHaveBeenCalledTimes(5); - expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); - expect( - taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update - ).toHaveBeenCalledWith(...generateSavedObjectParams({})); + expect( + taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update + ).toHaveBeenCalledWith(...generateSavedObjectParams({})); - expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); - expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toHaveBeenCalledWith( - { - id: '1', - name: 'execute test', - type: 'alert', - description: 'execute [test] with name [rule-name] in [default] namespace', - }, - expect.any(Function) - ); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - expect( - jest.requireMock('../lib/wrap_scoped_cluster_client').createWrappedScopedClusterClientFactory - ).toHaveBeenCalled(); - spy1.mockRestore(); - spy2.mockRestore(); - }); - - test('should default to legacy alerts client if alert service is not defined', async () => { - const spy1 = jest - .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') - .mockImplementation(() => mockLegacyAlertsClient); - const spy2 = jest - .spyOn(RuleRunMetricsStoreModule, 'RuleRunMetricsStore') - .mockImplementation(() => ruleRunMetricsStore); - mockLegacyAlertsClient.getAlertsToSerialize.mockResolvedValue({ - alertsToReturn: {}, - recoveredAlertsToReturn: {}, - }); - ruleRunMetricsStore.getMetrics.mockReturnValue({ - numSearches: 3, - totalSearchDurationMs: 23423, - esSearchDurationMs: 33, - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - numberOfActiveAlerts: 0, - numberOfRecoveredAlerts: 0, - numberOfNewAlerts: 0, - hasReachedAlertLimit: false, - triggeredActionsStatus: 'complete', - }); - const taskRunner = new TaskRunner({ - ruleType: ruleTypeWithAlerts, - taskInstance: { - ...mockedTaskInstance, - state: { - ...mockedTaskInstance.state, - previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - }, - }, - context: { ...taskRunnerFactoryInitializerParams, alertsService: null }, - inMemoryMetrics, - }); - expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); + expect( + taskRunnerFactoryInitializerParams.executionContext.withContext + ).toHaveBeenCalledWith( + { + id: '1', + name: 'execute test', + type: 'alert', + description: 'execute [test] with name [rule-name] in [default] namespace', + }, + expect.any(Function) + ); + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + expect( + jest.requireMock('../lib/wrap_scoped_cluster_client') + .createWrappedScopedClusterClientFactory + ).toHaveBeenCalled(); + spy1.mockRestore(); + spy2.mockRestore(); + }); - rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); + test('should default to legacy alerts client if alert service is not defined', async () => { + const spy1 = jest + .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') + .mockImplementation(() => mockLegacyAlertsClient); + const spy2 = jest + .spyOn(RuleRunMetricsStoreModule, 'RuleRunMetricsStore') + .mockImplementation(() => ruleRunMetricsStore); + mockLegacyAlertsClient.getAlertsToSerialize.mockResolvedValue({ + alertsToReturn: {}, + recoveredAlertsToReturn: {}, + }); + ruleRunMetricsStore.getMetrics.mockReturnValue({ + numSearches: 3, + totalSearchDurationMs: 23423, + esSearchDurationMs: 33, + numberOfTriggeredActions: 0, + numberOfGeneratedActions: 0, + numberOfActiveAlerts: 0, + numberOfRecoveredAlerts: 0, + numberOfNewAlerts: 0, + hasReachedAlertLimit: false, + triggeredActionsStatus: 'complete', + }); + const taskRunner = new TaskRunner({ + ruleType: ruleTypeWithAlerts, + taskInstance: { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + }, + }, + context: { ...taskRunnerFactoryInitializerParams, alertsService: null }, + inMemoryMetrics, + }); + expect(AlertingEventLogger).toHaveBeenCalledTimes(1); - await taskRunner.run(); + rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); - expect(mockAlertsService.createAlertsClient).not.toHaveBeenCalled(); - expect(logger.error).not.toHaveBeenCalled(); - expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({ - logger, - ruleType: ruleTypeWithAlerts, - }); + await taskRunner.run(); - testCorrectAlertsClientUsed({ - alertsClientToUse: mockLegacyAlertsClient, - alertsClientNotToUse: mockAlertsClient, - }); + expect(mockAlertsService.createAlertsClient).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); + expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({ + logger, + ruleType: ruleTypeWithAlerts, + }); - expect(ruleType.executor).toHaveBeenCalledTimes(1); + testCorrectAlertsClientUsed({ + alertsClientToUse: mockLegacyAlertsClient, + alertsClientNotToUse: mockAlertsClient, + }); - expect(logger.debug).toHaveBeenCalledTimes(5); - expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); + expect(ruleType.executor).toHaveBeenCalledTimes(1); - expect( - taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update - ).toHaveBeenCalledWith(...generateSavedObjectParams({})); + expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); - expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); - expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toHaveBeenCalledWith( - { - id: '1', - name: 'execute test', - type: 'alert', - description: 'execute [test] with name [rule-name] in [default] namespace', - }, - expect.any(Function) - ); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - expect( - jest.requireMock('../lib/wrap_scoped_cluster_client').createWrappedScopedClusterClientFactory - ).toHaveBeenCalled(); - spy1.mockRestore(); - spy2.mockRestore(); - }); - - function testCorrectAlertsClientUsed< - AlertData extends RuleAlertData = never, - State extends AlertInstanceState = never, - Context extends AlertInstanceContext = never, - ActionGroupIds extends string = 'default', - RecoveryActionGroupId extends string = 'recovered' - >({ - alertsClientToUse, - alertsClientNotToUse, - }: { - alertsClientToUse: IAlertsClient< - AlertData, - State, - Context, - ActionGroupIds, - RecoveryActionGroupId - >; - alertsClientNotToUse: IAlertsClient< - AlertData, - State, - Context, - ActionGroupIds, - RecoveryActionGroupId - >; - }) { - expect(alertsClientToUse.initializeExecution).toHaveBeenCalledWith({ - activeAlertsFromState: {}, - flappingSettings: { - enabled: true, - lookBackWindow: 20, - statusChangeThreshold: 4, - }, - maxAlerts: 1000, - recoveredAlertsFromState: {}, - ruleLabel: "test:1: 'rule-name'", - }); - expect(alertsClientNotToUse.initializeExecution).not.toHaveBeenCalled(); - - expect(alertsClientToUse.checkLimitUsage).toHaveBeenCalled(); - expect(alertsClientNotToUse.checkLimitUsage).not.toHaveBeenCalled(); - - expect(alertsClientToUse.processAndLogAlerts).toHaveBeenCalledWith({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: true, - flappingSettings: { - enabled: true, - lookBackWindow: 20, - statusChangeThreshold: 4, - }, - notifyWhen: RuleNotifyWhen.ACTIVE, - maintenanceWindowIds: [], + expect( + taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update + ).toHaveBeenCalledWith(...generateSavedObjectParams({})); + + expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); + expect( + taskRunnerFactoryInitializerParams.executionContext.withContext + ).toHaveBeenCalledWith( + { + id: '1', + name: 'execute test', + type: 'alert', + description: 'execute [test] with name [rule-name] in [default] namespace', + }, + expect.any(Function) + ); + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + expect( + jest.requireMock('../lib/wrap_scoped_cluster_client') + .createWrappedScopedClusterClientFactory + ).toHaveBeenCalled(); + spy1.mockRestore(); + spy2.mockRestore(); + }); }); - expect(alertsClientNotToUse.processAndLogAlerts).not.toHaveBeenCalled(); - expect(alertsClientToUse.persistAlerts).toHaveBeenCalled(); - expect(alertsClientNotToUse.persistAlerts).not.toHaveBeenCalled(); + function testCorrectAlertsClientUsed< + AlertData extends RuleAlertData = never, + State extends AlertInstanceState = never, + Context extends AlertInstanceContext = never, + ActionGroupIds extends string = 'default', + RecoveryActionGroupId extends string = 'recovered' + >({ + alertsClientToUse, + alertsClientNotToUse, + }: { + alertsClientToUse: IAlertsClient< + AlertData, + State, + Context, + ActionGroupIds, + RecoveryActionGroupId + >; + alertsClientNotToUse: IAlertsClient< + AlertData, + State, + Context, + ActionGroupIds, + RecoveryActionGroupId + >; + }) { + expect(alertsClientToUse.initializeExecution).toHaveBeenCalledWith({ + activeAlertsFromState: {}, + flappingSettings: { + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 4, + }, + maxAlerts: 1000, + recoveredAlertsFromState: {}, + ruleLabel: "test:1: 'rule-name'", + }); + expect(alertsClientNotToUse.initializeExecution).not.toHaveBeenCalled(); + + expect(alertsClientToUse.checkLimitUsage).toHaveBeenCalled(); + expect(alertsClientNotToUse.checkLimitUsage).not.toHaveBeenCalled(); + + expect(alertsClientToUse.processAndLogAlerts).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + ruleRunMetricsStore, + shouldLogAlerts: true, + flappingSettings: { + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 4, + }, + notifyWhen: RuleNotifyWhen.ACTIVE, + maintenanceWindowIds: [], + }); + expect(alertsClientNotToUse.processAndLogAlerts).not.toHaveBeenCalled(); + + expect(alertsClientToUse.persistAlerts).toHaveBeenCalled(); + expect(alertsClientNotToUse.persistAlerts).not.toHaveBeenCalled(); - expect(alertsClientToUse.getProcessedAlerts).toHaveBeenCalledWith('activeCurrent'); - expect(alertsClientToUse.getProcessedAlerts).toHaveBeenCalledWith('recoveredCurrent'); - expect(alertsClientNotToUse.getProcessedAlerts).not.toHaveBeenCalled(); + expect(alertsClientToUse.getProcessedAlerts).toHaveBeenCalledWith('activeCurrent'); + expect(alertsClientToUse.getProcessedAlerts).toHaveBeenCalledWith('recoveredCurrent'); + expect(alertsClientNotToUse.getProcessedAlerts).not.toHaveBeenCalled(); - expect(alertsClientToUse.getAlertsToSerialize).toHaveBeenCalled(); - expect(alertsClientNotToUse.getAlertsToSerialize).not.toHaveBeenCalled(); + expect(alertsClientToUse.getAlertsToSerialize).toHaveBeenCalled(); + expect(alertsClientNotToUse.getAlertsToSerialize).not.toHaveBeenCalled(); + } } }); diff --git a/x-pack/plugins/alerting/server/test_utils/index.ts b/x-pack/plugins/alerting/server/test_utils/index.ts index 589dae529cee6..ec82b884fb427 100644 --- a/x-pack/plugins/alerting/server/test_utils/index.ts +++ b/x-pack/plugins/alerting/server/test_utils/index.ts @@ -6,6 +6,7 @@ */ import { RawAlertInstance } from '../../common'; +import { AlertingConfig } from '../config'; interface Resolvable { resolve: (arg: T) => void; @@ -45,3 +46,29 @@ export function alertsWithAnyUUID( } return newAlerts; } + +export function generateAlertingConfig(): AlertingConfig { + return { + healthCheck: { + interval: '5m', + }, + enableFrameworkAlerts: false, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + maxEphemeralActionsPerAlert: 10, + cancelAlertsOnRuleTimeout: true, + rules: { + minimumScheduleInterval: { value: '1m', enforce: false }, + run: { + actions: { + max: 1000, + }, + alerts: { + max: 1000, + }, + }, + }, + }; +} diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index c7e8294759657..bee42c98dc075 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -468,3 +468,5 @@ export interface RawRule extends SavedObjectAttributes { revision: number; running?: boolean | null; } + +export type { DataStreamAdapter } from './alerts_service/lib/data_stream_adapter'; diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 9a3976445c235..f38fbd085c7f0 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -56,6 +56,7 @@ "@kbn/core-capabilities-common", "@kbn/unified-search-plugin", "@kbn/core-http-server-mocks", + "@kbn/serverless", "@kbn/core-http-router-server-mocks", ], "exclude": ["target/**/*"] diff --git a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap index 7e0d699fddfc0..2bed81c9c05c3 100644 --- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap @@ -14,76 +14,148 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "services_per_agent": { "properties": { "android/java": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the android/java agent within the last day" + } }, "dotnet": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the dotnet (.Net) agent within the last day" + } }, "iOS/swift": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the iOS/swift agent within the last day" + } }, "go": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the go agent within the last day" + } }, "java": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the Java agent within the last day" + } }, "js-base": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the js-base agent within the last day" + } }, "nodejs": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the nodeJS agent within the last day" + } }, "php": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the PHH agent within the last day" + } }, "python": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the Python agent within the last day" + } }, "ruby": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the Ruby agent within the last day" + } }, "rum-js": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the rum-js agent within the last day" + } }, "otlp": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the otlp agent within the last day" + } }, "opentelemetry/cpp": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/cpp agent within the last day" + } }, "opentelemetry/dotnet": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/dotnet agent within the last day" + } }, "opentelemetry/erlang": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/erlang agent within the last day" + } }, "opentelemetry/go": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/go agent within the last day" + } }, "opentelemetry/java": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/java agent within the last day" + } }, "opentelemetry/nodejs": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/nodejs agent within the last day" + } }, "opentelemetry/php": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/php agent within the last day" + } }, "opentelemetry/python": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/python agent within the last day" + } }, "opentelemetry/ruby": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/ruby agent within the last day" + } }, "opentelemetry/rust": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/rust agent within the last day" + } }, "opentelemetry/swift": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/swift agent within the last day" + } }, "opentelemetry/webjs": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/webjs agent within the last day" + } } } }, @@ -1140,60 +1212,96 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "current_implementation": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "no_observer_name": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "no_rum": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "no_rum_no_observer_name": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "only_rum": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "only_rum_no_observer_name": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } } @@ -1366,10 +1474,10 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "services": { "properties": { "1d": { - "type": "long" - }, - "all": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of unique services within the last day" + } } } }, @@ -1552,7 +1660,10 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "shards": { "properties": { "total": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of shards for metric indices" + } } } }, @@ -1563,14 +1674,20 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "docs": { "properties": { "count": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of metric documents overall" + } } } }, "store": { "properties": { "size_in_bytes": { - "type": "long" + "type": "long", + "_meta": { + "description": "Size of the metric indicess in byte units overall." + } } } } @@ -1587,7 +1704,7 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "total": { "type": "long", "_meta": { - "description": "Total number of shards overall" + "description": "Total number of shards for span and trasnaction indices" } } } @@ -1819,7 +1936,10 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "pod": { "properties": { "name": { - "type": "keyword" + "type": "keyword", + "_meta": { + "description": "Kuberneted pod name " + } } } } @@ -1828,7 +1948,10 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "container": { "properties": { "id": { - "type": "keyword" + "type": "keyword", + "_meta": { + "description": "Container id" + } } } } diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_map/service_map.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_map/service_map.cy.ts index 2471617e7b1f4..899ffe9d928b7 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_map/service_map.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_map/service_map.cy.ts @@ -9,7 +9,7 @@ import { synthtrace } from '../../../synthtrace'; import { opbeans } from '../../fixtures/synthtrace/opbeans'; const start = '2021-10-10T00:00:00.000Z'; -const end = '2021-10-10T00:15:00.000Z'; +const end = '2021-10-10T00:01:00.000Z'; const serviceMapHref = url.format({ pathname: '/app/apm/service-map', @@ -29,7 +29,7 @@ const detailedServiceMap = url.format({ }, }); -describe('Service map', () => { +describe('service map', () => { before(() => { synthtrace.index( opbeans({ @@ -47,20 +47,28 @@ describe('Service map', () => { cy.loginAsViewerUser(); }); - describe('When navigating to service map', () => { - it('opens service map', () => { + describe('when navigating to service map', () => { + beforeEach(() => { + cy.intercept('GET', '/internal/apm/service-map?*').as('serviceMap'); cy.visitKibana(serviceMapHref); - cy.contains('h1', 'Services'); + + cy.wait('@serviceMap'); + }); + + it('shows nodes in service map', { retries: 3 }, () => { + cy.wait(500); + cy.getByTestSubj('serviceMap').matchImage(); }); - it('opens detailed service map', () => { + it('shows nodes in detailed service map', () => { cy.visitKibana(detailedServiceMap); cy.contains('h1', 'opbeans-java'); + cy.wait(500); + cy.getByTestSubj('serviceMap').matchImage(); }); - describe('When there is no data', () => { + describe('when there is no data', () => { it('shows empty state', () => { - cy.visitKibana(serviceMapHref); // we need to dismiss the service-group call out first cy.contains('Dismiss').click(); cy.getByTestSubj('apmUnifiedSearchBar').type('_id : foo{enter}'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index c191a9add4e96..204d05255ac58 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -8,6 +8,7 @@ import 'cypress-real-events/support'; import { Interception } from 'cypress/types/net-stubbing'; import 'cypress-axe'; import moment from 'moment'; +import '@frsource/cypress-plugin-visual-regression-diff'; import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/axe-config'; import { ApmUsername } from '../../../server/test_helpers/create_apm_users/authentication'; diff --git a/x-pack/plugins/apm/ftr_e2e/setup_cypress_node_events.ts b/x-pack/plugins/apm/ftr_e2e/setup_cypress_node_events.ts index 5194a7aa549b2..9fe1c8608fad2 100644 --- a/x-pack/plugins/apm/ftr_e2e/setup_cypress_node_events.ts +++ b/x-pack/plugins/apm/ftr_e2e/setup_cypress_node_events.ts @@ -10,6 +10,8 @@ import { LogLevel, } from '@kbn/apm-synthtrace'; import { createEsClientForTesting } from '@kbn/test'; +// eslint-disable-next-line @kbn/imports/no_unresolvable_imports +import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/plugins'; import del from 'del'; import { some } from 'lodash'; import { Readable } from 'stream'; @@ -35,6 +37,8 @@ export function setupNodeEvents( synthtraceEsClient.pipeline(synthtraceEsClient.getDefaultPipeline(false)); + initPlugin(on, config); + on('task', { // send logs to node process log(message) { diff --git a/x-pack/plugins/apm/public/assistant_functions/get_apm_services_list.ts b/x-pack/plugins/apm/public/assistant_functions/get_apm_services_list.ts index fdbcfb94da650..9047f768449e5 100644 --- a/x-pack/plugins/apm/public/assistant_functions/get_apm_services_list.ts +++ b/x-pack/plugins/apm/public/assistant_functions/get_apm_services_list.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import type { RegisterFunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common/types'; +import { ServiceHealthStatus } from '../../common/service_health_status'; import { callApmApi } from '../services/rest/create_call_apm_api'; import { NON_EMPTY_STRING } from '../utils/non_empty_string_ref'; @@ -28,6 +29,7 @@ export function registerGetApmServicesListFunction({ ), parameters: { type: 'object', + additionalProperties: false, properties: { 'service.environment': { ...NON_EMPTY_STRING, @@ -44,15 +46,30 @@ export function registerGetApmServicesListFunction({ description: 'The end of the time range, in Elasticsearch date math, like `now-24h`.', }, + healthStatus: { + type: 'array', + description: 'Filter service list by health status', + additionalProperties: false, + additionalItems: false, + items: { + type: 'string', + enum: [ + ServiceHealthStatus.unknown, + ServiceHealthStatus.healthy, + ServiceHealthStatus.warning, + ServiceHealthStatus.critical, + ], + }, + }, }, required: ['start', 'end'], } as const, }, async ({ arguments: args }, signal) => { - return callApmApi('GET /internal/apm/assistant/get_services_list', { + return callApmApi('POST /internal/apm/assistant/get_services_list', { signal, params: { - query: args, + body: args, }, }); } diff --git a/x-pack/plugins/apm/public/assistant_functions/get_apm_timeseries.tsx b/x-pack/plugins/apm/public/assistant_functions/get_apm_timeseries.tsx index 9bdfd4b4789d8..56269d884ad84 100644 --- a/x-pack/plugins/apm/public/assistant_functions/get_apm_timeseries.tsx +++ b/x-pack/plugins/apm/public/assistant_functions/get_apm_timeseries.tsx @@ -140,9 +140,8 @@ export function registerGetApmTimeseriesFunction({ description: 'The name of the service', }, 'service.environment': { - ...NON_EMPTY_STRING, description: - 'The environment that the service is running in.', + 'The environment that the service is running in. If undefined, all environments will be included. Only use this if you have confirmed the environment that the service is running in.', }, filter: { type: 'string', @@ -160,12 +159,7 @@ export function registerGetApmTimeseriesFunction({ 'The offset. Right: 15m. 8h. 1d. Wrong: -15m. -8h. -1d.', }, }, - required: [ - 'service.name', - 'service.environment', - 'timeseries', - 'title', - ], + required: ['service.name', 'timeseries', 'title'], }, }, }, diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/error_count_rule_type/index.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/error_count_rule_type/index.tsx index b42a3eb3b528f..e8fa1b130b2d7 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/error_count_rule_type/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/error_count_rule_type/index.tsx @@ -106,7 +106,9 @@ export function ErrorCountRuleType(props: Props) { start, end, groupBy: params.groupBy, - searchConfiguration: JSON.stringify(params.searchConfiguration), + searchConfiguration: params.searchConfiguration?.query?.query + ? JSON.stringify(params.searchConfiguration) + : undefined, }, }, } diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_duration_rule_type/index.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_duration_rule_type/index.tsx index 900f90253a6e1..cd8abf26c74d4 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_duration_rule_type/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_duration_rule_type/index.tsx @@ -136,7 +136,9 @@ export function TransactionDurationRuleType(props: Props) { start, end, groupBy: params.groupBy, - searchConfiguration: JSON.stringify(params.searchConfiguration), + searchConfiguration: params.searchConfiguration?.query?.query + ? JSON.stringify(params.searchConfiguration) + : undefined, }, }, } diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx index cf1132e58b9e0..dd37092a260ef 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx @@ -108,7 +108,9 @@ export function TransactionErrorRateRuleType(props: Props) { start, end, groupBy: params.groupBy, - searchConfiguration: JSON.stringify(params.searchConfiguration), + searchConfiguration: params.searchConfiguration?.query?.query + ? JSON.stringify(params.searchConfiguration) + : undefined, }, }, } diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx index d3422fac11546..b9a286ac05187 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx @@ -213,7 +213,7 @@ export function ServiceMap({
diff --git a/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts b/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts index a620d3e0be017..83671f6b97fc9 100644 --- a/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts +++ b/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts @@ -51,7 +51,6 @@ export async function initDiagnosticsBundle({ const kibanaClient = axios.create({ baseURL: kbHost ?? kibanaHost, auth, - // @ts-expect-error headers: { 'kbn-xsrf': 'true', ...apiKeyHeader }, }); const apmIndices = await getApmIndices(kibanaClient); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts index 3054c9f5e8bc7..b5d361777b602 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts @@ -6,164 +6,153 @@ */ import { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; -import { - AggregatedTransactionsCounts, - APMUsage, - TimeframeMap, - TimeframeMap1d, - TimeframeMapAll, - APMPerService, -} from './types'; +import { AggregatedTransactionsCounts, APMUsage, APMPerService } from './types'; import { ElasticAgentName } from '../../../typings/es_schemas/ui/fields/agent'; -const long: { type: 'long' } = { type: 'long' }; - -const keyword: { type: 'keyword' } = { type: 'keyword' }; - -const aggregatedTransactionCountSchema: MakeSchemaFrom = - { - expected_metric_document_count: long, - transaction_count: long, - }; - -const timeframeMap1dSchema: MakeSchemaFrom = { - '1d': long, -}; - -const timeframeMapAllSchema: MakeSchemaFrom = { - all: long, -}; - -const timeframeMapSchema: MakeSchemaFrom = { - ...timeframeMap1dSchema, - ...timeframeMapAllSchema, -}; - -const agentSchema: MakeSchemaFrom['agents'][ElasticAgentName] = { - agent: { - version: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'An array of the top 3 agent versions within the last day', - }, - }, +const aggregatedTransactionCountSchema: MakeSchemaFrom< + AggregatedTransactionsCounts, + true +> = { + expected_metric_document_count: { + type: 'long', + _meta: { + description: '', }, - activation_method: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'An array of the top 3 agent activation methods within the last day', - }, - }, + }, + transaction_count: { + type: 'long', + _meta: { + description: '', }, }, - service: { - framework: { - name: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'An array of the top 3 service framework name within the last day', - }, - }, - }, +}; + +const agentSchema: MakeSchemaFrom['agents'][ElasticAgentName] = + { + agent: { version: { type: 'array', items: { type: 'keyword', _meta: { description: - 'An array of the top 3 service framework version within the last day', + 'An array of the top 3 agent versions within the last day', }, }, }, - composite: { + activation_method: { type: 'array', items: { type: 'keyword', _meta: { description: - 'Composite field containing service framework and version sorted by doc count', + 'An array of the top 3 agent activation methods within the last day', }, }, }, }, - language: { - name: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'An array of the top 3 service language name within the last day', + service: { + framework: { + name: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'An array of the top 3 service framework name within the last day', + }, }, }, - }, - version: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'An array of the top 3 service language version within the last day', + version: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'An array of the top 3 service framework version within the last day', + }, }, }, - }, - composite: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'Composite field containing service language name and version sorted by doc count.', + composite: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'Composite field containing service framework and version sorted by doc count', + }, }, }, }, - }, - runtime: { - name: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'An array of the top 3 service runtime name within the last day', + language: { + name: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'An array of the top 3 service language name within the last day', + }, }, }, - }, - version: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'An array of the top 3 service runtime version within the last day', + version: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'An array of the top 3 service language version within the last day', + }, + }, + }, + composite: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'Composite field containing service language name and version sorted by doc count.', + }, }, }, }, - composite: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: - 'Composite field containing service runtime name and version sorted by doc count.', + runtime: { + name: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'An array of the top 3 service runtime name within the last day', + }, + }, + }, + version: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'An array of the top 3 service runtime version within the last day', + }, + }, + }, + composite: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: + 'Composite field containing service runtime name and version sorted by doc count.', + }, }, }, }, }, - }, -}; + }; const apmPerAgentSchema: Pick< - MakeSchemaFrom, + MakeSchemaFrom, 'services_per_agent' | 'agents' > = { // services_per_agent: AGENT_NAMES.reduce( @@ -177,30 +166,174 @@ const apmPerAgentSchema: Pick< // TODO: Find a way for `@kbn/telemetry-tools` to understand and evaluate expressions. // In the meanwhile, we'll have to maintain these lists up to date (TS will remind us to update) services_per_agent: { - 'android/java': long, - dotnet: long, - 'iOS/swift': long, - go: long, - java: long, - 'js-base': long, - nodejs: long, - php: long, - python: long, - ruby: long, - 'rum-js': long, - otlp: long, - 'opentelemetry/cpp': long, - 'opentelemetry/dotnet': long, - 'opentelemetry/erlang': long, - 'opentelemetry/go': long, - 'opentelemetry/java': long, - 'opentelemetry/nodejs': long, - 'opentelemetry/php': long, - 'opentelemetry/python': long, - 'opentelemetry/ruby': long, - 'opentelemetry/rust': long, - 'opentelemetry/swift': long, - 'opentelemetry/webjs': long, + 'android/java': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the android/java agent within the last day', + }, + }, + dotnet: { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the dotnet (.Net) agent within the last day', + }, + }, + 'iOS/swift': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the iOS/swift agent within the last day', + }, + }, + go: { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the go agent within the last day', + }, + }, + java: { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the Java agent within the last day', + }, + }, + 'js-base': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the js-base agent within the last day', + }, + }, + nodejs: { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the nodeJS agent within the last day', + }, + }, + php: { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the PHH agent within the last day', + }, + }, + python: { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the Python agent within the last day', + }, + }, + ruby: { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the Ruby agent within the last day', + }, + }, + 'rum-js': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the rum-js agent within the last day', + }, + }, + otlp: { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the otlp agent within the last day', + }, + }, + 'opentelemetry/cpp': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/cpp agent within the last day', + }, + }, + 'opentelemetry/dotnet': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/dotnet agent within the last day', + }, + }, + 'opentelemetry/erlang': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/erlang agent within the last day', + }, + }, + 'opentelemetry/go': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/go agent within the last day', + }, + }, + 'opentelemetry/java': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/java agent within the last day', + }, + }, + 'opentelemetry/nodejs': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/nodejs agent within the last day', + }, + }, + 'opentelemetry/php': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/php agent within the last day', + }, + }, + 'opentelemetry/python': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/python agent within the last day', + }, + }, + 'opentelemetry/ruby': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/ruby agent within the last day', + }, + }, + 'opentelemetry/rust': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/rust agent within the last day', + }, + }, + 'opentelemetry/swift': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/swift agent within the last day', + }, + }, + 'opentelemetry/webjs': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/webjs agent within the last day', + }, + }, }, agents: { 'android/java': agentSchema, @@ -217,23 +350,23 @@ const apmPerAgentSchema: Pick< }, }; -export const apmPerServiceSchema: MakeSchemaFrom = { +export const apmPerServiceSchema: MakeSchemaFrom = { service_id: { - ...keyword, + type: 'keyword', _meta: { description: 'Unique identifier that combines the SHA256 hashed representation of the service name and environment', }, }, num_service_nodes: { - ...long, + type: 'long', _meta: { description: 'Total number of the unique service instances that served the transaction within an hour', }, }, num_transaction_types: { - ...long, + type: 'long', _meta: { description: 'Total number of the unique transaction types within an hour', @@ -293,21 +426,21 @@ export const apmPerServiceSchema: MakeSchemaFrom = { }, agent: { name: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of agent name for the service from transaction documents within an hour. Sorted by _score', }, }, version: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of agent version for the service from transaction documents within an hour. Sorted by _score', }, }, activation_method: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of agent activation method for the service from transaction documents within an hour. Sorted by _score', @@ -317,14 +450,14 @@ export const apmPerServiceSchema: MakeSchemaFrom = { service: { language: { name: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of language name for the service from transaction documents within an hour. Sorted by _score', }, }, version: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of language version for the service from transaction documents within an hour. Sorted by _score', @@ -333,14 +466,14 @@ export const apmPerServiceSchema: MakeSchemaFrom = { }, framework: { name: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of service framework name from transaction documents within an hour. Sorted by _score. Example AWS Lambda', }, }, version: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of service framework version from transaction documents within an hour. Sorted by _score', @@ -349,14 +482,14 @@ export const apmPerServiceSchema: MakeSchemaFrom = { }, runtime: { name: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of service runtime name from transaction documents within an hour. Sorted by _score', }, }, version: { - ...keyword, + type: 'keyword', _meta: { description: 'The top value of service runtime version version from transaction documents within an hour. Sorted by _score', @@ -367,16 +500,26 @@ export const apmPerServiceSchema: MakeSchemaFrom = { // No data found kubernetes: { pod: { - name: keyword, + name: { + type: 'keyword', + _meta: { + description: 'Kuberneted pod name ', + }, + }, }, }, // No data found container: { - id: keyword, + id: { + type: 'keyword', + _meta: { + description: 'Container id', + }, + }, }, }; -export const apmSchema: MakeSchemaFrom = { +export const apmSchema: MakeSchemaFrom = { ...apmPerAgentSchema, has_any_services: { type: 'boolean', @@ -388,19 +531,19 @@ export const apmSchema: MakeSchemaFrom = { version: { apm_server: { major: { - ...long, + type: 'long', _meta: { description: 'The major version of the APM server. Example: 7', }, }, minor: { - ...long, + type: 'long', _meta: { description: 'The minor version of the APM server. Example: 17', }, }, patch: { - ...long, + type: 'long', _meta: { description: 'The patch version of the APM server. Example 3', }, @@ -409,14 +552,14 @@ export const apmSchema: MakeSchemaFrom = { }, environments: { services_without_environment: { - ...long, + type: 'long', _meta: { description: 'Number of services without an assigned environment within the last day. This is determined by checking the "service.environment" field and counting instances where it is null', }, }, services_with_multiple_environments: { - ...long, + type: 'long', _meta: { description: 'Number of services with more than one assigned environment within the last day', @@ -433,7 +576,6 @@ export const apmSchema: MakeSchemaFrom = { }, }, }, - // #NOTE No task identified for extracting the following information aggregated_transactions: { current_implementation: aggregatedTransactionCountSchema, no_observer_name: aggregatedTransactionCountSchema, @@ -491,14 +633,14 @@ export const apmSchema: MakeSchemaFrom = { counts: { transaction: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of transaction documents within the last day', }, }, all: { - ...long, + type: 'long', _meta: { description: 'Total number of transaction documents overall', }, @@ -506,13 +648,13 @@ export const apmSchema: MakeSchemaFrom = { }, span: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of span documents within the last day', }, }, all: { - ...long, + type: 'long', _meta: { description: 'Total number of span documents overall', }, @@ -520,13 +662,13 @@ export const apmSchema: MakeSchemaFrom = { }, error: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of error documents within the last day', }, }, all: { - ...long, + type: 'long', _meta: { description: 'Total number of error documents overall', }, @@ -534,13 +676,13 @@ export const apmSchema: MakeSchemaFrom = { }, metric: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of metric documents within the last day', }, }, all: { - ...long, + type: 'long', _meta: { description: 'Total number of metric documents overall', }, @@ -548,14 +690,14 @@ export const apmSchema: MakeSchemaFrom = { }, onboarding: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of onboarding documents within the last day', }, }, all: { - ...long, + type: 'long', _meta: { description: 'Total number of onboarding documents overall', }, @@ -563,7 +705,7 @@ export const apmSchema: MakeSchemaFrom = { }, agent_configuration: { all: { - ...long, + type: 'long', _meta: { description: 'Total number of apm-agent-configuration documents overall', @@ -572,7 +714,7 @@ export const apmSchema: MakeSchemaFrom = { }, max_transaction_groups_per_service: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of distinct transaction groups for the top service for the last 24 hours', @@ -581,7 +723,7 @@ export const apmSchema: MakeSchemaFrom = { }, max_error_groups_per_service: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of distinct error groups for the top service for the last 24 hours', @@ -590,23 +732,29 @@ export const apmSchema: MakeSchemaFrom = { }, traces: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of trace documents within the last day', }, }, all: { - ...long, + type: 'long', _meta: { description: 'Total number of trace documents overall', }, }, }, - // No tasks found - services: timeframeMapSchema, + services: { + '1d': { + type: 'long', + _meta: { + description: 'Total number of unique services within the last day', + }, + }, + }, environments: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of unique environments within the last day', @@ -615,7 +763,7 @@ export const apmSchema: MakeSchemaFrom = { }, span_destination_service_resource: { '1d': { - ...long, + type: 'long', _meta: { description: 'Total number of unique values of span.destination.service.resource within the last day', @@ -630,7 +778,7 @@ export const apmSchema: MakeSchemaFrom = { country_iso_code: { rum: { '1d': { - ...long, + type: 'long', _meta: { description: 'Unique country iso code captured for the agents js-base, rum-js and opentelemetry/webjs within the last day', @@ -644,7 +792,7 @@ export const apmSchema: MakeSchemaFrom = { original: { all_agents: { '1d': { - ...long, + type: 'long', _meta: { description: 'Unique user agent for all agents within the last day', @@ -653,7 +801,7 @@ export const apmSchema: MakeSchemaFrom = { }, rum: { '1d': { - ...long, + type: 'long', _meta: { description: 'Unique user agent for rum agent within the last day', @@ -666,7 +814,7 @@ export const apmSchema: MakeSchemaFrom = { name: { all_agents: { '1d': { - ...long, + type: 'long', _meta: { description: 'Unique transaction names for all agents within the last day', @@ -675,7 +823,7 @@ export const apmSchema: MakeSchemaFrom = { }, rum: { '1d': { - ...long, + type: 'long', _meta: { description: 'Unique transaction names for rum agent within the last day', @@ -689,7 +837,7 @@ export const apmSchema: MakeSchemaFrom = { retainment: { span: { ms: { - ...long, + type: 'long', _meta: { description: 'Represent the time difference in milliseconds between the current date and the date when the span document was recorded', @@ -698,7 +846,7 @@ export const apmSchema: MakeSchemaFrom = { }, transaction: { ms: { - ...long, + type: 'long', _meta: { description: 'Represent the time difference in milliseconds between the current date and the date when the transaction document was recorded', @@ -707,7 +855,7 @@ export const apmSchema: MakeSchemaFrom = { }, error: { ms: { - ...long, + type: 'long', _meta: { description: 'Represent the time difference in milliseconds between the current date and the date when the error document was recorded', @@ -716,7 +864,7 @@ export const apmSchema: MakeSchemaFrom = { }, metric: { ms: { - ...long, + type: 'long', _meta: { description: 'Represent the time difference in milliseconds between the current date and the date when the metric document was recorded', @@ -725,7 +873,7 @@ export const apmSchema: MakeSchemaFrom = { }, onboarding: { ms: { - ...long, + type: 'long', _meta: { description: 'Represent the time difference in milliseconds between the current date and the date when the onboarding document was recorded', @@ -736,7 +884,7 @@ export const apmSchema: MakeSchemaFrom = { integrations: { ml: { all_jobs_count: { - ...long, + type: 'long', _meta: { description: 'Total number of anomaly detection jobs associated with the jobs apm-*, *-high_mean_response_time', @@ -746,23 +894,44 @@ export const apmSchema: MakeSchemaFrom = { }, indices: { - // cannot find related data metric: { - shards: { total: long }, + shards: { + total: { + type: 'long', + _meta: { + description: 'Total number of shards for metric indices', + }, + }, + }, all: { total: { - docs: { count: long }, - store: { size_in_bytes: long }, + docs: { + count: { + type: 'long', + _meta: { + description: 'Total number of metric documents overall', + }, + }, + }, + store: { + size_in_bytes: { + type: 'long', + _meta: { + description: + 'Size of the metric indicess in byte units overall.', + }, + }, + }, }, }, }, - // cannot find related data traces: { shards: { total: { - ...long, + type: 'long', _meta: { - description: 'Total number of shards overall', + description: + 'Total number of shards for span and trasnaction indices', }, }, }, @@ -770,7 +939,7 @@ export const apmSchema: MakeSchemaFrom = { total: { docs: { count: { - ...long, + type: 'long', _meta: { description: 'Total number of transaction and span documents overall', @@ -779,7 +948,7 @@ export const apmSchema: MakeSchemaFrom = { }, store: { size_in_bytes: { - ...long, + type: 'long', _meta: { description: 'Size of the index in byte units overall.', }, @@ -790,7 +959,7 @@ export const apmSchema: MakeSchemaFrom = { }, shards: { total: { - ...long, + type: 'long', _meta: { description: 'Total number of shards overall', }, @@ -800,7 +969,7 @@ export const apmSchema: MakeSchemaFrom = { total: { docs: { count: { - ...long, + type: 'long', _meta: { description: 'Total number of all documents overall', }, @@ -808,7 +977,7 @@ export const apmSchema: MakeSchemaFrom = { }, store: { size_in_bytes: { - ...long, + type: 'long', _meta: { description: 'Size of the index in byte units overall.', }, @@ -829,7 +998,7 @@ export const apmSchema: MakeSchemaFrom = { }, }, total: { - ...long, + type: 'long', _meta: { description: 'Total number of service groups retrived from the saved object across all spaces', @@ -841,7 +1010,7 @@ export const apmSchema: MakeSchemaFrom = { aggregated_transactions: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "aggregated_transactions" task', @@ -852,7 +1021,7 @@ export const apmSchema: MakeSchemaFrom = { cloud: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "cloud" task', }, @@ -862,7 +1031,7 @@ export const apmSchema: MakeSchemaFrom = { host: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "host" task', }, @@ -872,7 +1041,7 @@ export const apmSchema: MakeSchemaFrom = { processor_events: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "processor_events" task', @@ -883,7 +1052,7 @@ export const apmSchema: MakeSchemaFrom = { agent_configuration: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "agent_configuration" task', @@ -894,7 +1063,7 @@ export const apmSchema: MakeSchemaFrom = { services: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "services" task', @@ -905,7 +1074,7 @@ export const apmSchema: MakeSchemaFrom = { versions: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "versions" task', @@ -916,7 +1085,7 @@ export const apmSchema: MakeSchemaFrom = { groupings: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "groupings" task', @@ -927,7 +1096,7 @@ export const apmSchema: MakeSchemaFrom = { integrations: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "integrations" task', @@ -938,7 +1107,7 @@ export const apmSchema: MakeSchemaFrom = { agents: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "agents" task', }, @@ -948,7 +1117,7 @@ export const apmSchema: MakeSchemaFrom = { indices_stats: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "indices_stats" task', @@ -959,7 +1128,7 @@ export const apmSchema: MakeSchemaFrom = { cardinality: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "cardinality" task', @@ -970,7 +1139,7 @@ export const apmSchema: MakeSchemaFrom = { environments: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "environments" task', @@ -981,7 +1150,7 @@ export const apmSchema: MakeSchemaFrom = { service_groups: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "service_groups" task', @@ -992,7 +1161,7 @@ export const apmSchema: MakeSchemaFrom = { per_service: { took: { ms: { - ...long, + type: 'long', _meta: { description: 'Execution time in milliseconds for the "per_service" task', diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts index e88e021fbd0cb..9c6c312c284f4 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts @@ -107,7 +107,7 @@ export interface APMUsage { max_transaction_groups_per_service: TimeframeMap1d; max_error_groups_per_service: TimeframeMap1d; traces: TimeframeMap; - services: TimeframeMap; + services: TimeframeMap1d; environments: TimeframeMap1d; span_destination_service_resource: TimeframeMap1d; }; diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index f0843516e4c9a..ba33057e99190 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -117,7 +117,7 @@ export function registerErrorCountRuleType({ const indices = await getApmIndices(savedObjectsClient); - const termFilterQuery = !ruleParams.searchConfiguration + const termFilterQuery = !ruleParams.searchConfiguration?.query?.query ? [ ...termQuery(SERVICE_NAME, ruleParams.serviceName, { queryEmptyString: false, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 7a1beb3ba9f9e..3570b93cd7039 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -135,7 +135,7 @@ export function registerTransactionDurationRuleType({ searchAggregatedTransactions ); - const termFilterQuery = !ruleParams.searchConfiguration + const termFilterQuery = !ruleParams.searchConfiguration?.query?.query ? [ ...termQuery(SERVICE_NAME, ruleParams.serviceName, { queryEmptyString: false, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 7015862db8d9e..764e251edd6a7 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -137,7 +137,7 @@ export function registerTransactionErrorRateRuleType({ ? indices.metric : indices.transaction; - const termFilterQuery = !ruleParams.searchConfiguration + const termFilterQuery = !ruleParams.searchConfiguration?.query?.query ? [ ...termQuery(SERVICE_NAME, ruleParams.serviceName, { queryEmptyString: false, diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts index c460acf4c3b3f..ffb1158270455 100644 --- a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts @@ -54,14 +54,14 @@ export async function getApmErrorDocument({ }, }); - const error = response.hits.hits[0]?._source as APMError; + const errorDoc = response.hits.hits[0]?._source as APMError; - if (!error) { + if (!errorDoc) { return undefined; } - return pick( - error, + const formattedResponse = pick( + errorDoc, 'message', 'error', '@timestamp', @@ -71,4 +71,11 @@ export async function getApmErrorDocument({ 'span.type', 'span.subtype' ); + + const { error, ...rest } = formattedResponse; + + return { + ...rest, + errorDoc: formattedResponse.error, + }; } diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/index.ts index 0c95cf0231368..7d0b4e2c3a434 100644 --- a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/index.ts +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/index.ts @@ -14,7 +14,6 @@ import { environmentQuery } from '../../../../common/utils/environment_query'; import { getBucketSize } from '../../../../common/utils/get_bucket_size'; import { termQuery } from '../../../../common/utils/term_query'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import { environmentRt } from '../../default_api_types'; import { getErrorEventRate } from './get_error_event_rate'; import { getExitSpanFailureRate } from './get_exit_span_failure_rate'; import { getExitSpanLatency } from './get_exit_span_latency'; @@ -37,7 +36,6 @@ export const getApmTimeseriesRt = t.type({ stats: t.array( t.intersection([ t.type({ - 'service.environment': environmentRt.props.environment, 'service.name': t.string, title: t.string, timeseries: t.union([ @@ -85,6 +83,7 @@ export const getApmTimeseriesRt = t.type({ t.partial({ filter: t.string, offset: t.string, + 'service.environment': t.string, }), ]) ), diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/route.ts b/x-pack/plugins/apm/server/routes/assistant_functions/route.ts index 1d02003c5e46c..436f514e423ff 100644 --- a/x-pack/plugins/apm/server/routes/assistant_functions/route.ts +++ b/x-pack/plugins/apm/server/routes/assistant_functions/route.ts @@ -18,7 +18,6 @@ import { getApmEventClient } from '../../lib/helpers/get_apm_event_client'; import { getMlClient } from '../../lib/helpers/get_ml_client'; import { getRandomSampler } from '../../lib/helpers/get_random_sampler'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; -import { environmentRt } from '../default_api_types'; import { getServicesItems } from '../services/get_services/get_services_items'; import { CorrelationValue, @@ -26,20 +25,20 @@ import { getApmCorrelationValues, } from './get_apm_correlation_values'; import { - type APMDownstreamDependency, downstreamDependenciesRouteRt, getAssistantDownstreamDependencies, + type APMDownstreamDependency, } from './get_apm_downstream_dependencies'; import { errorRouteRt, getApmErrorDocument } from './get_apm_error_document'; import { getApmServiceSummary, - type ServiceSummary, serviceSummaryRouteRt, + type ServiceSummary, } from './get_apm_service_summary'; import { - type ApmTimeseries, getApmTimeseries, getApmTimeseriesRt, + type ApmTimeseries, } from './get_apm_timeseries'; const getApmTimeSeriesRoute = createApmServerRoute({ @@ -204,15 +203,23 @@ interface ApmServicesListItem { type ApmServicesListContent = ApmServicesListItem[]; const getApmServicesListRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/assistant/get_services_list', + endpoint: 'POST /internal/apm/assistant/get_services_list', params: t.type({ - query: t.intersection([ + body: t.intersection([ t.type({ start: t.string, end: t.string, }), t.partial({ - 'service.environment': environmentRt.props.environment, + 'service.environment': t.string, + healthStatus: t.array( + t.union([ + t.literal(ServiceHealthStatus.unknown), + t.literal(ServiceHealthStatus.healthy), + t.literal(ServiceHealthStatus.warning), + t.literal(ServiceHealthStatus.critical), + ]) + ), }), ]), }), @@ -221,7 +228,9 @@ const getApmServicesListRoute = createApmServerRoute({ }, handler: async (resources): Promise<{ content: ApmServicesListContent }> => { const { params } = resources; - const { query } = params; + const { body } = params; + + const { healthStatus } = body; const [apmEventClient, apmAlertsClient, mlClient, randomSampler] = await Promise.all([ @@ -235,8 +244,8 @@ const getApmServicesListRoute = createApmServerRoute({ }), ]); - const start = datemath.parse(query.start)?.valueOf()!; - const end = datemath.parse(query.end)?.valueOf()!; + const start = datemath.parse(body.start)?.valueOf()!; + const end = datemath.parse(body.end)?.valueOf()!; const serviceItems = await getServicesItems({ apmAlertsClient, @@ -244,7 +253,7 @@ const getApmServicesListRoute = createApmServerRoute({ documentType: ApmDocumentType.TransactionMetric, start, end, - environment: query['service.environment'] ?? ENVIRONMENT_ALL.value, + environment: body['service.environment'] || ENVIRONMENT_ALL.value, kuery: '', logger: resources.logger, randomSampler, @@ -253,7 +262,7 @@ const getApmServicesListRoute = createApmServerRoute({ mlClient, }); - const mappedItems = serviceItems.items.map((item): ApmServicesListItem => { + let mappedItems = serviceItems.items.map((item): ApmServicesListItem => { return { 'service.name': item.serviceName, 'agent.name': item.agentName, @@ -264,6 +273,12 @@ const getApmServicesListRoute = createApmServerRoute({ }; }); + if (healthStatus && healthStatus.length) { + mappedItems = mappedItems.filter((item): boolean => + healthStatus.includes(item.healthStatus) + ); + } + return { content: mappedItems, }; diff --git a/x-pack/plugins/apm/server/routes/fleet/source_maps.ts b/x-pack/plugins/apm/server/routes/fleet/source_maps.ts index 4eb0b011cef3c..ad2bc870c7f33 100644 --- a/x-pack/plugins/apm/server/routes/fleet/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/fleet/source_maps.ts @@ -19,18 +19,12 @@ import { getPackagePolicyWithSourceMap } from './get_package_policy_decorators'; const doUnzip = promisify(unzip); -interface ApmMapArtifactBody { +interface ApmSourceMapArtifactBody { serviceName: string; serviceVersion: string; bundleFilepath: string; - sourceMap: string; -} - -interface ApmSourceMapArtifactBody - extends Omit { sourceMap: SourceMap; } - export type ArtifactSourceMap = Omit & { body: ApmSourceMapArtifactBody; }; @@ -110,23 +104,6 @@ export async function createFleetSourceMapArtifact({ }); } -export async function createFleetAndroidMapArtifact({ - apmArtifactBody, - fleetPluginStart, -}: { - apmArtifactBody: ApmMapArtifactBody; - fleetPluginStart: FleetPluginStart; -}) { - const apmArtifactClient = getApmArtifactClient(fleetPluginStart); - const identifier = `${apmArtifactBody.serviceName}-${apmArtifactBody.serviceVersion}-android`; - - return apmArtifactClient.createArtifact({ - type: 'sourcemap', - identifier, - content: JSON.stringify(apmArtifactBody), - }); -} - export async function deleteFleetSourcemapArtifact({ id, fleetPluginStart, diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/route.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/route.ts index 434ae4785ce5a..3698a13a1f2a6 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/route.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/route.ts @@ -8,12 +8,12 @@ import * as t from 'io-ts'; import { SavedObject } from '@kbn/core/server'; import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; +import { saveApmIndices } from '@kbn/apm-data-access-plugin/server/saved_objects/apm_indices'; import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; import { getApmIndexSettings, ApmIndexSettingsResponse, } from './get_apm_indices'; -import { saveApmIndices } from './save_apm_indices'; // get list of apm indices and values const apmIndexSettingsRoute = createApmServerRoute({ diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts deleted file mode 100644 index e9d2bd5fbea92..0000000000000 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.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 { SavedObjectsClientContract } from '@kbn/core/server'; -import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; -import { - APMIndicesSavedObjectBody, - APM_INDEX_SETTINGS_SAVED_OBJECT_ID, - APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, -} from '@kbn/apm-data-access-plugin/server/saved_objects/apm_indices'; -import { withApmSpan } from '../../../utils/with_apm_span'; - -export function saveApmIndices( - savedObjectsClient: SavedObjectsClientContract, - apmIndices: Partial -) { - return withApmSpan('save_apm_indices', () => - savedObjectsClient.create( - APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, - { apmIndices: removeEmpty(apmIndices), isSpaceAware: true }, - { id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, overwrite: true } - ) - ); -} - -// remove empty/undefined values -function removeEmpty(apmIndices: Partial) { - return Object.entries(apmIndices) - .map(([key, value]) => [key, value?.trim()]) - .filter(([_, value]) => !!value) - .reduce((obj, [key, value]) => { - obj[key] = value; - return obj; - }, {} as Record); -} diff --git a/x-pack/plugins/apm/server/routes/source_maps/bulk_create_apm_source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps/bulk_create_apm_source_maps.ts index 3aa5c9a780aa3..1495f89f0fceb 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/bulk_create_apm_source_maps.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/bulk_create_apm_source_maps.ts @@ -10,7 +10,7 @@ import { Artifact } from '@kbn/fleet-plugin/server'; import { getUnzippedArtifactBody } from '../fleet/source_maps'; import { APM_SOURCE_MAP_INDEX } from '../settings/apm_indices/apm_system_index_constants'; import { ApmSourceMap } from './create_apm_source_map_index_template'; -import { getEncodedSourceMapContent, getSourceMapId } from './sourcemap_utils'; +import { getEncodedContent, getSourceMapId } from './sourcemap_utils'; export async function bulkCreateApmSourceMaps({ artifacts, @@ -24,7 +24,7 @@ export async function bulkCreateApmSourceMaps({ const { serviceName, serviceVersion, bundleFilepath, sourceMap } = await getUnzippedArtifactBody(artifact.body); - const { contentEncoded, contentHash } = await getEncodedSourceMapContent( + const { contentEncoded, contentHash } = await getEncodedContent( sourceMap ); diff --git a/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map.ts b/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map.ts index bda2601080434..95614778024fa 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map.ts @@ -10,11 +10,7 @@ import { Logger } from '@kbn/core/server'; import { APM_SOURCE_MAP_INDEX } from '../settings/apm_indices/apm_system_index_constants'; import { ApmSourceMap } from './create_apm_source_map_index_template'; import { SourceMap } from './route'; -import { - getEncodedSourceMapContent, - getEncodedContent, - getSourceMapId, -} from './sourcemap_utils'; +import { getEncodedContent, getSourceMapId } from './sourcemap_utils'; export async function createApmSourceMap({ internalESClient, @@ -35,75 +31,9 @@ export async function createApmSourceMap({ serviceName: string; serviceVersion: string; }) { - const { contentEncoded, contentHash } = await getEncodedSourceMapContent( + const { contentEncoded, contentHash } = await getEncodedContent( sourceMapContent ); - return await doCreateApmMap({ - internalESClient, - logger, - fleetId, - created, - bundleFilepath, - serviceName, - serviceVersion, - contentEncoded, - contentHash, - }); -} - -export async function createApmAndroidMap({ - internalESClient, - logger, - fleetId, - created, - mapContent, - bundleFilepath, - serviceName, - serviceVersion, -}: { - internalESClient: ElasticsearchClient; - logger: Logger; - fleetId: string; - created: string; - mapContent: string; - bundleFilepath: string; - serviceName: string; - serviceVersion: string; -}) { - const { contentEncoded, contentHash } = await getEncodedContent(mapContent); - return await doCreateApmMap({ - internalESClient, - logger, - fleetId, - created, - bundleFilepath, - serviceName, - serviceVersion, - contentEncoded, - contentHash, - }); -} -async function doCreateApmMap({ - internalESClient, - logger, - fleetId, - created, - bundleFilepath, - serviceName, - serviceVersion, - contentEncoded, - contentHash, -}: { - internalESClient: ElasticsearchClient; - logger: Logger; - fleetId: string; - created: string; - bundleFilepath: string; - serviceName: string; - serviceVersion: string; - contentEncoded: string; - contentHash: string; -}) { const doc: ApmSourceMap = { fleet_id: fleetId, created, diff --git a/x-pack/plugins/apm/server/routes/source_maps/route.ts b/x-pack/plugins/apm/server/routes/source_maps/route.ts index c0cc2404af492..ae29267644668 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/route.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/route.ts @@ -9,24 +9,19 @@ import { SavedObjectsClientContract } from '@kbn/core/server'; import { Artifact } from '@kbn/fleet-plugin/server'; import { jsonRt, toNumberRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; -import { either } from 'fp-ts/lib/Either'; import { ApmFeatureFlags } from '../../../common/apm_feature_flags'; import { getInternalSavedObjectsClient } from '../../lib/helpers/get_internal_saved_objects_client'; import { stringFromBufferRt } from '../../utils/string_from_buffer_rt'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { createFleetSourceMapArtifact, - createFleetAndroidMapArtifact, deleteFleetSourcemapArtifact, getCleanedBundleFilePath, listSourceMapArtifacts, ListSourceMapArtifactsResponse, updateSourceMapsOnFleetPolicies, } from '../fleet/source_maps'; -import { - createApmSourceMap, - createApmAndroidMap, -} from './create_apm_source_map'; +import { createApmSourceMap } from './create_apm_source_map'; import { deleteApmSourceMap } from './delete_apm_sourcemap'; import { runFleetSourcemapArtifactsMigration } from './schedule_source_map_migration'; @@ -46,24 +41,6 @@ export const sourceMapRt = t.intersection([ export type SourceMap = t.TypeOf; -const androidMapValidation = new t.Type( - 'ANDROID_MAP_VALIDATION', - t.string.is, - (input, context): t.Validation => - either.chain( - t.string.validate(input, context), - (str): t.Validation => { - const firstLine = str.split('\n', 1)[0]; - if (firstLine.trim() === '# compiler: R8') { - return t.success(str); - } else { - return t.failure(input, context); - } - } - ), - (a): string => a -); - function throwNotImplementedIfSourceMapNotAvailable( featureFlags: ApmFeatureFlags ): void { @@ -114,7 +91,7 @@ const uploadSourceMapRoute = createApmServerRoute({ endpoint: 'POST /api/apm/sourcemaps 2023-10-31', options: { tags: ['access:apm', 'access:apm_write'], - body: { accepts: ['multipart/form-data'], maxBytes: 100 * 1024 * 1024 }, + body: { accepts: ['multipart/form-data'] }, }, params: t.type({ body: t.type({ @@ -192,85 +169,6 @@ const uploadSourceMapRoute = createApmServerRoute({ }, }); -const uploadAndroidMapRoute = createApmServerRoute({ - endpoint: 'POST /api/apm/androidmaps 2023-10-31', - options: { - tags: ['access:apm', 'access:apm_write'], - body: { accepts: ['multipart/form-data'], maxBytes: 100 * 1024 * 1024 }, - }, - params: t.type({ - body: t.type({ - service_name: t.string, - service_version: t.string, - map_file: t - .union([t.string, stringFromBufferRt]) - .pipe(androidMapValidation), - }), - }), - handler: async ({ - params, - plugins, - core, - logger, - featureFlags, - }): Promise => { - throwNotImplementedIfSourceMapNotAvailable(featureFlags); - - const { - service_name: serviceName, - service_version: serviceVersion, - map_file: sourceMapContent, - } = params.body; - const bundleFilepath = 'android'; - const fleetPluginStart = await plugins.fleet?.start(); - const coreStart = await core.start(); - const internalESClient = coreStart.elasticsearch.client.asInternalUser; - const savedObjectsClient = await getInternalSavedObjectsClient(coreStart); - try { - if (fleetPluginStart) { - // create source map as fleet artifact - const artifact = await createFleetAndroidMapArtifact({ - fleetPluginStart, - apmArtifactBody: { - serviceName, - serviceVersion, - bundleFilepath, - sourceMap: sourceMapContent, - }, - }); - - // sync source map to APM managed index - await createApmAndroidMap({ - internalESClient, - logger, - fleetId: artifact.id, - created: artifact.created, - mapContent: sourceMapContent, - bundleFilepath, - serviceName, - serviceVersion, - }); - - // sync source map to fleet policy - await updateSourceMapsOnFleetPolicies({ - coreStart, - fleetPluginStart, - savedObjectsClient: - savedObjectsClient as unknown as SavedObjectsClientContract, - internalESClient, - }); - - return artifact; - } - } catch (e) { - throw Boom.internal( - 'Something went wrong while creating a new android map', - e - ); - } - }, -}); - const deleteSourceMapRoute = createApmServerRoute({ endpoint: 'DELETE /api/apm/sourcemaps/{id} 2023-10-31', options: { tags: ['access:apm', 'access:apm_write'] }, @@ -332,6 +230,5 @@ export const sourceMapsRouteRepository = { ...listSourceMapRoute, ...uploadSourceMapRoute, ...deleteSourceMapRoute, - ...uploadAndroidMapRoute, ...migrateFleetArtifactsSourceMapRoute, }; diff --git a/x-pack/plugins/apm/server/routes/source_maps/sourcemap_utils.ts b/x-pack/plugins/apm/server/routes/source_maps/sourcemap_utils.ts index 408d1b98f9c5a..20ff2fa4bd41c 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/sourcemap_utils.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/sourcemap_utils.ts @@ -16,11 +16,8 @@ function asSha256Encoded(content: BinaryLike): string { return createHash('sha256').update(content).digest('hex'); } -export async function getEncodedSourceMapContent(sourceMapContent: SourceMap) { - return getEncodedContent(JSON.stringify(sourceMapContent)); -} -export async function getEncodedContent(textContent: string) { - const contentBuffer = Buffer.from(textContent); +export async function getEncodedContent(sourceMapContent: SourceMap) { + const contentBuffer = Buffer.from(JSON.stringify(sourceMapContent)); const contentZipped = await deflateAsync(contentBuffer); const contentEncoded = contentZipped.toString('base64'); const contentHash = asSha256Encoded(contentZipped); diff --git a/x-pack/plugins/apm_data_access/server/saved_objects/apm_indices.ts b/x-pack/plugins/apm_data_access/server/saved_objects/apm_indices.ts index 7ab90ef0a605c..96b9c31d6b91c 100644 --- a/x-pack/plugins/apm_data_access/server/saved_objects/apm_indices.ts +++ b/x-pack/plugins/apm_data_access/server/saved_objects/apm_indices.ts @@ -11,6 +11,7 @@ import { schema } from '@kbn/config-schema'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { SavedObjectsClientContract } from '@kbn/core/server'; import { updateApmOssIndexPaths } from './migrations/update_apm_oss_index_paths'; +import { APMIndices } from '..'; export const APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE = 'apm-indices'; export const APM_INDEX_SETTINGS_SAVED_OBJECT_ID = 'apm-indices'; @@ -73,6 +74,28 @@ export const apmIndicesSavedObjectDefinition: SavedObjectsType = { }, }; +export function saveApmIndices( + savedObjectsClient: SavedObjectsClientContract, + apmIndices: Partial +) { + return savedObjectsClient.create( + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + { apmIndices: removeEmpty(apmIndices), isSpaceAware: true }, + { id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, overwrite: true } + ); +} + +// remove empty/undefined values +function removeEmpty(apmIndices: Partial) { + return Object.entries(apmIndices) + .map(([key, value]) => [key, value?.trim()]) + .filter(([_, value]) => !!value) + .reduce((obj, [key, value]) => { + obj[key] = value; + return obj; + }, {} as Record); +} + export async function getApmIndicesSavedObject(savedObjectsClient: SavedObjectsClientContract) { try { const apmIndicesSavedObject = await savedObjectsClient.get>( diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts b/x-pack/plugins/apm_data_access/server/saved_objects/save_apm_indices.test.ts similarity index 95% rename from x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts rename to x-pack/plugins/apm_data_access/server/saved_objects/save_apm_indices.test.ts index e72282ba6275a..22278e6c16b56 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts +++ b/x-pack/plugins/apm_data_access/server/saved_objects/save_apm_indices.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { saveApmIndices } from './save_apm_indices'; +import { saveApmIndices } from './apm_indices'; import { SavedObjectsClientContract } from '@kbn/core/server'; describe('saveApmIndices', () => { diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index fdd10cbb5bf94..4283adf4c081a 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -50,13 +50,15 @@ export { INTERNAL_BULK_CREATE_ATTACHMENTS_URL, SAVED_OBJECT_TYPES, CASE_COMMENT_SAVED_OBJECT, + CASES_CONNECTORS_CAPABILITY, + GET_CONNECTORS_CONFIGURE_API_TAG, } from './constants'; export type { AttachmentAttributes } from './types/domain'; export { ConnectorTypes, AttachmentType, ExternalReferenceStorageType } from './types/domain'; export { getCasesFromAlertsUrl, getCaseFindUserActionsUrl, throwErrors } from './api'; export { StatusAll } from './ui/types'; -export { createUICapabilities } from './utils/capabilities'; -export { getApiTags } from './utils/api_tags'; +export { createUICapabilities, type CasesUiCapabilities } from './utils/capabilities'; +export { getApiTags, type CasesApiTags } from './utils/api_tags'; export { CaseMetricsFeature } from './types/api'; export type { SingleCaseMetricsResponse, CasesMetricsResponse } from './types/api'; diff --git a/x-pack/plugins/cases/common/utils/api_tags.ts b/x-pack/plugins/cases/common/utils/api_tags.ts index 2568c0e79b9a0..3fbad714e55f9 100644 --- a/x-pack/plugins/cases/common/utils/api_tags.ts +++ b/x-pack/plugins/cases/common/utils/api_tags.ts @@ -14,7 +14,13 @@ import { HttpApiTagOperation } from '../constants/types'; import type { Owner } from '../constants/types'; import { constructFilesHttpOperationTag } from '../files'; -export const getApiTags = (owner: Owner) => { +export interface CasesApiTags { + all: readonly string[]; + read: readonly string[]; + delete: readonly string[]; +} + +export const getApiTags = (owner: Owner): CasesApiTags => { const create = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Create); const deleteTag = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Delete); const read = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Read); diff --git a/x-pack/plugins/cases/common/utils/capabilities.ts b/x-pack/plugins/cases/common/utils/capabilities.ts index e9c05eda47171..28b3fa00f9272 100644 --- a/x-pack/plugins/cases/common/utils/capabilities.ts +++ b/x-pack/plugins/cases/common/utils/capabilities.ts @@ -14,11 +14,16 @@ import { UPDATE_CASES_CAPABILITY, } from '../constants'; +export interface CasesUiCapabilities { + all: readonly string[]; + read: readonly string[]; + delete: readonly string[]; +} /** * Return the UI capabilities for each type of operation. These strings must match the values defined in the UI * here: x-pack/plugins/cases/public/client/helpers/capabilities.ts */ -export const createUICapabilities = () => ({ +export const createUICapabilities = (): CasesUiCapabilities => ({ all: [ CREATE_CASES_CAPABILITY, READ_CASES_CAPABILITY, diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index f912ce26f581e..8fd0a61c35f30 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -11,7 +11,7 @@ import type { ISavedObjectsSerializer } from '@kbn/core-saved-objects-server'; import { createFileServiceMock } from '@kbn/files-plugin/server/mocks'; import { securityMock } from '@kbn/security-plugin/server/mocks'; -import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client.mock'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; import { makeLensEmbeddableFactory } from '@kbn/lens-plugin/server/embeddable/make_lens_embeddable_factory'; import { serializerMock } from '@kbn/core-saved-objects-base-server-mocks'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx index d0b6412eaa393..a9b8fdaa2f190 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx @@ -279,9 +279,7 @@ const VulnerabilitiesDataGrid = ({ [pageSize, setUrlQuery] ); - const showVulnerabilityFlyout = flyoutVulnerabilityIndex - ? flyoutVulnerabilityIndex > invalidIndex - : undefined; + const showVulnerabilityFlyout = flyoutVulnerabilityIndex > invalidIndex; if (data?.page.length === 0) { return ; diff --git a/x-pack/plugins/cloud_security_posture/server/config.ts b/x-pack/plugins/cloud_security_posture/server/config.ts index 472f66c78be8c..735cdb3cbe00f 100644 --- a/x-pack/plugins/cloud_security_posture/server/config.ts +++ b/x-pack/plugins/cloud_security_posture/server/config.ts @@ -4,14 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { schema, type TypeOf } from '@kbn/config-schema'; import type { PluginConfigDescriptor } from '@kbn/core/server'; +import { schema, offeringBasedSchema, type TypeOf } from '@kbn/config-schema'; + const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + + // Setting only allowed in the Serverless offering + serverless: schema.object({ + enabled: offeringBasedSchema({ + serverless: schema.literal(true), + options: { defaultValue: schema.contextRef('serverless') }, + }), + }), }); -type CloudSecurityPostureConfig = TypeOf; + +export type CloudSecurityPostureConfig = TypeOf; export const config: PluginConfigDescriptor = { schema: configSchema, diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/create_indices.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/create_indices.ts index c269e201f4243..06e13248fa1c5 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_indices/create_indices.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_indices/create_indices.ts @@ -4,10 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { errors } from '@elastic/elasticsearch'; import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; - +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { BENCHMARK_SCORE_INDEX_DEFAULT_NS, BENCHMARK_SCORE_INDEX_PATTERN, @@ -20,8 +19,21 @@ import { latestFindingsPipelineIngestConfig, scorePipelineIngestConfig } from '. import { latestIndexConfigs } from './latest_indices'; import { IndexConfig, IndexTemplateParams } from './types'; +import { CloudSecurityPostureConfig } from '../config'; + +interface IndexTemplateSettings { + index: { + default_pipeline: string; + }; + lifecycle?: { name: string }; +} + // TODO: Add integration tests -export const initializeCspIndices = async (esClient: ElasticsearchClient, logger: Logger) => { +export const initializeCspIndices = async ( + esClient: ElasticsearchClient, + cloudSecurityPostureConfig: CloudSecurityPostureConfig, + logger: Logger +) => { await Promise.allSettled([ createPipelineIfNotExists(esClient, scorePipelineIngestConfig, logger), createPipelineIfNotExists(esClient, latestFindingsPipelineIngestConfig, logger), @@ -32,9 +44,14 @@ export const initializeCspIndices = async (esClient: ElasticsearchClient, logger createVulnerabilitiesLatestIndexPromise, createBenchmarkScoreIndexPromise, ] = await Promise.allSettled([ - createLatestIndex(esClient, logger, latestIndexConfigs.findings), - createLatestIndex(esClient, logger, latestIndexConfigs.vulnerabilities), - createBenchmarkScoreIndex(esClient, logger), + createLatestIndex(esClient, latestIndexConfigs.findings, cloudSecurityPostureConfig, logger), + createLatestIndex( + esClient, + latestIndexConfigs.vulnerabilities, + cloudSecurityPostureConfig, + logger + ), + createBenchmarkScoreIndex(esClient, cloudSecurityPostureConfig, logger), ]); if (createFindingsLatestIndexPromise.status === 'rejected') { @@ -48,25 +65,31 @@ export const initializeCspIndices = async (esClient: ElasticsearchClient, logger } }; -const createBenchmarkScoreIndex = async (esClient: ElasticsearchClient, logger: Logger) => { +const createBenchmarkScoreIndex = async ( + esClient: ElasticsearchClient, + cloudSecurityPostureConfig: CloudSecurityPostureConfig, + logger: Logger +) => { try { // Deletes old assets from previous versions as part of upgrade process const INDEX_TEMPLATE_V830 = 'cloud_security_posture.scores'; await deleteIndexTemplateSafe(esClient, logger, INDEX_TEMPLATE_V830); + const settings: IndexTemplateSettings = { + index: { + default_pipeline: latestFindingsPipelineIngestConfig.id, + }, + lifecycle: { name: '' }, + }; + if (cloudSecurityPostureConfig.serverless.enabled) delete settings.lifecycle; + // We always want to keep the index template updated await esClient.indices.putIndexTemplate({ name: BENCHMARK_SCORE_INDEX_TEMPLATE_NAME, index_patterns: BENCHMARK_SCORE_INDEX_PATTERN, template: { mappings: benchmarkScoreMapping, - settings: { - default_pipeline: scorePipelineIngestConfig.id, - // TODO: once we will convert the score index to datastream we will no longer override the ilm to be empty - lifecycle: { - name: '', - }, - }, + settings, }, _meta: { package: { @@ -98,8 +121,9 @@ const createBenchmarkScoreIndex = async (esClient: ElasticsearchClient, logger: const createLatestIndex = async ( esClient: ElasticsearchClient, - logger: Logger, - indexConfig: IndexConfig + indexConfig: IndexConfig, + cloudSecurityPostureConfig: CloudSecurityPostureConfig, + logger: Logger ) => { const { indexName, indexPattern, indexTemplateName, indexDefaultName } = indexConfig; try { @@ -121,7 +145,7 @@ const createLatestIndex = async ( }; // We always want to keep the index template updated - await updateIndexTemplate(esClient, logger, indexTemplateParams); + await updateIndexTemplate(esClient, indexTemplateParams, cloudSecurityPostureConfig, logger); const result = await createIndexSafe(esClient, logger, indexDefaultName); @@ -192,10 +216,21 @@ const createIndexSafe = async (esClient: ElasticsearchClient, logger: Logger, in const updateIndexTemplate = async ( esClient: ElasticsearchClient, - logger: Logger, - indexTemplateParams: IndexTemplateParams + indexTemplateParams: IndexTemplateParams, + cloudSecurityPostureConfig: CloudSecurityPostureConfig, + logger: Logger ) => { const { indexTemplateName, indexPattern, template, composedOf, _meta } = indexTemplateParams; + + const settings: IndexTemplateSettings = { + ...template?.settings, // nothing inside + index: { + default_pipeline: latestFindingsPipelineIngestConfig.id, + }, + lifecycle: { name: '' }, + }; + if (cloudSecurityPostureConfig.serverless.enabled) delete settings.lifecycle; + try { await esClient.indices.putIndexTemplate({ name: indexTemplateName, @@ -203,18 +238,13 @@ const updateIndexTemplate = async ( priority: 500, template: { mappings: template?.mappings, - settings: { - ...template?.settings, - default_pipeline: latestFindingsPipelineIngestConfig.id, - lifecycle: { - name: '', - }, - }, + settings, aliases: template?.aliases, }, _meta, composed_of: composedOf, }); + logger.info(`Updated index template successfully [Name: ${indexTemplateName}]`); } catch (e) { logger.error(`Failed to update index template [Name: ${indexTemplateName}]`); diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.ts b/x-pack/plugins/cloud_security_posture/server/plugin.ts index 30ff887a354ca..8b77581efd39d 100755 --- a/x-pack/plugins/cloud_security_posture/server/plugin.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.ts @@ -49,6 +49,7 @@ import { setupFindingsStatsTask, } from './tasks/findings_stats_task'; import { registerCspmUsageCollector } from './lib/telemetry/collectors/register'; +import { CloudSecurityPostureConfig } from './config'; export class CspPlugin implements @@ -60,6 +61,7 @@ export class CspPlugin > { private readonly logger: Logger; + private readonly config: CloudSecurityPostureConfig; private isCloudEnabled?: boolean; /** @@ -69,8 +71,9 @@ export class CspPlugin */ #isInitialized: boolean = false; - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); + this.config = initializerContext.config.get(); } public setup( @@ -203,7 +206,7 @@ export class CspPlugin async initialize(core: CoreStart, taskManager: TaskManagerStartContract): Promise { this.logger.debug('initialize'); const esClient = core.elasticsearch.client.asInternalUser; - await initializeCspIndices(esClient, this.logger); + await initializeCspIndices(esClient, this.config, this.logger); await initializeCspTransforms(esClient, this.logger); await scheduleFindingsStatsTask(taskManager, this.logger); this.#isInitialized = true; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx index d4b5bf3f57e27..90a17f070e1f7 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx @@ -9,7 +9,7 @@ import React, { FC } from 'react'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FieldIcon } from '@kbn/react-field'; -import { getFieldTypeName } from '@kbn/unified-field-list/src/utils/field_types/get_field_type_name'; +import { getFieldTypeName } from '@kbn/discover-utils'; import './_index.scss'; interface FieldTypeIconProps { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx index 401a437eb0f67..b4a09b3cb1c9d 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx @@ -8,7 +8,7 @@ import React, { FC, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { getFieldTypeName } from '@kbn/unified-field-list/src/utils/field_types/get_field_type_name'; +import { getFieldTypeName } from '@kbn/discover-utils'; import { FieldTypesHelpPopover } from './field_types_help_popover'; import { MultiSelectPicker, Option } from '../multi_select_picker'; import type { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx index 517400024084a..46adbecdfc814 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx @@ -9,7 +9,7 @@ import React, { FC, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import { getFieldTypeName } from '@kbn/unified-field-list/src/utils/field_types/get_field_type_name'; +import { getFieldTypeName } from '@kbn/discover-utils'; import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; import { FieldTypesHelpPopover } from '../../../common/components/field_types_filter/field_types_help_popover'; import { FieldTypeIcon } from '../../../common/components/field_type_icon'; diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 75dd55b83e274..5221ef48b5efc 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -26,6 +26,7 @@ "@kbn/data-views-plugin", "@kbn/datemath", "@kbn/discover-plugin", + "@kbn/discover-utils", "@kbn/embeddable-plugin", "@kbn/embeddable-plugin", "@kbn/es-query", diff --git a/x-pack/plugins/discover_log_explorer/README.md b/x-pack/plugins/discover_log_explorer/README.md deleted file mode 100755 index 4b3453a451bf8..0000000000000 --- a/x-pack/plugins/discover_log_explorer/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Discover Log Explorer - -This plugin registers a `log-explorer` profile using the Discover customization framework, offering several affordances specifically designed for log consumption. - -The plugin enhances the capabilities of Discover in the following ways: - -- **Dataset selector**: this customization on the Discover page replaces the DataViews picker with a Logs dataset selector built ad-hoc to provide a better experience when navigating throught all the available datasets. - diff --git a/x-pack/plugins/discover_log_explorer/common/runtime_types.ts b/x-pack/plugins/discover_log_explorer/common/runtime_types.ts deleted file mode 100644 index d6d0336eafdd7..0000000000000 --- a/x-pack/plugins/discover_log_explorer/common/runtime_types.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { Context, Errors, IntersectionType, Type, UnionType, ValidationError } from 'io-ts'; - -type ErrorFactory = (message: string) => Error; - -const getErrorPath = ([first, ...rest]: Context): string[] => { - if (typeof first === 'undefined') { - return []; - } else if (first.type instanceof IntersectionType) { - const [, ...next] = rest; - return getErrorPath(next); - } else if (first.type instanceof UnionType) { - const [, ...next] = rest; - return [first.key, ...getErrorPath(next)]; - } - - return [first.key, ...getErrorPath(rest)]; -}; - -const getErrorType = ({ context }: ValidationError) => - context[context.length - 1]?.type?.name ?? 'unknown'; - -const formatError = (error: ValidationError) => - error.message ?? - `in ${getErrorPath(error.context).join('/')}: ${JSON.stringify( - error.value - )} does not match expected type ${getErrorType(error)}`; - -export const formatErrors = (errors: ValidationError[]) => - `Failed to validate: \n${errors.map((error) => ` ${formatError(error)}`).join('\n')}`; - -export const createPlainError = (message: string) => new Error(message); - -export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => { - throw createError(formatErrors(errors)); -}; - -export const decodeOrThrow = - ( - runtimeType: Type, - createError: ErrorFactory = createPlainError - ) => - (inputValue: InputValue) => - pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); diff --git a/x-pack/plugins/discover_log_explorer/kibana.jsonc b/x-pack/plugins/discover_log_explorer/kibana.jsonc deleted file mode 100644 index 89d00e3ce4eeb..0000000000000 --- a/x-pack/plugins/discover_log_explorer/kibana.jsonc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "plugin", - "id": "@kbn/discover-log-explorer-plugin", - "owner": "@elastic/infra-monitoring-ui", - "description": "This plugin exposes and registers Logs+ features.", - "plugin": { - "id": "discoverLogExplorer", - "server": true, - "browser": true, - "configPath": ["xpack", "discoverLogExplorer"], - "requiredPlugins": ["data", "dataViews", "discover", "fleet", "kibanaReact", "kibanaUtils", "controls", "embeddable"], - "optionalPlugins": [], - "requiredBundles": [] - } -} diff --git a/x-pack/plugins/discover_log_explorer/public/deep_links.ts b/x-pack/plugins/discover_log_explorer/public/deep_links.ts deleted file mode 100644 index 2740fbab56b16..0000000000000 --- a/x-pack/plugins/discover_log_explorer/public/deep_links.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 { AppDeepLink, AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { LOG_EXPLORER_PROFILE_ID } from '../common/constants'; - -export const getLogExplorerDeepLink = ({ isVisible }: { isVisible: boolean }): AppDeepLink => ({ - id: LOG_EXPLORER_PROFILE_ID, - title: i18n.translate('xpack.discoverLogExplorer.deepLink', { - defaultMessage: 'Logs Explorer', - }), - path: `#/p/${LOG_EXPLORER_PROFILE_ID}`, - category: DEFAULT_APP_CATEGORIES.observability, - euiIconType: 'logoObservability', - navLinkStatus: isVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.default, -}); diff --git a/x-pack/plugins/discover_log_explorer/public/plugin.ts b/x-pack/plugins/discover_log_explorer/public/plugin.ts deleted file mode 100644 index e39c831cdb432..0000000000000 --- a/x-pack/plugins/discover_log_explorer/public/plugin.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; -import { LOG_EXPLORER_PROFILE_ID } from '../common/constants'; -import { DiscoverLogExplorerConfig } from '../common/plugin_config'; -import { createLogExplorerProfileCustomizations } from './customizations/log_explorer_profile'; -import { getLogExplorerDeepLink } from './deep_links'; -import { - DiscoverLogExplorerPluginSetup, - DiscoverLogExplorerPluginStart, - DiscoverLogExplorerStartDeps, -} from './types'; - -export class DiscoverLogExplorerPlugin - implements Plugin -{ - private config: DiscoverLogExplorerConfig; - - constructor(context: PluginInitializerContext) { - this.config = context.config.get(); - } - - public setup() {} - - public start(core: CoreStart, plugins: DiscoverLogExplorerStartDeps) { - const { discover, data } = plugins; - - discover.registerCustomizationProfile(LOG_EXPLORER_PROFILE_ID, { - customize: createLogExplorerProfileCustomizations({ core, data }), - deepLinks: [getLogExplorerDeepLink({ isVisible: this.config.featureFlags.deepLinkVisible })], - }); - } -} diff --git a/x-pack/plugins/discover_log_explorer/public/types.ts b/x-pack/plugins/discover_log_explorer/public/types.ts deleted file mode 100644 index 4ec95ba94ec5a..0000000000000 --- a/x-pack/plugins/discover_log_explorer/public/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DiscoverStart } from '@kbn/discover-plugin/public'; - -export type DiscoverLogExplorerPluginSetup = void; -export type DiscoverLogExplorerPluginStart = void; - -export interface DiscoverLogExplorerStartDeps { - data: DataPublicPluginStart; - discover: DiscoverStart; -} diff --git a/x-pack/plugins/discover_log_explorer/server/config.ts b/x-pack/plugins/discover_log_explorer/server/config.ts deleted file mode 100644 index 0fece328c2964..0000000000000 --- a/x-pack/plugins/discover_log_explorer/server/config.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 { schema, offeringBasedSchema } from '@kbn/config-schema'; -import { PluginConfigDescriptor } from '@kbn/core/server'; - -import { DiscoverLogExplorerConfig } from '../common/plugin_config'; - -export const configSchema = schema.object({ - featureFlags: schema.object({ - deepLinkVisible: offeringBasedSchema({ - serverless: schema.boolean(), - options: { - defaultValue: false, - }, - }), - }), -}); - -export const config: PluginConfigDescriptor = { - schema: configSchema, - exposeToBrowser: { - featureFlags: { - deepLinkVisible: true, - }, - }, -}; diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 9a23e9794eb85..8c8ace6fd434f 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -79,7 +79,7 @@ export const ANALYTICS_PLUGIN = { }; export const ELASTICSEARCH_PLUGIN = { - ID: 'elasticsearch', + ID: 'enterpriseSearchElasticsearch', NAME: i18n.translate('xpack.enterpriseSearch.elasticsearch.productName', { defaultMessage: 'Elasticsearch', }), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/fetch_index_pipeline_parameters.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/fetch_index_pipeline_parameters.test.ts new file mode 100644 index 0000000000000..9290289a76b72 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/fetch_index_pipeline_parameters.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockHttpValues } from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { fetchIndexPipelineParams } from './fetch_index_pipeline_parameters'; + +describe('FetchIndexPipelineParametersApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('fetchIndexPipelineParams', () => { + it('calls correct api', async () => { + const response = { + 'pipeline-name': {}, + }; + const promise = Promise.resolve(response); + http.get.mockReturnValue(promise); + const result = fetchIndexPipelineParams({ indexName: 'index-name' }); + await nextTick(); + expect(http.get).toHaveBeenCalledWith( + '/internal/enterprise_search/indices/index-name/pipeline_parameters' + ); + await expect(result).resolves.toEqual(response); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/fetch_index_pipeline_parameters.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/fetch_index_pipeline_parameters.ts new file mode 100644 index 0000000000000..901a40e5c9e23 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/fetch_index_pipeline_parameters.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IngestPipelineParams } from '../../../../../common/types/connectors'; +import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export interface FetchIndexPipelineParametersArgs { + indexName: string; +} +export type FetchIndexPipelineParametersResponse = IngestPipelineParams; + +export const fetchIndexPipelineParams = async ({ indexName }: FetchIndexPipelineParametersArgs) => { + const route = `/internal/enterprise_search/indices/${indexName}/pipeline_parameters`; + + return await HttpLogic.values.http.get(route); +}; + +export const FetchIndexPipelineParametersApiLogic = createApiLogic( + ['fetch_index_pipeline_params_api_logic'], + fetchIndexPipelineParams, + { + showErrorFlash: false, + } +); + +export type FetchIndexPipelineParametersApiLogicActions = Actions< + FetchIndexPipelineParametersArgs, + FetchIndexPipelineParametersResponse +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx index 83371d415ad84..ce8cb3c699478 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx @@ -228,7 +228,7 @@ export const SelectConnector: React.FC = () => { {filteredConnectors.map((connector) => ( { const { http } = useValues(HttpLogic); - const { apiKey, isGenerateModalOpen } = useValues(OverviewLogic); - const { openGenerateModal, closeGenerateModal } = useActions(OverviewLogic); + const { apiKey, isGenerateModalOpen, indexPipelineParameters } = useValues(OverviewLogic); + const { fetchIndexPipelineParameters, openGenerateModal, closeGenerateModal } = + useActions(OverviewLogic); const { indexName } = useValues(IndexViewLogic); const { services } = useKibana(); - const { isCloud } = useValues(KibanaLogic); const cloudContext = useCloudDetails(); - const codeArgs = { + useEffect(() => { + fetchIndexPipelineParameters({ indexName }); + }, [indexName]); + + const codeArgs: LanguageDefinitionSnippetArguments = { apiKey, + cloudId: cloudContext.cloudId, + extraIngestDocumentValues: { + _extract_binary_content: indexPipelineParameters.extract_binary_content, + _reduce_whitespace: indexPipelineParameters.reduce_whitespace, + _run_ml_inference: indexPipelineParameters.run_ml_inference, + }, + indexName, + ingestPipeline: indexPipelineParameters.name, url: cloudContext.elasticsearchUrl || DEFAULT_URL, }; const assetBasePath = http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/client_libraries/`); - const [selectedLanguage, setSelectedLanguage] = - useState(javascriptDefinition); + const [selectedLanguage, setSelectedLanguage] = useState(curlDefinition); return ( <> {isGenerateModalOpen && ( @@ -207,39 +220,23 @@ export const APIGettingStarted = () => { />
- {isCloud - ? i18n.translate( - 'xpack.enterpriseSearch.content.overview.gettingStarted.cloudId.cloudTitle', - { - defaultMessage: 'Store your unique Cloud ID', - } - ) - : i18n.translate( - 'xpack.enterpriseSearch.content.overview.gettingStarted.cloudId.elasticTitle', - { - defaultMessage: 'Store your elasticsearch URL', - } - )} + {i18n.translate( + 'xpack.enterpriseSearch.content.overview.gettingStarted.cloudId.elasticTitle', + { + defaultMessage: 'Store your Elasticsearch URL', + } + )}
@@ -261,28 +258,24 @@ export const APIGettingStarted = () => { overflow-wrap: anywhere; `} > - {codeArgs.url} + {codeArgs.cloudId + ? dedent`{ + CloudID: "${codeArgs.cloudId}", + Url: "${codeArgs.url}", + }` + : codeArgs.url}
} links={[]} - title={ - isCloud - ? i18n.translate( - 'xpack.enterpriseSearch.overview.gettingStarted.cloudId.panelTitleCloud', - { - defaultMessage: 'Copy your Cloud ID', - } - ) - : i18n.translate( - 'xpack.enterpriseSearch.overview.gettingStarted.cloudId.panelTitleElastic', - { - defaultMessage: 'Copy your elasticsearch URL', - } - ) - } + title={i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStarted.cloudId.panelTitleElastic', + { + defaultMessage: 'Copy your Elasticsearch URL', + } + )} overviewPanelProps={{ color: 'plain', hasShadow: false }} /> @@ -290,7 +283,7 @@ export const APIGettingStarted = () => { description={i18n.translate( 'xpack.enterpriseSearch.overview.gettingStarted.configureClient.description', { - defaultMessage: 'Initialize your client with your unique API key and Cloud ID', + defaultMessage: 'Initialize your client with your unique API key', } )} rightPanelContent={ @@ -363,7 +356,11 @@ export const APIGettingStarted = () => { { 'buildSearchQuery', codeArgs )} - consoleRequest={getConsoleRequest('buildSearchQuery')} + consoleRequest={getLanguageDefinitionCodeSnippet( + consoleDefinition, + 'buildSearchQuery', + codeArgs + )} selectedLanguage={selectedLanguage} setSelectedLanguage={setSelectedLanguage} assetBasePath={assetBasePath} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/console.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/console.ts new file mode 100644 index 0000000000000..9d395b0a68c67 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/console.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LanguageDefinition } from '@kbn/search-api-panels'; + +import { ingestKeysToJSON } from './helpers'; + +export const consoleDefinition: Partial = { + buildSearchQuery: ({ indexName }) => `POST /${indexName ?? 'books'}/_search?pretty + { + "query": { + "query_string": { + "query": "snow" + } + } + }`, + ingestData: ({ indexName, ingestPipeline, extraIngestDocumentValues }) => { + const ingestDocumentKeys = ingestPipeline ? ingestKeysToJSON(extraIngestDocumentValues) : ''; + return `POST _bulk?pretty${ingestPipeline ? `&pipeline=${ingestPipeline}` : ''} + { "index" : { "_index" : "${indexName}" } } + {"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470${ingestDocumentKeys}} + { "index" : { "_index" : "${indexName}" } } + {"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585${ingestDocumentKeys}} + { "index" : { "_index" : "${indexName}" } } + {"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328${ingestDocumentKeys}} + { "index" : { "_index" : "${indexName}" } } + {"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227${ingestDocumentKeys}} + { "index" : { "_index" : "${indexName}" } } + {"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268${ingestDocumentKeys}} + { "index" : { "_index" : "${indexName}" } } + {"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311${ingestDocumentKeys}}`; + }, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/curl.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/curl.ts index 4131c42b95fd9..8bac32d754064 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/curl.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/curl.ts @@ -10,8 +10,10 @@ import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../../../shared/doc_links'; +import { ingestKeysToJSON } from './helpers'; + export const curlDefinition: LanguageDefinition = { - buildSearchQuery: `curl -X POST "\$\{ES_URL\}/books/_search?pretty" \\ + buildSearchQuery: ({ indexName }) => `curl -X POST "\$\{ES_URL\}/${indexName}/_search?pretty" \\ -H "Authorization: ApiKey "\$\{API_KEY\}"" \\ -H "Content-Type: application/json" \\ -d' @@ -33,23 +35,28 @@ export API_KEY="${apiKey}"`, }, iconType: 'curl.svg', id: Languages.CURL, - ingestData: `curl -X POST "\$\{ES_URL\}/_bulk?pretty" \\ + ingestData: ({ indexName, ingestPipeline, extraIngestDocumentValues }) => { + const ingestDocumentKeys = ingestPipeline ? ingestKeysToJSON(extraIngestDocumentValues) : ''; + return `curl -X POST "\$\{ES_URL\}/_bulk?pretty${ + ingestPipeline ? `&pipeline=${ingestPipeline}` : '' + }" \\ -H "Authorization: ApiKey "\$\{API_KEY\}"" \\ -H "Content-Type: application/json" \\ -d' -{ "index" : { "_index" : "books" } } -{"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470} -{ "index" : { "_index" : "books" } } -{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585} -{ "index" : { "_index" : "books" } } -{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328} -{ "index" : { "_index" : "books" } } -{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227} -{ "index" : { "_index" : "books" } } -{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268} -{ "index" : { "_index" : "books" } } -{"name": "The Handmaid'"'"'s Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311} -'`, +{ "index" : { "_index" : "${indexName}" } } +{"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470${ingestDocumentKeys}} +{ "index" : { "_index" : "${indexName}" } } +{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585${ingestDocumentKeys}} +{ "index" : { "_index" : "${indexName}" } } +{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328${ingestDocumentKeys}} +{ "index" : { "_index" : "${indexName}" } } +{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227${ingestDocumentKeys}} +{ "index" : { "_index" : "${indexName}" } } +{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268${ingestDocumentKeys}} +{ "index" : { "_index" : "${indexName}" } } +{"name": "The Handmaid'"'"'s Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311${ingestDocumentKeys}} +'`; + }, ingestDataIndex: '', installClient: `# if cURL is not already installed on your system # then install it with the package manager of your choice @@ -60,7 +67,7 @@ brew install curl`, defaultMessage: 'cURL', }), languageStyling: 'shell', - testConnection: `curl "\$\{ES_URL\}" \\ + testConnection: ({ indexName }) => `curl "\$\{ES_URL\}/${indexName}" \\ -H "Authorization: ApiKey "\$\{API_KEY\}"" \\ -H "Content-Type: application/json"`, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/go.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/go.ts index 0bb2b99a0682f..6c504bba26bdc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/go.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/go.ts @@ -10,32 +10,45 @@ import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../../../shared/doc_links'; +import { ingestKeysToJSON } from './helpers'; + export const goDefinition: LanguageDefinition = { - buildSearchQuery: `searchResp, err := es.Search(). - Index("books"). - Q("snow"). - Do(context.Background()) + buildSearchQuery: ({ indexName }) => `searchResp, err := es.Search( + es.Search.WithContext(context.Background()), + es.Search.WithIndex("${indexName}"), + es.Search.WithQuery("snow"), + es.Search.WithTrackTotalHits(true), + es.Search.WithPretty(), +) fmt.Println(searchResp, err)`, - configureClient: ({ url, apiKey }) => `import ( + configureClient: ({ url, apiKey, cloudId }) => `import ( + "bytes" "context" "fmt" "log" - "strings" -​ - elasticsearch "github.com/elastic/go-elasticsearch/v8" + + "github.com/elastic/go-elasticsearch/v8" ) -func main() { - cfg := elasticsearch.Config{ - Address: "${url}", - APIKey: "${apiKey}", - } - es, err := elasticsearch.NewClient(cfg) - if err != nil { - log.Fatalf("Error creating the client: %s", err) +// ... + +cfg := elasticsearch.Config{ + ${ + cloudId + ? `CloudID:"${cloudId}",` + : `Addresses: []string{ + "${url}", + },` } -}`, + APIKey: "${apiKey}", +} + +es, err := elasticsearch.NewClient(cfg) +if err != nil { + log.Fatalf("Error creating the client: %s", err) +} +`, docLink: docLinks.clientsGoIndex, github: { label: i18n.translate('xpack.enterpriseSearch.languages.go.githubLink', { @@ -45,33 +58,42 @@ func main() { }, iconType: 'go.svg', id: Languages.GO, - ingestData: `ingestResult, err := es.Bulk(). - Index("books"). - Raw(strings.NewReader(\` + ingestData: ({ indexName, ingestPipeline, extraIngestDocumentValues }) => { + const ingestDocumentKeys = ingestPipeline ? ingestKeysToJSON(extraIngestDocumentValues) : ''; + return `buf := bytes.NewBufferString(\` {"index":{"_id":"9780553351927"}} -{"name":"Snow Crash","author":"Neal Stephenson","release_date":"1992-06-01","page_count": 470} +{"name":"Snow Crash","author":"Neal Stephenson","release_date":"1992-06-01","page_count": 470${ingestDocumentKeys}} { "index": { "_id": "9780441017225"}} -{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585} +{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585${ingestDocumentKeys}} { "index": { "_id": "9780451524935"}} -{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328} +{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328${ingestDocumentKeys}} { "index": { "_id": "9781451673319"}} -{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227} +{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227${ingestDocumentKeys}} { "index": { "_id": "9780060850524"}} -{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268} +{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268${ingestDocumentKeys}} { "index": { "_id": "9780385490818"}} -{"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311}\n\`)). - Do(context.Background()) +{"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311${ingestDocumentKeys}} +\`) + +ingestResult, err := es.Bulk( + bytes.NewReader(buf.Bytes()), + es.Bulk.WithIndex("${indexName}"),${ + ingestPipeline ? `\n es.Bulk.WithPipeline("${ingestPipeline}"),` : '' + } +) -fmt.Println(ingestResult, err)`, +fmt.Println(ingestResult, err)`; + }, ingestDataIndex: '', installClient: 'go get github.com/elastic/go-elasticsearch/v8@latest', name: i18n.translate('xpack.enterpriseSearch.languages.go', { defaultMessage: 'Go', }), - testConnection: `infores, err := es.Info().Do(context.Background()) - if err != nil { - log.Fatalf("Error getting response: %s", err) - } + testConnection: `// API Key should have cluster monitoring rights +infores, err := es.Info() +if err != nil { + log.Fatalf("Error getting response: %s", err) +} - fmt.Println(infores)`, +fmt.Println(infores)`, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/helpers.test.ts new file mode 100644 index 0000000000000..df13ea273061e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/helpers.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ingestKeysToJSON, ingestKeysToPHP, ingestKeysToRuby } from './helpers'; + +describe('getting started language helpers', () => { + describe('ingestKeysToJSON', () => { + it('return empty string when given undefined', () => { + expect(ingestKeysToJSON(undefined)).toEqual(''); + }); + it('return json keys with quotes when given expected data', () => { + expect(ingestKeysToJSON({ _foo: true, _bar: false })).toEqual( + ', "_foo": true, "_bar": false' + ); + }); + }); + describe('ingestKeysToPHP', () => { + it('return empty string when given undefined', () => { + expect(ingestKeysToPHP(undefined)).toEqual(''); + }); + it('return json keys with quotes when given expected data', () => { + expect(ingestKeysToPHP({ _foo: true, _bar: false })).toEqual( + `\n '_foo' => true,\n '_bar' => false,` + ); + }); + }); + describe('ingestKeysToRuby', () => { + it('return empty string when given undefined', () => { + expect(ingestKeysToRuby(undefined)).toEqual(''); + }); + it('return json keys with quotes when given expected data', () => { + expect(ingestKeysToRuby({ _foo: true, _bar: false })).toEqual(', _foo: true, _bar: false'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/helpers.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/helpers.ts new file mode 100644 index 0000000000000..5d98779a851b3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/helpers.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 { LanguageDefinitionSnippetArguments } from '@kbn/search-api-panels'; + +export const ingestKeysToJSON = ( + extraIngestDocumentValues: LanguageDefinitionSnippetArguments['extraIngestDocumentValues'] +) => + extraIngestDocumentValues + ? Object.entries(extraIngestDocumentValues).reduce((result, value) => { + result += `, "${value[0]}": ${value[1]}`; + return result; + }, '') + : ''; + +export const ingestKeysToPHP = ( + extraIngestDocumentValues: LanguageDefinitionSnippetArguments['extraIngestDocumentValues'] +) => + extraIngestDocumentValues + ? Object.entries(extraIngestDocumentValues).reduce((result, value) => { + result += `\n '${value[0]}' => ${value[1]},`; + return result; + }, '') + : ''; + +export const ingestKeysToRuby = ( + extraIngestDocumentValues: LanguageDefinitionSnippetArguments['extraIngestDocumentValues'] +) => + extraIngestDocumentValues + ? Object.entries(extraIngestDocumentValues).reduce((result, value) => { + result += `, ${value[0]}: ${value[1]}`; + return result; + }, '') + : ''; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/javascript.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/javascript.ts index 3b636490a495e..51d7e4ef68625 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/javascript.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/javascript.ts @@ -10,11 +10,13 @@ import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../../../shared/doc_links'; +import { ingestKeysToJSON } from './helpers'; + export const javascriptDefinition: LanguageDefinition = { - buildSearchQuery: `// Let's search! + buildSearchQuery: ({ indexName }) => `// Let's search! const searchResult = await client.search({ - index: 'my-index-name', - q: '9HY9SWR' + index: '${indexName}', + q: 'snow' }); console.log(searchResult.hits.hits) @@ -35,40 +37,45 @@ const client = new Client({ }, iconType: 'javascript.svg', id: Languages.JAVASCRIPT, - ingestData: `// Sample flight data + ingestData: ({ indexName, ingestPipeline, extraIngestDocumentValues }) => { + const ingestDocumentKeys = ingestPipeline ? ingestKeysToJSON(extraIngestDocumentValues) : ''; + return `// Sample data books const dataset = [ - {'flight': '9HY9SWR', 'price': 841.2656419677076, 'delayed': false}, - {'flight': 'X98CCZO', 'price': 882.9826615595518, 'delayed': false}, - {'flight': 'UFK2WIZ', 'price': 190.6369038508356, 'delayed': true}, + {"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470${ingestDocumentKeys}}, + {"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585${ingestDocumentKeys}}, + {"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328${ingestDocumentKeys}}, + {"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227${ingestDocumentKeys}}, + {"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268${ingestDocumentKeys}}, + {"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311${ingestDocumentKeys}}, ]; // Index with the bulk helper const result = await client.helpers.bulk({ - datasource: dataset, - onDocument (doc) { - return { index: { _index: 'my-index-name' }}; - } + datasource: dataset,${ingestPipeline ? `\n pipeline: "${ingestPipeline}",` : ''} + onDocument: (doc) => ({ index: { _index: '${indexName}' }}), }); console.log(result); /** { - total: 3, + total: 6, failed: 0, retry: 0, - successful: 3, + successful: 6, noop: 0, - time: 421, - bytes: 293, + time: 82, + bytes: 1273, aborted: false } -*/`, +*/`; + }, ingestDataIndex: '', installClient: 'npm install @elastic/elasticsearch@8', name: i18n.translate('xpack.enterpriseSearch.languages.javascript', { defaultMessage: 'JavaScript', }), - testConnection: `const resp = await client.info(); + testConnection: `// API Key should have cluster monitor rights. +const resp = await client.info(); console.log(resp); /** @@ -79,10 +86,10 @@ console.log(resp); version: { build_flavor: 'default', build_type: 'docker', - build_hash: 'c94b4700cda13820dad5aa74fae6db185ca5c304', - build_date: '2022-10-24T16:54:16.433628434Z', - build_snapshot: false, - lucene_version: '9.4.1', + build_hash: 'ca3dc3a882d76f14d2765906ce3b1cf421948d19', + build_date: '2023-08-28T11:24:16.383660553Z', + build_snapshot: true, + lucene_version: '9.7.0', minimum_wire_compatibility_version: '7.17.0', minimum_index_compatibility_version: '7.0.0' }, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/php.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/php.ts index eec2e50e275dd..3146ca60af306 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/php.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/php.ts @@ -10,9 +10,11 @@ import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../../../shared/doc_links'; +import { ingestKeysToPHP } from './helpers'; + export const phpDefinition: LanguageDefinition = { - buildSearchQuery: `$params = [ - 'index' => 'books', + buildSearchQuery: ({ indexName }) => `$params = [ + 'index' => '${indexName}', 'body' => [ 'q' => 'snow' ] @@ -33,11 +35,13 @@ print_r($response->asArray());`, }, iconType: 'php.svg', id: Languages.PHP, - ingestData: `$params = [ + ingestData: ({ indexName, ingestPipeline, extraIngestDocumentValues }) => { + const ingestDocumentKeys = ingestPipeline ? ingestKeysToPHP(extraIngestDocumentValues) : ''; + return `$params = [${ingestPipeline ? `\n 'pipeline' => '${ingestPipeline}',` : ''} 'body' => [ [ 'index' => [ - '_index' => 'books', + '_index' => '${indexName}', '_id' => '9780553351927', ], ], @@ -45,11 +49,11 @@ print_r($response->asArray());`, 'name' => 'Snow Crash', 'author' => 'Neal Stephenson', 'release_date' => '1992-06-01', - 'page_count' => 470, + 'page_count' => 470,${ingestDocumentKeys} ], [ 'index' => [ - '_index' => 'books', + '_index' => '${indexName}', '_id' => '9780441017225', ], ], @@ -57,11 +61,11 @@ print_r($response->asArray());`, 'name' => 'Revelation Space', 'author' => 'Alastair Reynolds', 'release_date' => '2000-03-15', - 'page_count' => 585, + 'page_count' => 585,${ingestDocumentKeys} ], [ 'index' => [ - '_index' => 'books', + '_index' => '${indexName}', '_id' => '9780451524935', ], ], @@ -69,11 +73,11 @@ print_r($response->asArray());`, 'name' => '1984', 'author' => 'George Orwell', 'release_date' => '1985-06-01', - 'page_count' => 328, + 'page_count' => 328,${ingestDocumentKeys} ], [ 'index' => [ - '_index' => 'books', + '_index' => '${indexName}', '_id' => '9781451673319', ], ], @@ -81,11 +85,11 @@ print_r($response->asArray());`, 'name' => 'Fahrenheit 451', 'author' => 'Ray Bradbury', 'release_date' => '1953-10-15', - 'page_count' => 227, + 'page_count' => 227,${ingestDocumentKeys} ], [ 'index' => [ - '_index' => 'books', + '_index' => '${indexName}', '_id' => '9780060850524', ], ], @@ -93,32 +97,34 @@ print_r($response->asArray());`, 'name' => 'Brave New World', 'author' => 'Aldous Huxley', 'release_date' => '1932-06-01', - 'page_count' => 268, + 'page_count' => 268,${ingestDocumentKeys} ], [ 'index' => [ - '_index' => 'books', + '_index' => '${indexName}', '_id' => '9780385490818', ], ], [ - 'name' => 'The Handmaid\'s Tale', + 'name' => 'The Handmaid\\'s Tale', 'author' => 'Margaret Atwood', 'release_date' => '1985-06-01', - 'page_count' => 311, + 'page_count' => 311,${ingestDocumentKeys} ], ], ]; $response = $client->bulk($params); echo $response->getStatusCode(); - echo (string) $response->getBody();`, + echo (string) $response->getBody();`; + }, ingestDataIndex: '', installClient: 'composer require elasticsearch/elasticsearch', name: i18n.translate('xpack.enterpriseSearch.languages.php', { defaultMessage: 'PHP', }), - testConnection: `$response = $client->info(); + testConnection: `// API Key should have cluster monitor rights. +$response = $client->info(); echo $response->getStatusCode(); echo (string) $response->getBody();`, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/python.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/python.ts index f3fd81b2c1152..24723ba3632da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/python.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/python.ts @@ -10,8 +10,10 @@ import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../../../shared/doc_links'; +import { ingestKeysToJSON } from './helpers'; + export const pythonDefinition: LanguageDefinition = { - buildSearchQuery: `client.search(index="books", q="snow")`, + buildSearchQuery: ({ indexName }) => `client.search(index="${indexName}", q="snow")`, configureClient: ({ url, apiKey }) => `from elasticsearch import Elasticsearch client = Elasticsearch( @@ -27,22 +29,25 @@ client = Elasticsearch( }, iconType: 'python.svg', id: Languages.PYTHON, - ingestData: `documents = [ - { "index": { "_index": "books", "_id": "9780553351927"}}, - {"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470}, - { "index": { "_index": "books", "_id": "9780441017225"}}, - {"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585}, - { "index": { "_index": "books", "_id": "9780451524935"}}, - {"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328}, - { "index": { "_index": "books", "_id": "9781451673319"}}, - {"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227}, - { "index": { "_index": "books", "_id": "9780060850524"}}, - {"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268}, - { "index": { "_index": "books", "_id": "9780385490818"}}, - {"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311}, + ingestData: ({ indexName, ingestPipeline, extraIngestDocumentValues }) => { + const ingestDocumentKeys = ingestPipeline ? ingestKeysToJSON(extraIngestDocumentValues) : ''; + return `documents = [ + { "index": { "_index": "${indexName}", "_id": "9780553351927"}}, + {"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470${ingestDocumentKeys}}, + { "index": { "_index": "${indexName}", "_id": "9780441017225"}}, + {"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585${ingestDocumentKeys}}, + { "index": { "_index": "${indexName}", "_id": "9780451524935"}}, + {"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328${ingestDocumentKeys}}, + { "index": { "_index": "${indexName}", "_id": "9781451673319"}}, + {"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227${ingestDocumentKeys}}, + { "index": { "_index": "${indexName}", "_id": "9780060850524"}}, + {"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268${ingestDocumentKeys}}, + { "index": { "_index": "${indexName}", "_id": "9780385490818"}}, + {"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311${ingestDocumentKeys}}, ] -client.bulk(operations=documents)`, +client.bulk(operations=documents${ingestPipeline ? `, pipeline="${ingestPipeline}"` : ''})`; + }, ingestDataIndex: '', installClient: `python -m pip install elasticsearch @@ -52,5 +57,6 @@ client.bulk(operations=documents)`, name: i18n.translate('xpack.enterpriseSearch.languages.python', { defaultMessage: 'Python', }), - testConnection: `client.info()`, + testConnection: `# API key should have cluster monitor rights +client.info()`, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/ruby.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/ruby.ts index 5f67424900d6b..43a104b0f7a8b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/ruby.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/ruby.ts @@ -10,11 +10,13 @@ import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../../../shared/doc_links'; +import { ingestKeysToRuby } from './helpers'; + export const rubyDefinition: LanguageDefinition = { - buildSearchQuery: `client.search(index: 'books', q: 'snow')`, - configureClient: ({ url, apiKey }) => `client = ElasticsearchServerless::Client.new( + buildSearchQuery: ({ indexName }) => `client.search(index: '${indexName}', q: 'snow')`, + configureClient: ({ url, apiKey, cloudId }) => `client = Elasticsearch::Client.new( api_key: '${apiKey}', - url: '${url}' + ${cloudId ? `cloud_id: ${cloudId},` : `url: '${url}',`} ) `, docLink: docLinks.clientsRubyOverview, @@ -26,19 +28,23 @@ export const rubyDefinition: LanguageDefinition = { }, iconType: 'ruby.svg', id: Languages.RUBY, - ingestData: `documents = [ - { index: { _index: 'books', data: {name: "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470} } }, - { index: { _index: 'books', data: {name: "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585} } }, - { index: { _index: 'books', data: {name: "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328} } }, - { index: { _index: 'books', data: {name: "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227} } }, - { index: { _index: 'books', data: {name: "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268} } }, - { index: { _index: 'books', data: {name: "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311} } } + ingestData: ({ indexName, ingestPipeline, extraIngestDocumentValues }) => { + const ingestDocumentKeys = ingestPipeline ? ingestKeysToRuby(extraIngestDocumentValues) : ''; + return `documents = [ + { index: { _index: '${indexName}', data: {name: "Snow Crash", author: "Neal Stephenson", release_date: "1992-06-01", page_count: 470${ingestDocumentKeys}} } }, + { index: { _index: '${indexName}', data: {name: "Revelation Space", author: "Alastair Reynolds", release_date: "2000-03-15", page_count: 585${ingestDocumentKeys}} } }, + { index: { _index: '${indexName}', data: {name: "1984", author: "George Orwell", release_date: "1985-06-01", page_count: 328${ingestDocumentKeys}} } }, + { index: { _index: '${indexName}', data: {name: "Fahrenheit 451", author: "Ray Bradbury", release_date: "1953-10-15", page_count: 227${ingestDocumentKeys}} } }, + { index: { _index: '${indexName}', data: {name: "Brave New World", author: "Aldous Huxley", release_date: "1932-06-01", page_count: 268${ingestDocumentKeys}} } }, + { index: { _index: '${indexName}', data: {name: "The Handmaid's Tale", author: "Margaret Atwood", release_date: "1985-06-01", page_count: 311${ingestDocumentKeys}} } } ] -client.bulk(body: documents)`, +client.bulk(body: documents${ingestPipeline ? `, pipeline: "${ingestPipeline}"` : ''})`; + }, ingestDataIndex: '', - installClient: `$ gem install elasticsearch -v x.x.x`, + installClient: `$ gem install elasticsearch`, name: i18n.translate('xpack.enterpriseSearch.languages.ruby', { defaultMessage: 'Ruby', }), - testConnection: `client.info`, + testConnection: `# API Key should have cluster monitoring rights. +client.info`, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts index 94630fc054b26..64b4dad084d96 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts @@ -197,6 +197,6 @@ export const getConnectorTemplate = ({ } elasticsearch: - host: "${host || 'https://locahost:9200'}" + host: "${host || 'http://localhost:9200'}" api_key: "${apiKeyData?.encoded || ''}" `; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/overview.logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/overview.logic.ts index 34584d394b93a..dbe0610b4a855 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/overview.logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/overview.logic.ts @@ -7,7 +7,9 @@ import { kea, MakeLogicType } from 'kea'; +import { DEFAULT_PIPELINE_VALUES } from '../../../../../common/constants'; import { Status } from '../../../../../common/types/api'; +import { IngestPipelineParams } from '../../../../../common/types/connectors'; import { KibanaLogic } from '../../../shared/kibana'; import { GenerateApiKeyLogic } from '../../api/generate_api_key/generate_api_key_logic'; @@ -15,6 +17,10 @@ import { CachedFetchIndexApiLogic, CachedFetchIndexApiLogicActions, } from '../../api/index/cached_fetch_index_api_logic'; +import { + FetchIndexPipelineParametersApiLogic, + FetchIndexPipelineParametersApiLogicActions, +} from '../../api/pipelines/fetch_index_pipeline_parameters'; import { SEARCH_INDICES_PATH } from '../../routes'; @@ -22,6 +28,7 @@ interface OverviewLogicActions { apiError: CachedFetchIndexApiLogicActions['apiError']; apiReset: typeof GenerateApiKeyLogic.actions.apiReset; closeGenerateModal: void; + fetchIndexPipelineParameters: FetchIndexPipelineParametersApiLogicActions['makeRequest']; openGenerateModal: void; toggleClientsPopover: void; toggleManageApiKeyPopover: void; @@ -32,6 +39,8 @@ interface OverviewLogicValues { apiKeyData: typeof GenerateApiKeyLogic.values.data; apiKeyStatus: typeof GenerateApiKeyLogic.values.status; indexData: typeof CachedFetchIndexApiLogic.values.indexData; + indexPipelineData: typeof FetchIndexPipelineParametersApiLogic.values.data; + indexPipelineParameters: IngestPipelineParams; isClientsPopoverOpen: boolean; isError: boolean; isGenerateModalOpen: boolean; @@ -48,12 +57,21 @@ export const OverviewLogic = kea ({ @@ -95,6 +113,11 @@ export const OverviewLogic = kea apiKeyStatus === Status.SUCCESS ? apiKeyData.apiKey.encoded : '', ], + indexPipelineParameters: [ + () => [selectors.indexPipelineData], + (indexPipelineData: typeof FetchIndexPipelineParametersApiLogic.values.data) => + indexPipelineData ?? DEFAULT_PIPELINE_VALUES, + ], isError: [() => [selectors.status], (status) => status === Status.ERROR], isLoading: [ () => [selectors.status, selectors.indexData], diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/elasticsearch_product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/elasticsearch_product_card.tsx index 3c0609bcf5788..6febf9d072f48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/elasticsearch_product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/elasticsearch_product_card.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ELASTICSEARCH_PLUGIN } from '../../../../../common/constants'; +import { docLinks } from '../../../shared/doc_links'; import { ProductCard } from '../product_card'; import { BehavioralAnalyticsProductCard } from './behavioral_analytics_product_card'; @@ -26,6 +27,11 @@ export const ElasticsearchProductCard = () => { icon="logoElasticsearch" name={ELASTICSEARCH_PLUGIN.NAME} productId={ELASTICSEARCH_PLUGIN.ID} + emptyCta + cta={i18n.translate('xpack.enterpriseSearch.elasticsearchCard.cta', { + defaultMessage: 'Learn more', + })} + url={docLinks.elasticsearchGettingStarted} rightPanelItems={[ , , diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/enterprise_search_product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/enterprise_search_product_card.tsx index b53203aade931..e332d4fad9401 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/enterprise_search_product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/enterprise_search_product_card.tsx @@ -13,6 +13,7 @@ import { ENTERPRISE_SEARCH_PRODUCT_NAME, ENTERPRISE_SEARCH_CONTENT_PLUGIN, } from '../../../../../common/constants'; +import { docLinks } from '../../../shared/doc_links'; import { ProductCard } from '../product_card'; import { AppSearchProductCard } from './app_search_product_card'; @@ -25,6 +26,10 @@ export const EnterpriseSearchProductCard = () => ( 'Standalone applications tailored to simpler, user-friendly and business-focused search experiences.', })} emptyCta + cta={i18n.translate('xpack.enterpriseSearch.enterpriseSearchCard.cta', { + defaultMessage: 'Learn more', + })} + url={docLinks.start} icon="logoEnterpriseSearch" name={ENTERPRISE_SEARCH_PRODUCT_NAME} productId={ENTERPRISE_SEARCH_CONTENT_PLUGIN.ID} diff --git a/x-pack/plugins/enterprise_search/server/lib/pipelines/get_index_pipeline.test.ts b/x-pack/plugins/enterprise_search/server/lib/pipelines/get_index_pipeline.test.ts new file mode 100644 index 0000000000000..58476ed18b741 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/pipelines/get_index_pipeline.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { IScopedClusterClient } from '@kbn/core/server'; + +import { DEFAULT_PIPELINE_VALUES } from '../../../common/constants'; + +import { getIndexPipelineParameters } from './get_index_pipeline'; + +describe('getIndexPipelineParameters', () => { + const defaultMockClient = () => ({ + asCurrentUser: { + get: jest.fn().mockResolvedValue({}), + indices: { + getMapping: jest.fn().mockResolvedValue({}), + }, + ingest: { + getPipeline: jest.fn().mockRejectedValue('Pipeline not found'), + }, + search: jest.fn().mockResolvedValue({ + hits: { + hits: [], + }, + }), + }, + }); + let mockClient = defaultMockClient(); + let client: IScopedClusterClient; + beforeEach(() => { + jest.resetAllMocks(); + + mockClient = defaultMockClient(); + client = mockClient as unknown as IScopedClusterClient; + }); + it('returns default pipeline if custom not found', async () => { + await expect(getIndexPipelineParameters('my-index', client)).resolves.toEqual( + DEFAULT_PIPELINE_VALUES + ); + }); + it('returns connector pipeline params if found', async () => { + mockClient.asCurrentUser.search = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { + _id: 'unit-test', + _source: {}, + }, + ], + }, + }); + mockClient.asCurrentUser.get = jest.fn().mockResolvedValue({ + _id: 'unit-test', + _source: { + pipeline: { + extract_binary_content: false, + name: 'unit-test-pipeline', + reduce_whitespace: true, + run_ml_inference: true, + }, + }, + }); + await expect(getIndexPipelineParameters('my-index', client)).resolves.toEqual({ + extract_binary_content: false, + name: 'unit-test-pipeline', + reduce_whitespace: true, + run_ml_inference: true, + }); + }); + it('returns default pipeline if fetch custom throws', async () => { + mockClient.asCurrentUser.ingest.getPipeline = jest.fn().mockRejectedValue('Boom'); + + await expect(getIndexPipelineParameters('my-index', client)).resolves.toEqual( + DEFAULT_PIPELINE_VALUES + ); + }); + it('returns custom pipeline if found', async () => { + mockClient.asCurrentUser.ingest.getPipeline = jest.fn().mockResolvedValueOnce({ + 'my-index': { + fake: 'ingest-pipeline', + }, + }); + + await expect(getIndexPipelineParameters('my-index', client)).resolves.toEqual({ + extract_binary_content: true, + name: 'my-index', + reduce_whitespace: true, + run_ml_inference: false, + }); + }); + it('returns default connector index pipeline if found in mapping', async () => { + mockClient.asCurrentUser.indices.getMapping = jest.fn().mockResolvedValueOnce({ + '.elastic-connectors-v1': { + mappings: { + _meta: { + pipeline: { + default_extract_binary_content: false, + default_name: 'my-unit-test-index', + default_reduce_whitespace: false, + default_run_ml_inference: true, + }, + }, + }, + }, + }); + + await expect(getIndexPipelineParameters('my-index', client)).resolves.toEqual({ + extract_binary_content: false, + name: 'my-unit-test-index', + reduce_whitespace: false, + run_ml_inference: true, + }); + }); + it('returns connector params with custom pipeline name', async () => { + mockClient.asCurrentUser.indices.getMapping = jest.fn().mockResolvedValueOnce({ + '.elastic-connectors-v1': { + mappings: { + _meta: { + pipeline: { + default_extract_binary_content: false, + default_name: 'my-unit-test-index', + default_reduce_whitespace: false, + default_run_ml_inference: true, + }, + }, + }, + }, + }); + mockClient.asCurrentUser.ingest.getPipeline = jest.fn().mockResolvedValueOnce({ + 'my-index': { + fake: 'ingest-pipeline', + }, + }); + + await expect(getIndexPipelineParameters('my-index', client)).resolves.toEqual({ + extract_binary_content: false, + name: 'my-index', + reduce_whitespace: false, + run_ml_inference: true, + }); + }); + it('returns defaults if get mapping fails with IndexNotFoundException', async () => { + mockClient.asCurrentUser.indices.getMapping = jest.fn().mockRejectedValue({ + meta: { + body: { + error: { + type: 'index_not_found_exception', + }, + }, + }, + }); + + await expect(getIndexPipelineParameters('my-index', client)).resolves.toEqual( + DEFAULT_PIPELINE_VALUES + ); + }); + it('throws if get mapping fails with non-IndexNotFoundException', async () => { + mockClient.asCurrentUser.indices.getMapping = jest.fn().mockRejectedValue('Boom'); + + await expect(getIndexPipelineParameters('my-index', client)).rejects.toEqual('Boom'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/lib/pipelines/get_index_pipeline.ts b/x-pack/plugins/enterprise_search/server/lib/pipelines/get_index_pipeline.ts new file mode 100644 index 0000000000000..45813a109de76 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/pipelines/get_index_pipeline.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IScopedClusterClient } from '@kbn/core/server'; + +import { IngestPipelineParams } from '../../../common/types/connectors'; +import { fetchConnectorByIndexName } from '../connectors/fetch_connectors'; + +import { getDefaultPipeline } from './get_default_pipeline'; + +export const getIndexPipelineParameters = async ( + indexName: string, + client: IScopedClusterClient +): Promise => { + // Get the default pipeline data and check for a custom pipeline in parallel + // we want to throw the error if getDefaultPipeline() fails so we're not catching it on purpose + const [defaultPipeline, connector, customPipelineResp] = await Promise.all([ + getDefaultPipeline(client), + fetchConnectorByIndexName(client, indexName), + client.asCurrentUser.ingest + .getPipeline({ + id: `${indexName}`, + }) + .catch(() => null), + ]); + if (connector && connector.pipeline) { + return connector.pipeline; + } + let pipelineName = defaultPipeline.name; + + if (customPipelineResp && customPipelineResp[indexName]) { + pipelineName = indexName; + } + + return { + ...defaultPipeline, + name: pipelineName, + }; +}; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 779c46907338f..ed6ef8f3886bf 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -203,7 +203,7 @@ export class EnterpriseSearchPlugin implements Plugin { + const indexName = decodeURIComponent(request.params.indexName); + const { client } = (await context.core).elasticsearch; + const body = await getIndexPipelineParameters(indexName, client); + return response.ok({ + body, + headers: { 'content-type': 'application/json' }, + }); + }) + ); + router.get( { path: '/internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors', diff --git a/x-pack/plugins/enterprise_search/server/utils/search_result_provider.ts b/x-pack/plugins/enterprise_search/server/utils/search_result_provider.ts index 13ead48887acb..0869a16b79dc7 100644 --- a/x-pack/plugins/enterprise_search/server/utils/search_result_provider.ts +++ b/x-pack/plugins/enterprise_search/server/utils/search_result_provider.ts @@ -98,32 +98,34 @@ export function getSearchResultProvider( ] : []), ...(config.hasConnectors ? CONNECTOR_DEFINITIONS : []), - ...[ - { - keywords: ['app', 'search', 'engines'], - name: i18n.translate('xpack.enterpriseSearch.searchProvider.appSearch.name', { - defaultMessage: 'App Search', - }), - serviceType: 'app_search', - url: APP_SEARCH_PLUGIN.URL, - }, - { - keywords: ['workplace', 'search'], - name: i18n.translate('xpack.enterpriseSearch.searchProvider.workplaceSearch.name', { - defaultMessage: 'Workplace Search', - }), - serviceType: 'workplace_search', - url: WORKPLACE_SEARCH_PLUGIN.URL, - }, - { - keywords: ['esre', 'search'], - name: i18n.translate('xpack.enterpriseSearch.searchProvider.esre.name', { - defaultMessage: 'ESRE', - }), - serviceType: 'esre', - url: ESRE_PLUGIN.URL, - }, - ], + ...(config.canDeployEntSearch + ? [ + { + keywords: ['app', 'search', 'engines'], + name: i18n.translate('xpack.enterpriseSearch.searchProvider.appSearch.name', { + defaultMessage: 'App Search', + }), + serviceType: 'app_search', + url: APP_SEARCH_PLUGIN.URL, + }, + { + keywords: ['workplace', 'search'], + name: i18n.translate('xpack.enterpriseSearch.searchProvider.workplaceSearch.name', { + defaultMessage: 'Workplace Search', + }), + serviceType: 'workplace_search', + url: WORKPLACE_SEARCH_PLUGIN.URL, + }, + { + keywords: ['esre', 'search'], + name: i18n.translate('xpack.enterpriseSearch.searchProvider.esre.name', { + defaultMessage: 'ESRE', + }), + serviceType: 'esre', + url: ESRE_PLUGIN.URL, + }, + ] + : []), ]; const result = services .map((service) => { diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts index f7503acf77179..10225de1a1ad4 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -424,6 +424,7 @@ export class LensAttributes { operationType: capitalize(operationType), }, }), + customLabel: true, filter: columnFilter, operationType, params: diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts index c083962b0e21b..7e479fad1db0d 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts @@ -48,6 +48,7 @@ export const testMobileKPIAttr = { params: {}, scale: 'ratio', sourceField: 'system.memory.usage', + customLabel: true, dataType: 'number', filter: { query: diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index e40b9f8606fdb..d675b1b42bb36 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -204,3 +204,16 @@ export const DOWNLOAD_SOURCE_API_ROUTES = { UPDATE_PATTERN: `${API_ROOT}/agent_download_sources/{sourceId}`, DELETE_PATTERN: `${API_ROOT}/agent_download_sources/{sourceId}`, }; + +// API versioning constants +export const API_VERSIONS = { + public: { + v1: '2023-10-31', + }, + internal: { + v1: '1', + }, +}; + +export const PUBLIC_API_ACCESS = 'public'; +export const INTERNAL_API_ACCESS = 'internal'; diff --git a/x-pack/plugins/fleet/cypress/e2e/a11y/home_page.cy.ts b/x-pack/plugins/fleet/cypress/e2e/a11y/home_page.cy.ts index 39debfc94662a..7af771d60f9ef 100644 --- a/x-pack/plugins/fleet/cypress/e2e/a11y/home_page.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/a11y/home_page.cy.ts @@ -33,6 +33,8 @@ import { AGENT_POLICY_NAME_LINK } from '../../screens/integrations'; import { cleanupAgentPolicies, unenrollAgent } from '../../tasks/cleanup'; import { setFleetServerHost } from '../../tasks/fleet_server'; +import { API_VERSIONS } from '../../../common/constants'; + describe('Home page', () => { before(() => { setFleetServerHost('https://fleetserver:8220'); @@ -152,7 +154,7 @@ describe('Home page', () => { method: 'POST', url: '/api/fleet/agent_policies', body: { name: 'Agent policy for A11y test', namespace: 'default', id: 'agent-policy-a11y' }, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); beforeEach(() => { @@ -164,7 +166,7 @@ describe('Home page', () => { method: 'POST', url: '/api/fleet/agent_policies/delete', body: { agentPolicyId: 'agent-policy-a11y' }, - headers: { 'kbn-xsrf': 'kibana' }, + headers: { 'kbn-xsrf': 'kibana', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); it('Uninstall Tokens Table', () => { diff --git a/x-pack/plugins/fleet/cypress/e2e/agent_binary_download_source.cy.ts b/x-pack/plugins/fleet/cypress/e2e/agent_binary_download_source.cy.ts index 095a87a28e130..3d5fa3f98fe25 100644 --- a/x-pack/plugins/fleet/cypress/e2e/agent_binary_download_source.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/agent_binary_download_source.cy.ts @@ -15,6 +15,8 @@ import { cleanupDownloadSources } from '../tasks/cleanup'; import { FLEET, navigateTo } from '../tasks/navigation'; import { CONFIRM_MODAL } from '../screens/navigation'; +import { API_VERSIONS } from '../../common/constants'; + describe('Agent binary download source section', () => { beforeEach(() => { cleanupDownloadSources(); @@ -80,7 +82,7 @@ describe('Agent binary download source section', () => { id: 'fleet-local-registry', host: 'https://new-custom-host.co', }, - headers: { 'kbn-xsrf': 'kibana' }, + headers: { 'kbn-xsrf': 'kibana', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); cy.request({ method: 'POST', @@ -93,7 +95,7 @@ describe('Agent binary download source section', () => { id: 'new-agent-policy', download_source_id: 'fleet-local-registry', }, - headers: { 'kbn-xsrf': 'kibana' }, + headers: { 'kbn-xsrf': 'kibana', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }).then((response: any) => { navigateTo('app/fleet/policies/new-agent-policy/settings'); cy.getBySel(AGENT_POLICY_FORM.DOWNLOAD_SOURCE_SELECT).contains('Custom Host'); diff --git a/x-pack/plugins/fleet/cypress/e2e/agent_policy.cy.ts b/x-pack/plugins/fleet/cypress/e2e/agent_policy.cy.ts index 63da2cbbfa389..916590c05c0b6 100644 --- a/x-pack/plugins/fleet/cypress/e2e/agent_policy.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/agent_policy.cy.ts @@ -5,7 +5,8 @@ * 2.0. */ import { TOAST_CLOSE_BTN } from '../screens/navigation'; - +import { setupFleetServer } from '../tasks/fleet_server'; +import { AGENT_FLYOUT, AGENT_POLICY_DETAILS_PAGE } from '../screens/fleet'; describe('Edit agent policy', () => { beforeEach(() => { cy.intercept('/api/fleet/agent_policies/policy-1', { @@ -59,4 +60,68 @@ describe('Edit agent policy', () => { expect(interception.request.body.description).to.equal('desc'); }); }); + + it('should show correct fleet server host for custom URL', () => { + setupFleetServer(); + + cy.intercept('/api/fleet/agent_policies/policy-1', { + item: { + id: 'policy-1', + name: 'Agent policy 1', + description: 'desc', + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + status: 'active', + fleet_server_host_id: 'fleet-server-1', + package_policies: [], + }, + }); + + const apiKey = { + id: 'key-1', + active: true, + api_key_id: 'PefGQYoB0MXWbqVD6jhr', + api_key: 'this-is-the-api-key', + name: 'key-1', + policy_id: 'policy-1', + created_at: '2023-08-29T14:51:10.473Z', + }; + + cy.intercept('/api/fleet/enrollment_api_keys?**', { + items: [apiKey], + total: 1, + page: 1, + perPage: 10000, + }); + cy.intercept('/api/fleet/enrollment_api_keys/key-1', { + item: apiKey, + }); + cy.intercept('/api/fleet/fleet_server_hosts', { + items: [ + { + id: 'fleet-default-fleet-server-host', + name: 'Default', + is_default: true, + host_urls: ['https://192.168.1.23:8220'], + is_preconfigured: true, + }, + { + id: 'fleet-server-1', + name: 'custom host', + host_urls: ['https://xxx.yyy.zzz:443'], + is_default: false, + is_preconfigured: false, + }, + ], + page: 1, + perPage: 10000, + total: 2, + }); + cy.visit('/app/fleet/policies/policy-1'); + + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.ADD_AGENT_LINK).click(); + cy.getBySel(AGENT_FLYOUT.KUBERNETES_PLATFORM_TYPE).click(); + cy.contains('https://xxx.yyy.zzz:443'); + cy.contains('this-is-the-api-key'); + }); }); diff --git a/x-pack/plugins/fleet/cypress/e2e/agents/agent_list.cy.ts b/x-pack/plugins/fleet/cypress/e2e/agents/agent_list.cy.ts index fd85e05e51aec..1e3bf95bb2356 100644 --- a/x-pack/plugins/fleet/cypress/e2e/agents/agent_list.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/agents/agent_list.cy.ts @@ -13,6 +13,8 @@ import { deleteFleetServerDocs, deleteAgentDocs, cleanupAgentPolicies } from '.. import type { CreateAgentPolicyRequest } from '../../../common/types'; import { setUISettings } from '../../tasks/ui_settings'; +import { API_VERSIONS } from '../../../common/constants'; + const createAgentDocs = (kibanaVersion: string) => [ createAgentDoc('agent-1', 'policy-1'), // this agent will have upgrade available createAgentDoc('agent-2', 'policy-2', 'error', kibanaVersion), @@ -66,7 +68,7 @@ function createAgentPolicy(body: CreateAgentPolicyRequest['body']) { cy.request({ method: 'POST', url: '/api/fleet/agent_policies', - headers: { 'kbn-xsrf': 'xx' }, + headers: { 'kbn-xsrf': 'xx', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, body, }); } diff --git a/x-pack/plugins/fleet/cypress/e2e/enrollment_token.cy.ts b/x-pack/plugins/fleet/cypress/e2e/enrollment_token.cy.ts index 84b910c856dfb..57149b435e433 100644 --- a/x-pack/plugins/fleet/cypress/e2e/enrollment_token.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/enrollment_token.cy.ts @@ -8,6 +8,8 @@ import { cleanupAgentPolicies } from '../tasks/cleanup'; import { ENROLLMENT_TOKENS } from '../screens/fleet'; +import { API_VERSIONS } from '../../common/constants'; + describe('Enrollment token page', () => { before(() => { cy.request({ @@ -20,7 +22,7 @@ describe('Enrollment token page', () => { monitoring_enabled: ['logs', 'metrics'], id: 'agent-policy-1', }, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); diff --git a/x-pack/plugins/fleet/cypress/e2e/fleet_agent_flyout.cy.ts b/x-pack/plugins/fleet/cypress/e2e/fleet_agent_flyout.cy.ts index b907fb8ef4c79..6ca1eb669da19 100644 --- a/x-pack/plugins/fleet/cypress/e2e/fleet_agent_flyout.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/fleet_agent_flyout.cy.ts @@ -11,6 +11,8 @@ import { createAgentDoc } from '../tasks/agents'; import { setFleetServerHost } from '../tasks/fleet_server'; import { FLEET, navigateTo } from '../tasks/navigation'; +import { API_VERSIONS } from '../../common/constants'; + const FLEET_SERVER_POLICY_ID = 'fleet-server-policy'; function cleanUp() { @@ -28,7 +30,7 @@ describe('Fleet add agent flyout', () => { cy.request({ method: 'POST', url: '/api/fleet/agent_policies', - headers: { 'kbn-xsrf': 'xx' }, + headers: { 'kbn-xsrf': 'xx', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, body: { id: FLEET_SERVER_POLICY_ID, name: 'Fleet Server policy', diff --git a/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts b/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts index 3e570d0e76f78..1ce6636198fe7 100644 --- a/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts @@ -31,6 +31,7 @@ import { import { LOADING_SPINNER, CONFIRM_MODAL } from '../screens/navigation'; import { ADD_PACKAGE_POLICY_BTN } from '../screens/fleet'; import { cleanupAgentPolicies } from '../tasks/cleanup'; +import { API_VERSIONS } from '../../common/constants'; function setupIntegrations() { cy.intercept( @@ -50,6 +51,28 @@ function setupIntegrations() { cy.wait('@packages'); } +// Infinite scroll +function getAllIntegrations() { + const cardItems = new Set(); + + for (let i = 0; i < 10; i++) { + cy.scrollTo(0, i * 600); + cy.wait(50); + cy.getBySel(INTEGRATION_LIST) + .find('.euiCard') + .each((element) => { + const attrValue = element.attr('data-test-subj'); + if (attrValue) { + cardItems.add(attrValue); + } + }); + } + + return cy.then(() => { + return [...cardItems.values()]; + }); +} + it('should install integration without policy', () => { cy.visit('/app/integrations/detail/tomcat/settings'); @@ -113,7 +136,7 @@ describe('Add Integration - Real API', () => { namespace: 'default', monitoring_enabled: ['logs', 'metrics'], }, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); cy.request('/api/fleet/agent_policies').then((response: any) => { @@ -175,10 +198,15 @@ describe('Add Integration - Real API', () => { cy.getBySel(getIntegrationCategories('aws')).click({ scrollBehavior: false }); cy.getBySel(INTEGRATIONS_SEARCHBAR.BADGE).contains('AWS').should('exist'); - cy.getBySel(INTEGRATION_LIST).find('.euiCard').should('have.length.greaterThan', 29); + + getAllIntegrations().then((items) => { + expect(items).to.have.length.greaterThan(29); + }); cy.getBySel(INTEGRATIONS_SEARCHBAR.INPUT).clear().type('Cloud'); - cy.getBySel(INTEGRATION_LIST).find('.euiCard').should('have.length.greaterThan', 3); + getAllIntegrations().then((items) => { + expect(items).to.have.length.greaterThan(3); + }); cy.getBySel(INTEGRATIONS_SEARCHBAR.REMOVE_BADGE_BUTTON).click(); cy.getBySel(INTEGRATIONS_SEARCHBAR.BADGE).should('not.exist'); }); diff --git a/x-pack/plugins/fleet/cypress/e2e/package_policy_pipelines_and_mappings_real.cy.ts b/x-pack/plugins/fleet/cypress/e2e/package_policy_pipelines_and_mappings_real.cy.ts index 3fed3fef1d94b..60c75327f06ad 100644 --- a/x-pack/plugins/fleet/cypress/e2e/package_policy_pipelines_and_mappings_real.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/package_policy_pipelines_and_mappings_real.cy.ts @@ -17,6 +17,8 @@ const INPUT_TEST_PACKAGE = 'input_package-1.0.0'; const INTEGRATION_TEST_PACKAGE = 'logs_integration-1.0.0'; const INTEGRATION_TEST_PACKAGE_NO_DATASET = 'logs_int_no_dataset-1.0.0'; +import { API_VERSIONS } from '../../common/constants'; + describe('Input package create and edit package policy', () => { const agentPolicyId = 'test-input-package-policy'; const agentPolicyName = 'Test input package policy'; @@ -45,7 +47,7 @@ describe('Input package create and edit package policy', () => { namespace: 'default', monitoring_enabled: [], }, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); after(() => { @@ -53,7 +55,7 @@ describe('Input package create and edit package policy', () => { cy.request({ method: 'POST', url: `/api/fleet/agent_policies/delete`, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, body: JSON.stringify({ agentPolicyId, }), @@ -120,7 +122,7 @@ describe('Integration package with custom dataset create and edit package policy namespace: 'default', monitoring_enabled: [], }, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); after(() => { @@ -128,7 +130,7 @@ describe('Integration package with custom dataset create and edit package policy cy.request({ method: 'POST', url: `/api/fleet/agent_policies/delete`, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, body: JSON.stringify({ agentPolicyId, }), @@ -184,7 +186,7 @@ describe('Integration package with fixed dataset create and edit package policy' namespace: 'default', monitoring_enabled: [], }, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); after(() => { @@ -192,7 +194,7 @@ describe('Integration package with fixed dataset create and edit package policy' cy.request({ method: 'POST', url: `/api/fleet/agent_policies/delete`, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, body: JSON.stringify({ agentPolicyId, }), diff --git a/x-pack/plugins/fleet/cypress/e2e/privileges_editor_role.cy.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_editor_role.cy.ts index 1fab19c3f018a..9923b4888b5e3 100644 --- a/x-pack/plugins/fleet/cypress/e2e/privileges_editor_role.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/privileges_editor_role.cy.ts @@ -17,6 +17,7 @@ import { AGENT_FLYOUT, } from '../screens/fleet'; import { ADD_INTEGRATION_POLICY_BTN } from '../screens/integrations'; +import { scrollToIntegration } from '../tasks/integrations'; const usersToCreate = [BuiltInEditorUser]; @@ -57,6 +58,7 @@ describe('When the user has Editor built-in role', () => { describe('Integrations app', () => { it('are visible and can be added', () => { loginWithUserAndWaitForPage(INTEGRATIONS, BuiltInEditorUser); + scrollToIntegration(getIntegrationCard('apache')); cy.getBySel(getIntegrationCard('apache')).click(); cy.getBySel(ADD_INTEGRATION_POLICY_BTN).should('not.be.disabled'); }); diff --git a/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_all_integrations_read.cy.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_all_integrations_read.cy.ts index 6f1e905e29b63..ae727dbcf7917 100644 --- a/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_all_integrations_read.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_all_integrations_read.cy.ts @@ -25,6 +25,7 @@ import { ADD_PACKAGE_POLICY_BTN, } from '../screens/fleet'; import { ADD_INTEGRATION_POLICY_BTN, AGENT_POLICY_NAME_LINK } from '../screens/integrations'; +import { scrollToIntegration } from '../tasks/integrations'; const rolesToCreate = [FleetAllIntegrReadRole]; const usersToCreate = [FleetAllIntegrReadUser]; @@ -81,6 +82,7 @@ describe('When the user has All privilege for Fleet but Read for integrations', describe('Integrations', () => { it('are visible but cannot be added', () => { loginWithUserAndWaitForPage(INTEGRATIONS, FleetAllIntegrReadUser); + scrollToIntegration(getIntegrationCard('apache')); cy.getBySel(getIntegrationCard('apache')).click(); cy.getBySel(ADD_INTEGRATION_POLICY_BTN).should('be.disabled'); }); diff --git a/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_none_integrations_all.cy.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_none_integrations_all.cy.ts index 71e7d948f928a..535d022dac1f3 100644 --- a/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_none_integrations_all.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_none_integrations_all.cy.ts @@ -15,6 +15,7 @@ import { import { loginWithUserAndWaitForPage, logout } from '../tasks/login'; import { ADD_INTEGRATION_POLICY_BTN, getIntegrationCard } from '../screens/integrations'; +import { scrollToIntegration } from '../tasks/integrations'; const rolesToCreate = [FleetNoneIntegrAllRole]; const usersToCreate = [FleetNoneIntegrAllUser]; @@ -34,7 +35,10 @@ describe('When the user has All privileges for Integrations but None for for Fle it('Integrations are visible but cannot be added', () => { loginWithUserAndWaitForPage(INTEGRATIONS, FleetNoneIntegrAllUser); - cy.getBySel(getIntegrationCard('apache')).click(); + + const integrationCardSelector = getIntegrationCard('apache'); + scrollToIntegration(integrationCardSelector); + cy.getBySel(integrationCardSelector).click(); cy.getBySel(ADD_INTEGRATION_POLICY_BTN).should('be.disabled'); }); }); diff --git a/x-pack/plugins/fleet/cypress/e2e/privileges_viewer_role.cy.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_viewer_role.cy.ts index 25a5ea23caf16..89eed6dcb2077 100644 --- a/x-pack/plugins/fleet/cypress/e2e/privileges_viewer_role.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/privileges_viewer_role.cy.ts @@ -13,6 +13,7 @@ import { getIntegrationCard } from '../screens/integrations'; import { MISSING_PRIVILEGES } from '../screens/fleet'; import { ADD_INTEGRATION_POLICY_BTN } from '../screens/integrations'; +import { scrollToIntegration } from '../tasks/integrations'; const usersToCreate = [BuiltInViewerUser]; @@ -44,6 +45,7 @@ describe('When the user has Viewer built-in role', () => { describe('Integrations', () => { it('are visible but cannot be added', () => { loginWithUserAndWaitForPage(INTEGRATIONS, BuiltInViewerUser); + scrollToIntegration(getIntegrationCard('apache')); cy.getBySel(getIntegrationCard('apache')).click(); cy.getBySel(ADD_INTEGRATION_POLICY_BTN).should('be.disabled'); }); diff --git a/x-pack/plugins/fleet/cypress/e2e/uninstall_token.cy.ts b/x-pack/plugins/fleet/cypress/e2e/uninstall_token.cy.ts index 212e7aa0b3a9e..3832feefe10e1 100644 --- a/x-pack/plugins/fleet/cypress/e2e/uninstall_token.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/uninstall_token.cy.ts @@ -11,6 +11,8 @@ import { cleanupAgentPolicies } from '../tasks/cleanup'; import { UNINSTALL_TOKENS } from '../screens/fleet'; import type { GetUninstallTokenResponse } from '../../common/types/rest_spec/uninstall_token'; +import { API_VERSIONS } from '../../common/constants'; + describe('Uninstall token page', () => { before(() => { cleanupAgentPolicies(); @@ -78,7 +80,7 @@ describe('Uninstall token page', () => { method: 'POST', url: '/api/fleet/agent_policies', body: { name: `Agent policy ${i}00`, namespace: 'default', id: `agent-policy-${i}00` }, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); } }; diff --git a/x-pack/plugins/fleet/cypress/plugins/index.ts b/x-pack/plugins/fleet/cypress/plugins/index.ts index ee01dd20c470c..23f50d0e23197 100644 --- a/x-pack/plugins/fleet/cypress/plugins/index.ts +++ b/x-pack/plugins/fleet/cypress/plugins/index.ts @@ -12,6 +12,8 @@ import fs from 'fs'; import fetch from 'node-fetch'; import { createEsClientForTesting } from '@kbn/test'; +import { API_VERSIONS } from '../../common/constants'; + const plugin: Cypress.PluginConfig = (on, config) => { const client = createEsClientForTesting({ esUrl: config.env.ELASTICSEARCH_URL, @@ -22,8 +24,9 @@ const plugin: Cypress.PluginConfig = (on, config) => { path: string; body?: any; contentType?: string; + version?: string; }) { - const { method, path, body, contentType } = opts; + const { method, path, body, contentType, version } = opts; const Authorization = `Basic ${Buffer.from( `elastic:${config.env.ELASTICSEARCH_PASSWORD}` ).toString('base64')}`; @@ -35,6 +38,7 @@ const plugin: Cypress.PluginConfig = (on, config) => { 'kbn-xsrf': 'cypress', 'Content-Type': contentType || 'application/json', Authorization, + ...(version ? { 'Elastic-Api-Version': version } : {}), }, ...(body ? { body } : {}), }); @@ -75,6 +79,7 @@ const plugin: Cypress.PluginConfig = (on, config) => { path: '/api/fleet/epm/packages', body: Buffer.from(zipContent, 'base64'), contentType: 'application/zip', + version: API_VERSIONS.public.v1, }); }, @@ -82,6 +87,7 @@ const plugin: Cypress.PluginConfig = (on, config) => { return kibanaFetch({ method: 'DELETE', path: `/api/fleet/epm/packages/${packageName}`, + version: API_VERSIONS.public.v1, }); }, }); diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts index 3b8ffcc63b6f6..af799b6066600 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts @@ -88,6 +88,7 @@ export const AGENT_FLYOUT = { MANAGED_TAB: 'managedTab', CONFIRM_AGENT_ENROLLMENT_BUTTON: 'ConfirmAgentEnrollmentButton', INCOMING_DATA_CONFIRMED_CALL_OUT: 'IncomingDataConfirmedCallOut', + KUBERNETES_PLATFORM_TYPE: 'platformTypeKubernetes', }; export const AGENT_POLICIES_CREATE_AGENT_POLICY_FLYOUT = { @@ -228,3 +229,7 @@ export const FLEET_SERVER_SETUP = { export const API_KEYS = { REVOKE_KEY_BUTTON: 'enrollmentTokenTable.revokeBtn', }; + +export const AGENT_POLICY_DETAILS_PAGE = { + ADD_AGENT_LINK: 'addAgentLink', +}; diff --git a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts index 2a1e57271f08a..8ca9a1cb32d25 100644 --- a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts +++ b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { API_VERSIONS } from '../../common/constants'; export function cleanupAgentPolicies() { cy.request('/api/fleet/agent_policies').then((response: any) => { @@ -14,7 +15,7 @@ export function cleanupAgentPolicies() { method: 'POST', url: '/api/fleet/agent_policies/delete', body: { agentPolicyId: policy.id }, - headers: { 'kbn-xsrf': 'kibana' }, + headers: { 'kbn-xsrf': 'kibana', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); }); @@ -28,7 +29,7 @@ export function unenrollAgent() { method: 'POST', url: `api/fleet/agents/${agent.id}/unenroll`, body: { revoke: true }, - headers: { 'kbn-xsrf': 'kibana' }, + headers: { 'kbn-xsrf': 'kibana', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); } @@ -43,7 +44,7 @@ export function cleanupDownloadSources() { cy.request({ method: 'DELETE', url: `/api/fleet/agent_download_sources/${ds.id}`, - headers: { 'kbn-xsrf': 'kibana' }, + headers: { 'kbn-xsrf': 'kibana', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, }); }); }); diff --git a/x-pack/plugins/fleet/cypress/tasks/fleet_server.ts b/x-pack/plugins/fleet/cypress/tasks/fleet_server.ts index 8ec8e75c1d13f..4f9df31eeba60 100644 --- a/x-pack/plugins/fleet/cypress/tasks/fleet_server.ts +++ b/x-pack/plugins/fleet/cypress/tasks/fleet_server.ts @@ -4,27 +4,34 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { API_VERSIONS } from '../../common/constants'; + import { createAgentDoc } from './agents'; const FLEET_SERVER_POLICY_ID = 'fleet-server-policy'; // Create a Fleet server policy -export function setupFleetServer() { - let policyId: string; +export async function setupFleetServer() { + const policyId: string = FLEET_SERVER_POLICY_ID; let kibanaVersion: string; cy.request({ method: 'POST', url: '/api/fleet/agent_policies', headers: { 'kbn-xsrf': 'xx' }, + failOnStatusCode: false, body: { id: FLEET_SERVER_POLICY_ID, name: 'Fleet Server policy', namespace: 'default', has_fleet_server: true, }, - }).then((response: any) => { - policyId = response.body.item.id; + }).then((response) => { + // 409 is expected if the policy already exists + // this allows the test to be run repeatedly in dev + if (response.status > 299 && response.status !== 409) { + throw new Error(`Failed to create Fleet Server policy: ${response.body.message}`); + } }); cy.getKibanaVersion().then((version) => { @@ -61,7 +68,7 @@ export function setFleetServerHost(host = 'https://fleetserver:8220') { cy.request({ method: 'POST', url: '/api/fleet/fleet_server_hosts', - headers: { 'kbn-xsrf': 'xx' }, + headers: { 'kbn-xsrf': 'xx', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, body: { name: 'Default host', host_urls: [host], diff --git a/x-pack/plugins/fleet/cypress/tasks/integrations.ts b/x-pack/plugins/fleet/cypress/tasks/integrations.ts index 71a8c3cd2f9a7..1cbb68cd58bff 100644 --- a/x-pack/plugins/fleet/cypress/tasks/integrations.ts +++ b/x-pack/plugins/fleet/cypress/tasks/integrations.ts @@ -9,11 +9,14 @@ import { ADD_INTEGRATION_POLICY_BTN, CREATE_PACKAGE_POLICY_SAVE_BTN, FLYOUT_CLOSE_BTN_SEL, + INTEGRATION_LIST, } from '../screens/integrations'; import { AGENT_POLICY_SYSTEM_MONITORING_CHECKBOX, EXISTING_HOSTS_TAB } from '../screens/fleet'; import { TOAST_CLOSE_BTN, CONFIRM_MODAL } from '../screens/navigation'; +import { API_VERSIONS } from '../../common/constants'; + export const addIntegration = ({ useExistingPolicy } = { useExistingPolicy: false }) => { cy.getBySel(ADD_INTEGRATION_POLICY_BTN).click(); if (useExistingPolicy) { @@ -53,7 +56,7 @@ export const deleteIntegrations = async () => { response.body.items.forEach((policy: any) => ids.push(policy.id)); cy.request({ url: `/api/fleet/package_policies/delete`, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, body: `{ "packagePolicyIds": ${JSON.stringify(ids)}, "force": true }`, method: 'POST', }); @@ -63,8 +66,24 @@ export const deleteIntegrations = async () => { export const installPackageWithVersion = (integration: string, version: string) => { cy.request({ url: `/api/fleet/epm/packages/${integration}/${version}`, - headers: { 'kbn-xsrf': 'cypress' }, + headers: { 'kbn-xsrf': 'cypress', 'Elastic-Api-Version': `${API_VERSIONS.public.v1}` }, body: '{ "force": true }', method: 'POST', }); }; + +export function scrollToIntegration(selector: string) { + cy.getBySel(INTEGRATION_LIST); + + return cy.window().then(async (win) => { + let found = false; + let i = 0; + while (!found && i < 10) { + win.scroll(0, i++ * 500); + await new Promise((resolve) => setTimeout(resolve, 200)); + if (win.document.querySelector(`[data-test-subj="${selector}"]`)) { + found = true; + } + } + }); +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx index 96bbc5b82565e..5a1ca420a6227 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { AGENTS_PREFIX } from '../../../constants'; import { sendDeleteAgentPolicy, useStartServices, useConfig, sendRequest } from '../../../hooks'; +import { API_VERSIONS } from '../../../../../../common/constants'; interface Props { children: (deleteAgentPolicy: DeleteAgentPolicy) => React.ReactElement; @@ -104,6 +105,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ query: { kuery: `${AGENTS_PREFIX}.policy_id : ${agentPolicyToCheck}`, }, + version: API_VERSIONS.public.v1, }); setAgentsCount(data?.total || 0); setIsLoadingAgentsCount(false); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/page_steps/install_agent/install_agent_managed.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/page_steps/install_agent/install_agent_managed.tsx index 8b4ff121f3e2a..e78f0407baf91 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/page_steps/install_agent/install_agent_managed.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/page_steps/install_agent/install_agent_managed.tsx @@ -78,6 +78,7 @@ export const InstallElasticAgentManagedPageStep: React.FC selectedApiKeyId: enrollmentAPIKey.id, isComplete: commandCopied || !!enrolledAgentIds.length, fullCopyButton: true, + fleetServerHost: fleetServerHosts?.[0], onCopy: () => setCommandCopied(true), }), ]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx index b07af445532a0..3ec77353fd211 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx @@ -69,7 +69,7 @@ export const HeaderRightContent: React.FunctionComponent + {isFleetServerPolicy ? ( } + {...inputs.kafkaPartitionTypeRandomInput.formRowProps} > } + {...inputs.kafkaPartitionTypeRoundRobinInput.formRowProps} > void, output?: Output) { ); const kafkaPartitionTypeRandomInput = useInput( - kafkaOutput?.random?.group_events ? `${kafkaOutput.random.group_events}` : undefined, - undefined, + kafkaOutput?.random?.group_events ? `${kafkaOutput.random.group_events}` : '1', + kafkaPartitionTypeInput.value === kafkaPartitionType.Random + ? validateKafkaPartitioningGroupEvents + : undefined, isDisabled('partition') ); const kafkaPartitionTypeHashInput = useInput( @@ -352,8 +355,10 @@ export function useOutputForm(onSucess: () => void, output?: Output) { isDisabled('partition') ); const kafkaPartitionTypeRoundRobinInput = useInput( - kafkaOutput?.round_robin?.group_events ? `${kafkaOutput.round_robin.group_events}` : undefined, - undefined, + kafkaOutput?.round_robin?.group_events ? `${kafkaOutput.round_robin.group_events}` : '1', + kafkaPartitionTypeInput.value === kafkaPartitionType.RoundRobin + ? validateKafkaPartitioningGroupEvents + : undefined, isDisabled('partition') ); @@ -492,6 +497,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { const sslCertificateValid = sslCertificateInput.validate(); const sslKeyValid = sslKeyInput.validate(); const diskQueuePathValid = diskQueuePathInput.validate(); + const partitioningRandomGroupEventsValid = kafkaPartitionTypeRandomInput.validate(); + const partitioningRoundRobinGroupEventsValid = kafkaPartitionTypeRoundRobinInput.validate(); if (isLogstash) { // validate logstash @@ -516,7 +523,9 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaDefaultTopicValid && kafkaTopicsValid && additionalYamlConfigValid && - kafkaClientIDValid + kafkaClientIDValid && + partitioningRandomGroupEventsValid && + partitioningRoundRobinGroupEventsValid ); } else { // validate ES @@ -546,6 +555,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { sslCertificateInput, sslKeyInput, diskQueuePathInput, + kafkaPartitionTypeRandomInput, + kafkaPartitionTypeRoundRobinInput, isLogstash, isKafka, ]); @@ -674,7 +685,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { : {}), partition: kafkaPartitionTypeInput.value, - ...(kafkaPartitionTypeRandomInput.value + ...(kafkaPartitionTypeInput.value === kafkaPartitionType.Random && + kafkaPartitionTypeRandomInput.value ? { random: { group_events: parseIntegerIfStringDefined( @@ -683,7 +695,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { }, } : {}), - ...(kafkaPartitionTypeRoundRobinInput.value + ...(kafkaPartitionTypeInput.value === kafkaPartitionType.RoundRobin && + kafkaPartitionTypeRoundRobinInput.value ? { round_robin: { group_events: parseIntegerIfStringDefined( @@ -692,7 +705,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { }, } : {}), - ...(kafkaPartitionTypeHashInput.value + ...(kafkaPartitionTypeInput.value === kafkaPartitionType.Hash && + kafkaPartitionTypeHashInput.value ? { hash: { hash: kafkaPartitionTypeHashInput.value, diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx index 1d640e74b81b1..9df23ce8a8e2a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx @@ -13,8 +13,11 @@ import type { IntegrationCardItem } from '../sections/epm/screens/home'; export const searchIdField = 'id'; export const fieldsToSearch = ['name', 'title', 'description']; -export function useLocalSearch(packageList: IntegrationCardItem[]) { +export function useLocalSearch(packageList: IntegrationCardItem[], isLoading: boolean) { const localSearchRef = useRef(new LocalSearch(searchIdField)); + if (isLoading) { + return localSearchRef; + } const localSearch = new LocalSearch(searchIdField); localSearch.indexStrategy = new PrefixIndexStrategy(); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx index 4e4aafa271b3b..71f0370d13832 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx @@ -160,13 +160,11 @@ export const IntegrationPreference = ({ return ( - {prereleaseIntegrationsEnabled !== undefined && ( - - )} + {title} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/controls.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/controls.tsx index dbb20ae57caf3..08004cadeb46d 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/controls.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/controls.tsx @@ -5,31 +5,9 @@ * 2.0. */ -import type { ReactNode } from 'react'; -import React, { useCallback } from 'react'; -import { css } from '@emotion/react'; +import React, { type ReactNode } from 'react'; -import { - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiSpacer, - EuiTitle, - EuiText, -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n-react'; - -import { Loading } from '../../../../components'; - -import type { IntegrationCardItem } from '../../screens/home'; - -import type { ExtendedIntegrationCategory } from '../../screens/home/category_facets'; - -import type { IntegrationsURLParameters } from '../../screens/home/hooks/use_available_packages'; - -import { PackageCard } from '../package_card'; +import { EuiFlexGroup, EuiSpacer, EuiTitle } from '@elastic/eui'; interface ControlsColumnProps { controls: ReactNode; @@ -55,111 +33,3 @@ export const ControlsColumn = ({ controls, title }: ControlsColumnProps) => { ); }; - -interface GridColumnProps { - list: IntegrationCardItem[]; - isLoading: boolean; - showMissingIntegrationMessage?: boolean; - showCardLabels?: boolean; -} - -export const GridColumn = ({ - list, - showMissingIntegrationMessage = false, - showCardLabels = false, - isLoading, -}: GridColumnProps) => { - if (isLoading) return ; - - return ( - - {list.length ? ( - list.map((item) => { - return ( - .euiPopover, - & > .euiPopover > .euiPopover__anchor, - & > .euiPopover > .euiPopover__anchor > .euiCard { - height: 100%; - } - `} - > - - - ); - }) - ) : ( - - -

- {showMissingIntegrationMessage ? ( - - ) : ( - - )} -

-
-
- )} -
- ); -}; - -interface MissingIntegrationContentProps { - resetQuery: () => void; - setSelectedCategory: (category: ExtendedIntegrationCategory) => void; - setUrlandPushHistory: (params: IntegrationsURLParameters) => void; -} - -export const MissingIntegrationContent = ({ - resetQuery, - setSelectedCategory, - setUrlandPushHistory, -}: MissingIntegrationContentProps) => { - const handleCustomInputsLinkClick = useCallback(() => { - resetQuery(); - setSelectedCategory('custom'); - setUrlandPushHistory({ - categoryId: 'custom', - subCategoryId: '', - }); - }, [resetQuery, setSelectedCategory, setUrlandPushHistory]); - - return ( - -

- - - - ), - forumLink: ( - - - - ), - }} - /> -

-
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx new file mode 100644 index 0000000000000..0e85390a4b327 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useRef, useEffect } from 'react'; +import { css } from '@emotion/react'; +import { EuiFlexGrid, EuiFlexItem, EuiSpacer, EuiText, EuiAutoSizer } from '@elastic/eui'; +import { VariableSizeList as List } from 'react-window'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { WindowScroller } from 'react-virtualized'; + +import { Loading } from '../../../../components'; +import type { IntegrationCardItem } from '../../screens/home'; + +import { PackageCard } from '../package_card'; + +interface GridColumnProps { + list: IntegrationCardItem[]; + isLoading: boolean; + showMissingIntegrationMessage?: boolean; + showCardLabels?: boolean; +} + +const VirtualizedRow: React.FC<{ + index: number; + onHeightChange: (index: number, size: number) => void; + style: any; +}> = ({ index, children, style, onHeightChange }) => { + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + onHeightChange(index, ref.current.clientHeight); + } + }, [index, onHeightChange]); + + return ( +
+
+ {children} + +
+
+ ); +}; + +export const GridColumn = ({ + list, + showMissingIntegrationMessage = false, + showCardLabels = false, + isLoading, +}: GridColumnProps) => { + const itemsSizeRefs = useRef(new Map()); + const listRef = useRef(null); + + const onHeightChange = useCallback((index: number, size: number) => { + itemsSizeRefs.current.set(index, size); + if (listRef.current) { + listRef.current.resetAfterIndex(index); + } + }, []); + + if (isLoading) return ; + + if (!list.length) { + return ( + + + +

+ {showMissingIntegrationMessage ? ( + + ) : ( + + )} +

+
+
+
+ ); + } + return ( + <> + { + if (listRef.current) { + listRef.current.scrollTo(scrollTop); + } + }} + > + {() => ( + + {({ width }: { width: number }) => ( + { + const test = itemsSizeRefs.current.get(index) ?? 200; + + return test; + }} + height={window.innerHeight} // plus Don't see an integration message + estimatedItemSize={200} + width={width} + > + {({ index, style }) => { + return ( + + + {list.slice(index * 3, index * 3 + 3).map((item) => { + return ( + .euiPopover, + & > .euiPopover > .euiPopover__anchor, + & > .euiPopover > .euiPopover__anchor > .euiCard { + height: 100%; + } + `} + > + + + ); + })} + + + ); + }} + + )} + + )} + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx index 9f97957b07ef0..97adcaa7e40a6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx @@ -37,7 +37,9 @@ import { ExperimentalFeaturesService } from '../../../../services'; import { promoteFeaturedIntegrations } from '../utils'; -import { ControlsColumn, MissingIntegrationContent, GridColumn } from './controls'; +import { ControlsColumn } from './controls'; +import { GridColumn } from './grid'; +import { MissingIntegrationContent } from './missing_integrations'; import { SearchBox } from './search_box'; export interface Props { @@ -80,7 +82,7 @@ export const PackageListGrid: FunctionComponent = ({ callout, showCardLabels = true, }) => { - const localSearchRef = useLocalSearch(list); + const localSearchRef = useLocalSearch(list, !!isLoading); const [isPopoverOpen, setPopover] = useState(false); @@ -160,79 +162,88 @@ export const PackageListGrid: FunctionComponent = ({ }, [onSubCategoryClick, selectedSubCategory, splitSubcat?.hiddenSubCategories]); return ( - + - - + + + + {showIntegrationsSubcategories && availableSubCategories?.length ? : null} {showIntegrationsSubcategories ? ( - - {visibleSubCategories?.map((subCategory) => { - const isSelected = subCategory.id === selectedSubCategory; - return ( - - onSubCategoryClick(subCategory.id)} + + + {visibleSubCategories?.map((subCategory) => { + const isSelected = subCategory.id === selectedSubCategory; + return ( + + onSubCategoryClick(subCategory.id)} + > + + + + ); + })} + {hiddenSubCategoriesItems?.length ? ( + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" > - - + + - ); - })} - {hiddenSubCategoriesItems?.length ? ( - - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - ) : null} - + ) : null} + +
) : null} {callout ? ( <> @@ -241,12 +252,14 @@ export const PackageListGrid: FunctionComponent = ({ ) : null} - + + + {showMissingIntegrationMessage && ( <> diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/missing_integrations.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/missing_integrations.tsx new file mode 100644 index 0000000000000..2156d0a5658f6 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/missing_integrations.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiLink, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { ExtendedIntegrationCategory } from '../../screens/home/category_facets'; +import type { IntegrationsURLParameters } from '../../screens/home/hooks/use_available_packages'; + +interface MissingIntegrationContentProps { + resetQuery: () => void; + setSelectedCategory: (category: ExtendedIntegrationCategory) => void; + setUrlandPushHistory: (params: IntegrationsURLParameters) => void; +} + +export const MissingIntegrationContent = ({ + resetQuery, + setSelectedCategory, + setUrlandPushHistory, +}: MissingIntegrationContentProps) => { + const handleCustomInputsLinkClick = useCallback(() => { + resetQuery(); + setSelectedCategory('custom'); + setUrlandPushHistory({ + categoryId: 'custom', + subCategoryId: '', + }); + }, [resetQuery, setSelectedCategory, setUrlandPushHistory]); + + return ( + +

+ + + + ), + forumLink: ( + + + + ), + }} + /> +

+
+ ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/search_box.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/search_box.tsx index 593ad3e60e82b..9206de2d48f17 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/search_box.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/search_box.tsx @@ -54,6 +54,24 @@ export const SearchBox: FunctionComponent = ({ }); }; + const onCategoryButtonClick = () => { + if (selectedSubCategory) { + if (setSelectedSubCategory) setSelectedSubCategory(undefined); + setUrlandReplaceHistory({ + categoryId: selectedCategory, + subCategoryId: '', + }); + } else { + setCategory(''); + if (setSelectedSubCategory) setSelectedSubCategory(undefined); + setUrlandReplaceHistory({ + searchString: '', + categoryId: '', + subCategoryId: '', + }); + } + }; + const selectedCategoryTitle = selectedCategory ? categories.find((category) => category.id === selectedCategory)?.title : undefined; @@ -100,15 +118,7 @@ export const SearchBox: FunctionComponent = ({ {getCategoriesLabel}

+ } + body={ +

+ +

+ } + /> + ); + } + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + + + } + body={ + <> + + + + + + + + + } + /> + ); + } + + if (indexStats) { + // using "rowReverse" to keep docs links on the top of the stats code block on smaller screen + return ( + + + + + + + + + + +

+ +

+
+
+
+ + + + +

+ primaries, + totalField: total, + }} + /> +

+
+ + + + + +
+
+ + + + + {JSON.stringify(indexStats, null, 2)} + + + +
+ ); + } + + return null; +}; diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 829e4dda357b0..3daee29ec62e8 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -6,6 +6,7 @@ */ import { METRIC_TYPE } from '@kbn/analytics'; +import { IndicesStatsResponse } from '@elastic/elasticsearch/lib/api/types'; import { API_BASE_PATH, UIM_UPDATE_SETTINGS, @@ -326,3 +327,10 @@ export function useLoadIndexMappings(indexName: string) { method: 'get', }); } + +export function loadIndexStatistics(indexName: string) { + return sendRequest({ + path: `${API_BASE_PATH}/stats/${encodeURIComponent(indexName)}`, + method: 'get', + }); +} diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index 0eb9f62fffd09..3fc34477cacae 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -59,6 +59,7 @@ class DocumentationService { private runtimeFields: string = ''; private indicesComponentTemplate: string = ''; private bulkIndexAlias: string = ''; + private indexStats: string = ''; public setup(docLinks: DocLinksStart): void { const { links } = docLinks; @@ -111,6 +112,7 @@ class DocumentationService { this.runtimeFields = links.runtimeFields.overview; this.indicesComponentTemplate = links.apis.putComponentTemplate; this.bulkIndexAlias = links.apis.bulkIndexAlias; + this.indexStats = links.apis.indexStats; } public getEsDocsBase() { @@ -311,6 +313,10 @@ class DocumentationService { return this.bulkIndexAlias; } + public getIndexStats() { + return this.indexStats; + } + public getWellKnownTextLink() { return 'http://docs.opengeospatial.org/is/12-063r5/12-063r5.html'; } diff --git a/x-pack/plugins/index_management/public/application/services/index.ts b/x-pack/plugins/index_management/public/application/services/index.ts index 5d3cd1f52efe6..5512171b358af 100644 --- a/x-pack/plugins/index_management/public/application/services/index.ts +++ b/x-pack/plugins/index_management/public/application/services/index.ts @@ -26,6 +26,7 @@ export { useLoadNodesPlugins, loadIndex, useLoadIndexMappings, + loadIndexStatistics, } from './api'; export { sortTable } from './sort_table'; diff --git a/x-pack/plugins/infra/common/http_api/metadata_api.ts b/x-pack/plugins/infra/common/http_api/metadata_api.ts index 66d1d01f8175d..360923e5307a1 100644 --- a/x-pack/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/plugins/infra/common/http_api/metadata_api.ts @@ -83,6 +83,14 @@ export const InfraMetadataInfoRT = rt.partial({ cloud: InfraMetadataCloudRT, host: InfraMetadataHostRT, agent: InfraMetadataAgentRT, + '@timestamp': rt.string, +}); + +export const InfraMetadataInfoResponseRT = rt.partial({ + cloud: InfraMetadataCloudRT, + host: InfraMetadataHostRT, + agent: InfraMetadataAgentRT, + timestamp: rt.string, }); const InfraMetadataRequiredRT = rt.type({ @@ -92,7 +100,7 @@ const InfraMetadataRequiredRT = rt.type({ }); const InfraMetadataOptionalRT = rt.partial({ - info: InfraMetadataInfoRT, + info: InfraMetadataInfoResponseRT, }); export const InfraMetadataRT = rt.intersection([InfraMetadataRequiredRT, InfraMetadataOptionalRT]); diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx index 170cd511f463e..8dc2688956a49 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx @@ -230,7 +230,7 @@ export const LogRateAnalysis: FC = ({ r Do not mention indidivual p-values from the analysis results. Do not guess, just say what you are sure of. Do not repeat the given instructions in your output.`; - const now = new Date().toString(); + const now = new Date().toISOString(); return [ { diff --git a/x-pack/plugins/infra/public/components/asset_details/components/metadata_explanation.tsx b/x-pack/plugins/infra/public/components/asset_details/components/metadata_explanation.tsx new file mode 100644 index 0000000000000..bdd77916db02f --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/components/metadata_explanation.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText, EuiLink } from '@elastic/eui'; +import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { Popover } from '../tabs/common/popover'; +import { useMetadataStateProviderContext } from '../hooks/use_metadata_state'; + +const HOSTNAME_DOCS_LINK = + 'https://www.elastic.co/guide/en/ecs/current/ecs-host.html#field-host-name'; + +const MetadataExplanationTooltipContent = React.memo(() => { + const onClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( + + + + + ), + hostName: ( + + + + ), + }} + /> + + ); +}); + +export const MetadataExplanationMessage = () => { + const { metadata, loading } = useMetadataStateProviderContext(); + + return loading ? ( + + ) : metadata?.info?.timestamp ? ( + + + + + ), + time: ( + + ), + }} + /> + + + + + + + + + ) : null; +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/components/processes_explanation.tsx b/x-pack/plugins/infra/public/components/asset_details/components/processes_explanation.tsx new file mode 100644 index 0000000000000..5f1af3f9f8a26 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/components/processes_explanation.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText, EuiLink } from '@elastic/eui'; +import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useDateRangeProviderContext } from '../hooks/use_date_range'; +import { Popover } from '../tabs/common/popover'; + +const DOCUMENTATION_LINK = + 'https://www.elastic.co/guide/en/observability/current/view-infrastructure-metrics.html'; +const SYSTEM_INTEGRATION_DOCS_LINK = 'https://docs.elastic.co/en/integrations/system'; + +const ProcessesExplanationTooltipContent = React.memo(() => { + const onClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( + +

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

+

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

+
+ ); +}); + +export const ProcessesExplanationMessage = () => { + const { getDateRangeInTimestamp } = useDateRangeProviderContext(); + const dateFromRange = new Date(getDateRangeInTimestamp().to); + + return ( + + + + + ), + time: ( + + ), + }} + /> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx index 07baa3c5f589b..6e0cbf9ad2150 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx @@ -4,26 +4,88 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { useEuiTheme, EuiIcon, type EuiPageHeaderProps } from '@elastic/eui'; +import { + useEuiTheme, + EuiIcon, + type EuiPageHeaderProps, + type EuiBreadcrumbsProps, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { css } from '@emotion/react'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; import React, { useCallback, useMemo } from 'react'; import { capitalize } from 'lodash'; +import { useHistory, useLocation } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { APM_HOST_FILTER_FIELD } from '../constants'; import { LinkToAlertsRule, LinkToApmServices, LinkToNodeDetails } from '../links'; -import { FlyoutTabIds, type LinkOptions, type Tab, type TabIds } from '../types'; +import { FlyoutTabIds, type RouteState, type LinkOptions, type Tab, type TabIds } from '../types'; import { useAssetDetailsRenderPropsContext } from './use_asset_details_render_props'; import { useDateRangeProviderContext } from './use_date_range'; import { useTabSwitcherContext } from './use_tab_switcher'; type TabItem = NonNullable['tabs']>[number]; -export const usePageHeader = (tabs: Tab[], links?: LinkOptions[]) => { +export const usePageHeader = (tabs: Tab[] = [], links: LinkOptions[] = []) => { const { rightSideItems } = useRightSideItems(links); const { tabEntries } = useTabs(tabs); + const { breadcrumbs } = useTemplateHeaderBreadcrumbs(); + + return { rightSideItems, tabEntries, breadcrumbs }; +}; + +export const useTemplateHeaderBreadcrumbs = () => { + const history = useHistory(); + const location = useLocation(); + const { + services: { + application: { navigateToApp }, + }, + } = useKibanaContextForPlugin(); + + const onClick = (e: React.MouseEvent) => { + if (location.state) { + navigateToApp(location.state.originAppId, { + replace: true, + path: `${location.state.originPathname}${location.state.originSearch}`, + }); + } else { + history.goBack(); + } + e.preventDefault(); + }; + + const breadcrumbs: EuiBreadcrumbsProps['breadcrumbs'] = + // If there is a state object in location, it's persisted in case the page is opened in a new tab or after page refresh + // With that, we can show the return button. Otherwise, it will be hidden (ex: the user opened a shared URL or opened the page from their bookmarks) + location.state || history.length > 1 + ? [ + { + text: ( + + + + + + + + + ), + color: 'primary', + 'aria-current': false, + 'data-test-subj': 'infraAssetDetailsReturnButton', + href: '#', + onClick, + }, + ] + : []; - return { rightSideItems, tabEntries }; + return { breadcrumbs }; }; const useRightSideItems = (links?: LinkOptions[]) => { diff --git a/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx b/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx index 4b0e8a52d400c..87297b247bb35 100644 --- a/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonEmpty } from '@elastic/eui'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import { getNodeDetailUrl } from '../../../pages/link_to'; +import { useNodeDetailsRedirect } from '../../../pages/link_to'; import type { InventoryItemType } from '../../../../common/inventory_models/types'; export interface LinkToNodeDetailsProps { @@ -22,13 +22,16 @@ export const LinkToNodeDetails = ({ assetType, dateRangeTimestamp, }: LinkToNodeDetailsProps) => { + const { getNodeDetailUrl } = useNodeDetailsRedirect(); const nodeDetailMenuItemLinkProps = useLinkProps({ ...getNodeDetailUrl({ nodeType: assetType, nodeId: asset.id, - from: dateRangeTimestamp.from, - to: dateRangeTimestamp.to, - assetName: asset.name, + search: { + from: dateRangeTimestamp.from, + to: dateRangeTimestamp.to, + assetName: asset.name, + }, }), }); diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx index 263c61d46230b..823f330e742a7 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { EuiPopover, EuiIcon, IconType } from '@elastic/eui'; +import { PanelPaddingSize } from '@elastic/eui'; +import { EuiPopover, EuiIcon, type IconType, type IconColor, type IconSize } from '@elastic/eui'; import { css } from '@emotion/react'; import React from 'react'; import { useBoolean } from '../../../../hooks/use_boolean'; @@ -13,20 +14,28 @@ import { useBoolean } from '../../../../hooks/use_boolean'; export const Popover = ({ children, icon, + iconColor, + iconSize, + panelPaddingSize, ...props }: { children: React.ReactNode; icon: IconType; + iconColor?: IconColor; + iconSize?: IconSize; + panelPaddingSize?: PanelPaddingSize; 'data-test-subj'?: string; }) => { const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); return ( { } return ( - + <> + + +
+ ); }; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx index e19d261094d8f..7132587588f24 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx @@ -56,10 +56,7 @@ export const MetadataHeader = ({ metadataValue }: MetadataSummaryProps) => { {columnTitles[metadataValue.field as MetadataFields]} - + {metadataValue.tooltipLink ? ( + <> + + + + + + + + + + + + + + + + {(isCompactView ? metadataData(metadata?.info) @@ -98,21 +130,6 @@ export const MetadataSummaryList = ({ ) )} - - - - - - + ); }; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx index 9b5953f9292dd..9b4036c1cdd13 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx @@ -121,7 +121,7 @@ const MetricsSectionTitle = () => { - + diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx index f0568332328dc..fc4448d7f067c 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx @@ -19,6 +19,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { parseSearchString } from './parse_search_string'; import { ProcessesTable } from './processes_table'; import { STATE_NAMES } from './states'; @@ -31,6 +32,7 @@ import { import { getFieldByType } from '../../../../../common/inventory_models'; import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { useDateRangeProviderContext } from '../../hooks/use_date_range'; +import { ProcessesExplanationMessage } from '../../components/processes_explanation'; import { useAssetDetailsUrlState } from '../../hooks/use_asset_details_url_state'; const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({ @@ -44,6 +46,7 @@ export const Processes = () => { const { asset, assetType } = useAssetDetailsRenderPropsContext(); const [searchText, setSearchText] = useState(urlState?.processSearch ?? ''); + const [searchQueryError, setSearchQueryError] = useState(null); const [searchBarState, setSearchBarState] = useState(() => searchText ? Query.parse(searchText) : Query.MATCH_ALL ); @@ -69,22 +72,28 @@ export const Processes = () => { const debouncedSearchOnChange = useMemo(() => { return debounce<(queryText: string) => void>((queryText) => { - setUrlState({ processSearch: queryText }); setSearchText(queryText); }, 500); - }, [setUrlState]); + }, []); const searchBarOnChange = useCallback( - ({ query, queryText }) => { - setSearchBarState(query); - debouncedSearchOnChange(queryText); + ({ query, queryText, error: queryError }) => { + if (queryError) { + setSearchQueryError(queryError); + } else { + setUrlState({ processSearch: queryText }); + setSearchQueryError(null); + setSearchBarState(query); + debouncedSearchOnChange(queryText); + } }, - [debouncedSearchOnChange] + [debouncedSearchOnChange, setUrlState] ); const clearSearchBar = useCallback(() => { setSearchBarState(Query.MATCH_ALL); setUrlState({ processSearch: '' }); + setSearchQueryError(null); setSearchText(''); }, [setUrlState]); @@ -130,6 +139,11 @@ export const Processes = () => { + {loading ? ( + + ) : ( + !error && (response?.processList ?? []).length > 0 && + )} { isLoading={loading || !response} processList={response?.processList ?? []} sortBy={sortBy} + error={searchQueryError?.message} setSortBy={setSortBy} clearSearchBar={clearSearchBar} /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx index f5505da3a634b..95bfce420e784 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx @@ -27,6 +27,8 @@ import { EuiCode, } from '@elastic/eui'; import { css } from '@emotion/react'; +import { EuiTableRow } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; import { FORMATTERS } from '../../../../../common/formatters'; import type { SortBy } from '../../../../pages/metrics/inventory_view/hooks/use_process_list'; import type { Process } from './types'; @@ -40,6 +42,7 @@ interface TableProps { currentTime: number; isLoading: boolean; sortBy: SortBy; + error?: string; setSortBy: (s: SortBy) => void; clearSearchBar: () => void; } @@ -73,6 +76,7 @@ export const ProcessesTable = ({ currentTime, isLoading, sortBy, + error, setSortBy, clearSearchBar, }: TableProps) => { @@ -182,7 +186,11 @@ export const ProcessesTable = ({ } `} > - + {error ? ( + + ) : ( + + )} ); @@ -205,6 +213,32 @@ const LoadingPlaceholder = () => { ); }; +interface ProcessesTableErrorProps { + error: string; +} + +const ProcessesTableError = ({ error }: ProcessesTableErrorProps) => { + const { euiTheme } = useEuiTheme(); + + return ( + + + {error} + + + ); +}; + interface TableBodyProps { items: Process[]; currentTime: number; diff --git a/x-pack/plugins/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/infra/public/components/asset_details/template/page.tsx index bbf23196bf7ee..b08458731d813 100644 --- a/x-pack/plugins/infra/public/components/asset_details/template/page.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/template/page.tsx @@ -18,7 +18,7 @@ import type { ContentTemplateProps } from '../types'; export const Page = ({ header: { tabs = [], links = [] } }: ContentTemplateProps) => { const { asset, loading } = useAssetDetailsRenderPropsContext(); - const { rightSideItems, tabEntries } = usePageHeader(tabs, links); + const { rightSideItems, tabEntries, breadcrumbs } = usePageHeader(tabs, links); const { headerHeight } = useKibanaHeader(); return loading ? ( @@ -49,6 +49,7 @@ export const Page = ({ header: { tabs = [], links = [] } }: ContentTemplateProps pageTitle={asset.name} tabs={tabEntries} rightSideItems={rightSideItems} + breadcrumbs={breadcrumbs} /> diff --git a/x-pack/plugins/infra/public/components/asset_details/types.ts b/x-pack/plugins/infra/public/components/asset_details/types.ts index b17c2c988eb27..548068d93e661 100644 --- a/x-pack/plugins/infra/public/components/asset_details/types.ts +++ b/x-pack/plugins/infra/public/components/asset_details/types.ts @@ -6,6 +6,7 @@ */ import { TimeRange } from '@kbn/es-query'; +import { Search } from 'history'; import type { InventoryItemType } from '../../../common/inventory_models/types'; export interface Asset { @@ -79,4 +80,10 @@ export interface ContentTemplateProps { header: Pick; } +export interface RouteState { + originAppId: string; + originPathname?: string; + originSearch?: Search; +} + export type DataViewOrigin = 'logs' | 'metrics'; diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/container/container_metrics_table.test.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/container/container_metrics_table.test.tsx index 2395e3bca195a..0698aa9d651da 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/container/container_metrics_table.test.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/container/container_metrics_table.test.tsx @@ -21,6 +21,16 @@ import { createLazyContainerMetricsTable } from './create_lazy_container_metrics import IntegratedContainerMetricsTable from './integrated_container_metrics_table'; import { metricByField } from './use_container_metrics_table'; +jest.mock('../../../pages/link_to', () => ({ + useNodeDetailsRedirect: jest.fn(() => ({ + getNodeDetailUrl: jest.fn(() => ({ + app: 'metrics', + pathname: 'link-to/container-detail/example-01', + search: { from: '1546340400000', to: '1546344000000' }, + })), + })), +})); + describe('ContainerMetricsTable', () => { const timerange = { from: 'now-15m', diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.test.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.test.tsx index 970ec38707b16..c076038d50249 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.test.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.test.tsx @@ -21,6 +21,16 @@ import { HostMetricsTable } from './host_metrics_table'; import IntegratedHostMetricsTable from './integrated_host_metrics_table'; import { metricByField } from './use_host_metrics_table'; +jest.mock('../../../pages/link_to', () => ({ + useNodeDetailsRedirect: jest.fn(() => ({ + getNodeDetailUrl: jest.fn(() => ({ + app: 'metrics', + pathname: 'link-to/host-detail/example-01', + search: { from: '1546340400000', to: '1546344000000' }, + })), + })), +})); + describe('HostMetricsTable', () => { const timerange = { from: 'now-15m', diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.test.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.test.tsx index 10487aa5aae06..27f60b1906003 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.test.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.test.tsx @@ -21,6 +21,16 @@ import IntegratedPodMetricsTable from './integrated_pod_metrics_table'; import { PodMetricsTable } from './pod_metrics_table'; import { metricByField } from './use_pod_metrics_table'; +jest.mock('../../../pages/link_to', () => ({ + useNodeDetailsRedirect: jest.fn(() => ({ + getNodeDetailUrl: jest.fn(() => ({ + app: 'metrics', + pathname: 'link-to/pod-detail/example-01', + search: { from: '1546340400000', to: '1546344000000' }, + })), + })), +})); + describe('PodMetricsTable', () => { const timerange = { from: 'now-15m', diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/shared/components/metrics_node_details_link.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/shared/components/metrics_node_details_link.tsx index d70db66756ffa..fd8868f0218af 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/shared/components/metrics_node_details_link.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/shared/components/metrics_node_details_link.tsx @@ -10,7 +10,7 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; import type { InventoryItemType } from '../../../../../common/inventory_models/types'; -import { getNodeDetailUrl } from '../../../../pages/link_to'; +import { useNodeDetailsRedirect } from '../../../../pages/link_to'; import type { MetricsExplorerTimeOptions } from '../../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; type ExtractStrict = Extract; @@ -28,12 +28,15 @@ export const MetricsNodeDetailsLink = ({ nodeType, timerange, }: MetricsNodeDetailsLinkProps) => { + const { getNodeDetailUrl } = useNodeDetailsRedirect(); const linkProps = useLinkProps( getNodeDetailUrl({ nodeType, nodeId: id, - from: parse(timerange.from)?.valueOf(), - to: parse(timerange.to)?.valueOf(), + search: { + from: parse(timerange.from)?.valueOf(), + to: parse(timerange.to)?.valueOf(), + }, }) ); diff --git a/x-pack/plugins/infra/public/pages/link_to/index.ts b/x-pack/plugins/infra/public/pages/link_to/index.ts index 0991c6dba1936..aae22218a4f83 100644 --- a/x-pack/plugins/infra/public/pages/link_to/index.ts +++ b/x-pack/plugins/infra/public/pages/link_to/index.ts @@ -8,4 +8,5 @@ export { LinkToLogsPage } from './link_to_logs'; export { LinkToMetricsPage } from './link_to_metrics'; export { RedirectToNodeLogs } from './redirect_to_node_logs'; -export { getNodeDetailUrl, RedirectToNodeDetail } from './redirect_to_node_detail'; +export { RedirectToNodeDetail } from './redirect_to_node_detail'; +export { useNodeDetailsRedirect } from './use_node_details_redirect'; diff --git a/x-pack/plugins/infra/public/pages/link_to/query_params.ts b/x-pack/plugins/infra/public/pages/link_to/query_params.ts index e071ec0a82e34..a80f163993588 100644 --- a/x-pack/plugins/infra/public/pages/link_to/query_params.ts +++ b/x-pack/plugins/infra/public/pages/link_to/query_params.ts @@ -33,3 +33,8 @@ export const getNodeNameFromLocation = (location: Location) => { const nameParam = getParamFromQueryString(getQueryStringFromLocation(location), 'assetName'); return nameParam; }; + +export const getStateFromLocation = (location: Location) => { + const nameParam = getParamFromQueryString(getQueryStringFromLocation(location), 'state'); + return nameParam; +}; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx index 8045c95fe78d6..1bd8aa3b793c5 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -6,24 +6,24 @@ */ import React from 'react'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; - -import { LinkDescriptor } from '@kbn/observability-shared-plugin/public'; +import { Redirect, useLocation, useRouteMatch } from 'react-router-dom'; import { replaceMetricTimeInQueryString } from '../metrics/metric_detail/hooks/use_metrics_time'; -import { getFromFromLocation, getToFromLocation, getNodeNameFromLocation } from './query_params'; +import { + getFromFromLocation, + getToFromLocation, + getNodeNameFromLocation, + getStateFromLocation, +} from './query_params'; import { InventoryItemType } from '../../../common/inventory_models/types'; +import { RouteState } from '../../components/asset_details/types'; + +export const RedirectToNodeDetail = () => { + const { + params: { nodeType, nodeId }, + } = useRouteMatch<{ nodeType: InventoryItemType; nodeId: string }>(); -type RedirectToNodeDetailProps = RouteComponentProps<{ - nodeId: string; - nodeType: InventoryItemType; -}>; + const location = useLocation(); -export const RedirectToNodeDetail = ({ - match: { - params: { nodeId, nodeType }, - }, - location, -}: RedirectToNodeDetailProps) => { const searchString = replaceMetricTimeInQueryString( getFromFromLocation(location), getToFromLocation(location) @@ -38,33 +38,21 @@ export const RedirectToNodeDetail = ({ } } - return ; -}; + let state: RouteState | undefined; + try { + const stateFromLocation = getStateFromLocation(location); + state = stateFromLocation ? JSON.parse(stateFromLocation) : undefined; + } catch (err) { + state = undefined; + } -export const getNodeDetailUrl = ({ - nodeType, - nodeId, - to, - from, - assetName, -}: { - nodeType: InventoryItemType; - nodeId: string; - to?: number; - from?: number; - assetName?: string; -}): LinkDescriptor => { - return { - app: 'metrics', - pathname: `link-to/${nodeType}-detail/${nodeId}`, - search: { - ...(assetName ? { assetName } : undefined), - ...(to && from - ? { - to: `${to}`, - from: `${from}`, - } - : undefined), - }, - }; + return ( + + ); }; diff --git a/x-pack/plugins/infra/public/pages/link_to/use_node_details_redirect.test.tsx b/x-pack/plugins/infra/public/pages/link_to/use_node_details_redirect.test.tsx new file mode 100644 index 0000000000000..81b13a652b7eb --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/use_node_details_redirect.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { useNodeDetailsRedirect } from './use_node_details_redirect'; +import { coreMock } from '@kbn/core/public/mocks'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +const coreStartMock = coreMock.createStart(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(() => ({ + pathname: '', + search: '', + })), +})); + +const wrapper = ({ children }: { children: React.ReactNode }): JSX.Element => ( + {children} +); + +describe('useNodeDetailsRedirect', () => { + it('should return the LinkProperties', () => { + const { result } = renderHook(() => useNodeDetailsRedirect(), { wrapper }); + + const fromDateStrig = '2019-01-01T11:00:00Z'; + const toDateStrig = '2019-01-01T12:00:00Z'; + + expect( + result.current.getNodeDetailUrl({ + nodeType: 'host', + nodeId: 'example-01', + search: { + from: new Date(fromDateStrig).getTime(), + to: new Date(toDateStrig).getTime(), + }, + }) + ).toStrictEqual({ + app: 'metrics', + pathname: 'link-to/host-detail/example-01', + search: { from: '1546340400000', to: '1546344000000' }, + }); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/link_to/use_node_details_redirect.ts b/x-pack/plugins/infra/public/pages/link_to/use_node_details_redirect.ts new file mode 100644 index 0000000000000..ea4726b2e6816 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/use_node_details_redirect.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import type { LinkDescriptor } from '@kbn/observability-shared-plugin/public'; +import useObservable from 'react-use/lib/useObservable'; +import type { InventoryItemType } from '../../../common/inventory_models/types'; +import type { RouteState } from '../../components/asset_details/types'; +import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; + +interface QueryParams { + from?: number; + to?: number; + assetName?: string; +} + +export const useNodeDetailsRedirect = () => { + const location = useLocation(); + const { + services: { + application: { currentAppId$ }, + }, + } = useKibanaContextForPlugin(); + + const appId = useObservable(currentAppId$); + const getNodeDetailUrl = useCallback( + ({ + nodeType, + nodeId, + search, + }: { + nodeType: InventoryItemType; + nodeId: string; + search: QueryParams; + }): LinkDescriptor => { + const { to, from, ...rest } = search; + + return { + app: 'metrics', + pathname: `link-to/${nodeType}-detail/${nodeId}`, + search: { + ...rest, + ...(to && from + ? { + to: `${to}`, + from: `${from}`, + } + : undefined), + // While we don't have a shared state between all page in infra, this makes it possible to restore a page state when returning to the previous route + ...(location.search || location.pathname + ? { + state: JSON.stringify({ + originAppId: appId, + originSearch: location.search, + originPathname: location.pathname, + } as RouteState), + } + : undefined), + }, + }; + }, + [location.pathname, appId, location.search] + ); + + return { getNodeDetailUrl }; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/entry_title.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/entry_title.tsx index b853ca3c8f9b1..2019d2efa1c4e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/entry_title.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/entry_title.tsx @@ -6,9 +6,8 @@ */ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip, IconType } from '@elastic/eui'; -import { TimeRange } from '@kbn/es-query'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import { encode } from '@kbn/rison'; +import { useNodeDetailsRedirect } from '../../../../link_to'; import type { CloudProvider, HostNodeRow } from '../../hooks/use_hosts_table'; const cloudIcons: Record = { @@ -20,20 +19,24 @@ const cloudIcons: Record = { interface EntryTitleProps { onClick: () => void; - time: TimeRange; + dateRangeTs: { from: number; to: number }; title: HostNodeRow['title']; } -export const EntryTitle = ({ onClick, time, title }: EntryTitleProps) => { +export const EntryTitle = ({ onClick, dateRangeTs, title }: EntryTitleProps) => { const { name, cloudProvider } = title; + const { getNodeDetailUrl } = useNodeDetailsRedirect(); const link = useLinkProps({ - app: 'metrics', - pathname: `/detail/host/${name}`, - search: { - _a: encode({ time: { ...time, interval: '>=1m' } }), - assetName: name, - }, + ...getNodeDetailUrl({ + nodeId: name, + nodeType: 'host', + search: { + from: dateRangeTs.from, + to: dateRangeTs.to, + assetName: name, + }, + }), }); const iconType = (cloudProvider && cloudIcons[cloudProvider]) || cloudIcons.unknownProvider; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 19ba258e91b11..f70718fa855a6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -127,7 +127,7 @@ const sortTableData = export const useHostsTable = () => { const [selectedItems, setSelectedItems] = useState([]); const { hostNodes } = useHostsViewContext(); - const { parsedDateRange } = useUnifiedSearchContext(); + const { getDateRangeAsTimestamp } = useUnifiedSearchContext(); const [{ detailsItemId, pagination, sorting }, setProperties] = useHostsTableUrlState(); const { services: { @@ -236,7 +236,7 @@ export const useHostsTable = () => { render: (title: HostNodeRow['title']) => ( reportHostEntryClick(title)} /> ), @@ -343,7 +343,7 @@ export const useHostsTable = () => { width: '120px', }, ], - [detailsItemId, parsedDateRange, reportHostEntryClick, setProperties] + [detailsItemId, getDateRangeAsTimestamp, reportHostEntryClick, setProperties] ); const selection: EuiTableSelectionType = { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 14d0ab3c55939..4cca6263362ae 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -99,8 +99,11 @@ export const Layout = React.memo(({ currentView, reload, interval, nodes, loadin const dataBounds = calculateBoundsFromNodes(nodes); const bounds = autoBounds ? dataBounds : boundsOverride; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); + + const formatter = useCallback( + (val: string | number) => createInventoryMetricFormatter(options.metric)(val), + [options.metric] + ); const { onViewChange } = useWaffleViewState(); useEffect(() => { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index 74fd472629198..ed2f3bc7cba1d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -23,7 +23,7 @@ import { PropertiesTab } from './tabs/properties'; import { AnomaliesTab } from './tabs/anomalies/anomalies'; import { OsqueryTab } from './tabs/osquery'; import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN } from './tabs/shared'; -import { getNodeDetailUrl } from '../../../../link_to'; +import { useNodeDetailsRedirect } from '../../../../link_to'; import { findInventoryModel } from '../../../../../../common/inventory_models'; import { navigateToUptime } from '../../lib/navigate_to_uptime'; import { InfraClientCoreStart, InfraClientStartDeps } from '../../../../../types'; @@ -51,6 +51,7 @@ export const NodeContextPopover = ({ const inventoryModel = findInventoryModel(nodeType); const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; const { application, share } = useKibana().services; + const { getNodeDetailUrl } = useNodeDetailsRedirect(); const uiCapabilities = application?.capabilities; const canCreateAlerts = useMemo( () => Boolean(uiCapabilities?.infrastructure?.save), @@ -81,9 +82,11 @@ export const NodeContextPopover = ({ ...getNodeDetailUrl({ nodeType, nodeId: node.id, - from: nodeDetailFrom, - to: currentTime, - assetName: node.name, + search: { + from: nodeDetailFrom, + to: currentTime, + assetName: node.name, + }, }), }); const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 43de39a14c665..fec97c5fc0720 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -24,7 +24,7 @@ import { import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; import { AlertFlyout } from '../../../../../alerting/inventory/components/alert_flyout'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; -import { getNodeDetailUrl } from '../../../../link_to'; +import { useNodeDetailsRedirect } from '../../../../link_to'; import { findInventoryModel, findInventoryFields } from '../../../../../../common/inventory_models'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { navigateToUptime } from '../../lib/navigate_to_uptime'; @@ -38,6 +38,7 @@ interface Props { export const NodeContextMenu: React.FC = withTheme( ({ options, currentTime, node, nodeType }) => { + const { getNodeDetailUrl } = useNodeDetailsRedirect(); const [flyoutVisible, setFlyoutVisible] = useState(false); const inventoryModel = findInventoryModel(nodeType); const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; @@ -79,8 +80,11 @@ export const NodeContextMenu: React.FC = withTheme ...getNodeDetailUrl({ nodeType, nodeId: node.id, - from: nodeDetailFrom, - to: currentTime, + search: { + from: nodeDetailFrom, + to: currentTime, + assetName: node.name, + }, }), }); const apmTracesMenuItemLinkProps = useLinkProps({ diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx index 339374678ab7f..0ef201b612f2f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import dateMath from '@kbn/datemath'; import moment from 'moment'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useTemplateHeaderBreadcrumbs } from '../../../../components/asset_details/hooks/use_page_header'; import { useSourceContext } from '../../../../containers/metrics_source'; import { InventoryMetric, InventoryItemType } from '../../../../../common/inventory_models/types'; import { useNodeDetails } from '../hooks/use_node_details'; @@ -54,6 +55,7 @@ const parseRange = (range: MetricsTimeInput) => { export const NodeDetailsPage = (props: Props) => { const { metricIndicesExist } = useSourceContext(); + const { breadcrumbs } = useTemplateHeaderBreadcrumbs(); const [parsedTimeRange, setParsedTimeRange] = useState(parseRange(props.timeRange)); const { metrics, loading, makeRequest, error } = useNodeDetails( props.requiredMetrics, @@ -96,6 +98,7 @@ export const NodeDetailsPage = (props: Props) => { onRefresh={refetch} />, ], + breadcrumbs, }} > diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx index 36a7d3d853eb7..fa5baa6c9d815 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -8,6 +8,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { useRouteMatch } from 'react-router-dom'; +import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import type { InventoryItemType } from '../../../../common/inventory_models/types'; import { AssetDetailPage } from './asset_detail_page'; import { MetricsTimeProvider } from './hooks/use_metrics_time'; @@ -15,15 +16,19 @@ import { MetricDetailPage } from './metric_detail_page'; export const MetricDetail = () => { const { - params: { type: nodeType }, + params: { type: nodeType, node: nodeName }, } = useRouteMatch<{ type: InventoryItemType; node: string }>(); - const PageContent = () => (nodeType === 'host' ? : ); + useMetricsBreadcrumbs([ + { + text: nodeName, + }, + ]); return ( - + {nodeType === 'host' ? : } ); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx index 6823147c8b6b0..1f049814a23d3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx @@ -7,10 +7,9 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { useLinkProps } from '@kbn/observability-shared-plugin/public'; import { useRouteMatch } from 'react-router-dom'; -import { useMetadata } from '../../../components/asset_details/hooks/use_metadata'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; +import { useMetadata } from '../../../components/asset_details/hooks/use_metadata'; import { useSourceContext } from '../../../containers/metrics_source'; import { InfraLoadingPanel } from '../../../components/loading'; import { findInventoryModel } from '../../../../common/inventory_models'; @@ -19,7 +18,6 @@ import { NodeDetailsPage } from './components/node_details_page'; import type { InventoryItemType } from '../../../../common/inventory_models/types'; import { useMetricsTimeContext } from './hooks/use_metrics_time'; import { MetricsPageTemplate } from '../page_template'; -import { inventoryTitle } from '../../../translations'; export const MetricDetailPage = () => { const { @@ -57,16 +55,7 @@ export const MetricDetailPage = () => { [sideNav] ); - const inventoryLinkProps = useLinkProps({ - app: 'metrics', - pathname: '/inventory', - }); - useMetricsBreadcrumbs([ - { - ...inventoryLinkProps, - text: inventoryTitle, - }, { text: name, }, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx index 8f9106ed04539..248be6093585a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { MetricsExplorerChartContextMenu, createNodeDetailLink, Props } from './chart_context_menu'; +import { MetricsExplorerChartContextMenu, Props } from './chart_context_menu'; import { ReactWrapper, mount } from 'enzyme'; import { options, @@ -40,6 +40,16 @@ const mountComponentWithProviders = (props: Props): ReactWrapper => { ); }; +jest.mock('../../../link_to', () => ({ + useNodeDetailsRedirect: jest.fn(() => ({ + getNodeDetailUrl: jest.fn(() => ({ + app: 'metrics', + pathname: 'link-to/pod-detail/example-01', + search: { from: '1546340400000', to: '1546344000000' }, + })), + })), +})); + describe('MetricsExplorerChartContextMenu', () => { describe('component', () => { it('should just work', async () => { @@ -153,17 +163,4 @@ describe('MetricsExplorerChartContextMenu', () => { expect(component.find('button').length).toBe(1); }); }); - - describe('helpers', () => { - test('createNodeDetailLink()', () => { - const fromDateStrig = '2019-01-01T11:00:00Z'; - const toDateStrig = '2019-01-01T12:00:00Z'; - const link = createNodeDetailLink('host', 'example-01', fromDateStrig, toDateStrig); - expect(link).toStrictEqual({ - app: 'metrics', - pathname: 'link-to/host-detail/example-01', - search: { from: '1546340400000', to: '1546344000000' }, - }); - }); - }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index ac91f16ff040c..0888424c8f2db 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -26,7 +26,7 @@ import { MetricsExplorerChartOptions, } from '../hooks/use_metrics_explorer_options'; import { createTSVBLink } from './helpers/create_tsvb_link'; -import { getNodeDetailUrl } from '../../../link_to/redirect_to_node_detail'; +import { useNodeDetailsRedirect } from '../../../link_to'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; import { HOST_FIELD, POD_FIELD, CONTAINER_FIELD } from '../../../../../common/constants'; @@ -62,20 +62,6 @@ const dateMathExpressionToEpoch = (dateMathExpression: string, roundUp = false): return dateObj.valueOf(); }; -export const createNodeDetailLink = ( - nodeType: InventoryItemType, - nodeId: string, - from: string, - to: string -) => { - return getNodeDetailUrl({ - nodeType, - nodeId, - from: dateMathExpressionToEpoch(from), - to: dateMathExpressionToEpoch(to, true), - }); -}; - export const MetricsExplorerChartContextMenu: React.FC = ({ onFilter, options, @@ -85,6 +71,7 @@ export const MetricsExplorerChartContextMenu: React.FC = ({ uiCapabilities, chartOptions, }: Props) => { + const { getNodeDetailUrl } = useNodeDetailsRedirect(); const [isPopoverOpen, setPopoverState] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); const supportFiltering = options.groupBy != null && onFilter != null; @@ -120,7 +107,16 @@ export const MetricsExplorerChartContextMenu: React.FC = ({ const nodeType = source && options.groupBy && fieldToNodeType(source, options.groupBy); const nodeDetailLinkProps = useLinkProps({ app: 'metrics', - ...(nodeType ? createNodeDetailLink(nodeType, series.id, timeRange.from, timeRange.to) : {}), + ...(nodeType + ? getNodeDetailUrl({ + nodeType, + nodeId: series.id, + search: { + from: dateMathExpressionToEpoch(timeRange.from), + to: dateMathExpressionToEpoch(timeRange.to, true), + }, + }) + : {}), }); const tsvbLinkProps = useLinkProps({ ...createTSVBLink(source, options, series, timeRange, chartOptions), diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 7ea1dbdfa7cd2..04e3f34e067b4 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -130,6 +130,12 @@ export class Plugin implements InfraClientPluginClass { label: 'Logs', sortKey: 200, entries: [ + { + label: 'Explorer', + app: 'observability-log-explorer', + path: '/', + isBetaFeature: true, + }, { label: 'Stream', app: 'logs', path: '/stream' }, { label: 'Anomalies', app: 'logs', path: '/anomalies' }, { label: 'Categories', app: 'logs', path: '/log-categories' }, diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index 18ee48ed5ad09..c452069855265 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -83,7 +83,10 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { id, name, features: [...metricFeatures, ...cloudMetricsFeatures], - info, + info: { + ...info, + timestamp: info['@timestamp'], + }, }), }); } diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts index bc93d1f539e2a..5470efcb1fb47 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -59,7 +59,7 @@ export const getNodeInfo = async ( index: sourceConfiguration.metricAlias, body: { size: 1, - _source: ['host.*', 'cloud.*', 'agent.*'], + _source: ['host.*', 'cloud.*', 'agent.*', TIMESTAMP_FIELD], sort: [{ [TIMESTAMP_FIELD]: 'desc' }], query: { bool: { 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 c86c9931f93da..95cef3bba61bb 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 @@ -123,7 +123,7 @@ export function LensEditConfigurationFlyout({ diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_select.tsx index 909bc64781399..3c5eeaf801143 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_select.tsx @@ -12,11 +12,11 @@ import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; import { useExistingFieldsReader } from '@kbn/unified-field-list/src/hooks/use_existing_fields'; import { FieldOption, FieldOptionValue, FieldPicker } from '@kbn/visualization-ui-components'; +import { getFieldIconType } from '@kbn/unified-field-list'; import type { OperationType } from '../form_based'; import type { OperationSupportMatrix } from './operation_support'; import { fieldContainsData } from '../../../shared_components'; import type { IndexPattern } from '../../../types'; -import { getFieldType } from '../pure_utils'; export type FieldChoiceWithOperationType = FieldOptionValue & { operationType: OperationType; @@ -89,7 +89,7 @@ export function FieldSelect({ value: { type: 'field' as const, field, - dataType: fieldInstance ? getFieldType(fieldInstance) : undefined, + dataType: fieldInstance ? getFieldIconType(fieldInstance) : undefined, // Use the operation directly, or choose the first compatible operation. // All fields are guaranteed to have at least one operation because they // won't appear in the list otherwise diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.test.tsx index 28cc1cedcd804..981beede0330f 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.test.tsx @@ -359,5 +359,73 @@ describe('percentile ranks', () => { .prop('value') ).toEqual('miaou'); }); + + it('should support decimals on dimension edit', () => { + const updateLayerSpy = jest.fn(); + const instance = mount( + + ); + + const input = instance + .find('[data-test-subj="lns-indexPattern-percentile_ranks-input"]') + .find(EuiFieldNumber); + + act(() => { + input.prop('onChange')!({ + currentTarget: { value: '10.5' }, + } as React.ChangeEvent); + }); + + instance.update(); + + expect(updateLayerSpy).toHaveBeenCalled(); + }); + + it('should not support decimals on inline edit', () => { + const updateLayerSpy = jest.fn(); + const instance = mount( + + ); + + const input = instance + .find('[data-test-subj="lns-indexPattern-percentile_ranks-input"]') + .find(EuiFieldNumber); + + act(() => { + input.prop('onChange')!({ + currentTarget: { value: '10.5' }, + } as React.ChangeEvent); + }); + + instance.update(); + + expect(updateLayerSpy).not.toHaveBeenCalled(); + + expect( + instance + .find('[data-test-subj="lns-indexPattern-percentile_ranks-form"]') + .first() + .prop('isInvalid') + ).toEqual(true); + expect( + instance + .find('[data-test-subj="lns-indexPattern-percentile_ranks-input"]') + .find(EuiFieldNumber) + .prop('value') + ).toEqual('10.5'); + }); }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx index 7e071f8ddbd98..eda66e93765a5 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx @@ -189,7 +189,7 @@ export const percentileRanksOperation: OperationDefinition< }); const onChange = useCallback( (value) => { - if (!isValidNumber(value) || Number(value) === currentColumn.params.value) { + if (!isValidNumber(value, isInline) || Number(value) === currentColumn.params.value) { return; } paramEditorUpdater({ @@ -209,7 +209,7 @@ export const percentileRanksOperation: OperationDefinition< }, } as PercentileRanksIndexPatternColumn); }, - [paramEditorUpdater, currentColumn, indexPattern] + [isInline, currentColumn, paramEditorUpdater, indexPattern] ); const { inputValue, handleInputChange: handleInputChangeWithoutValidation } = useDebouncedValue< string | undefined @@ -220,7 +220,7 @@ export const percentileRanksOperation: OperationDefinition< }, { allowFalsyValue: true } ); - const inputValueIsValid = isValidNumber(inputValue); + const inputValueIsValid = isValidNumber(inputValue, isInline); const handleInputChange: EuiFieldNumberProps['onChange'] = useCallback( (e) => { @@ -250,7 +250,7 @@ export const percentileRanksOperation: OperationDefinition< compressed value={inputValue ?? ''} onChange={handleInputChange} - step="any" + step={isInline ? 1 : 'any'} aria-label={percentileRanksLabel} /> diff --git a/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts b/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts index f8c2901b5c230..593d34f450212 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts @@ -66,6 +66,22 @@ describe('Text based languages utils', () => { expect(indexPattern).toBe(''); }); + + it('should return the index pattern for es|ql query', () => { + const indexPattern = getIndexPatternFromTextBasedQuery({ + esql: 'from foo | keep bytes, memory ', + }); + + expect(indexPattern).toBe('foo'); + }); + + it('should return empty index pattern for non es|ql query', () => { + const indexPattern = getIndexPatternFromTextBasedQuery({ + lang1: 'from foo | keep bytes, memory ', + } as unknown as AggregateQuery); + + expect(indexPattern).toBe(''); + }); }); describe('loadIndexPatternRefs', () => { diff --git a/x-pack/plugins/lens/public/datasources/text_based/utils.ts b/x-pack/plugins/lens/public/datasources/text_based/utils.ts index cd8bb2b8c84fa..aea93add73680 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/utils.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/utils.ts @@ -8,7 +8,11 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; -import { type AggregateQuery, getIndexPatternFromSQLQuery } from '@kbn/es-query'; +import { + type AggregateQuery, + getIndexPatternFromSQLQuery, + getIndexPatternFromESQLQuery, +} from '@kbn/es-query'; import type { DatatableColumn } from '@kbn/expressions-plugin/public'; import { generateId } from '../../id_generator'; import { fetchDataFromAggregateQuery } from './fetch_data_from_aggregate_query'; @@ -135,6 +139,9 @@ export function getIndexPatternFromTextBasedQuery(query: AggregateQuery): string if ('sql' in query) { indexPattern = getIndexPatternFromSQLQuery(query.sql); } + if ('esql' in query) { + indexPattern = getIndexPatternFromESQLQuery(query.esql); + } // other textbased queries.... return indexPattern; diff --git a/x-pack/plugins/lens/public/lens_suggestions_api.ts b/x-pack/plugins/lens/public/lens_suggestions_api.ts index d9414db7ff346..cddcf5ade4cf3 100644 --- a/x-pack/plugins/lens/public/lens_suggestions_api.ts +++ b/x-pack/plugins/lens/public/lens_suggestions_api.ts @@ -85,6 +85,9 @@ export const suggestionsApi = ({ visualizationState: activeVisualization.visualizationState, dataViews, }).filter((sug) => !sug.hide && sug.visualizationId !== 'lnsLegacyMetric'); - - return [activeVisualization, ...newSuggestions]; + const suggestionsList = [activeVisualization, ...newSuggestions]; + // until we separate the text based suggestions logic from the dataview one, + // we want to sort XY first + const sortXYFirst = suggestionsList.sort((a, b) => (a.visualizationId === 'lnsXY' ? -1 : 1)); + return sortXYFirst; }; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index ec4276f79bcfd..f4381a8675872 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -20,81 +20,88 @@ import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from import { validateExceptionListSize } from './validate'; export const createEndpointListItemRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-all'], }, path: ENDPOINT_LIST_ITEM_URL, - validate: { - body: buildRouteValidation< - typeof createEndpointListItemRequest, - CreateEndpointListItemRequestDecoded - >(createEndpointListItemRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation< + typeof createEndpointListItemRequest, + CreateEndpointListItemRequestDecoded + >(createEndpointListItemRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { - name, - tags, - meta, - comments, - description, - entries, - item_id: itemId, - os_types: osTypes, - type, - } = request.body; - const exceptionLists = await getExceptionListClient(context); - const exceptionListItem = await exceptionLists.getEndpointListItem({ - id: undefined, - itemId, - }); - if (exceptionListItem != null) { - return siemResponse.error({ - body: `exception list item id: "${itemId}" already exists`, - statusCode: 409, - }); - } else { - const createdList = await exceptionLists.createEndpointListItem({ + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { + name, + tags, + meta, comments, description, entries, - itemId, - meta, - name, - osTypes, - tags, + item_id: itemId, + os_types: osTypes, type, + } = request.body; + const exceptionLists = await getExceptionListClient(context); + const exceptionListItem = await exceptionLists.getEndpointListItem({ + id: undefined, + itemId, }); - const [validated, errors] = validate(createdList, createEndpointListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (exceptionListItem != null) { + return siemResponse.error({ + body: `exception list item id: "${itemId}" already exists`, + statusCode: 409, + }); } else { - const listSizeError = await validateExceptionListSize( - exceptionLists, - ENDPOINT_LIST_ID, - 'agnostic' - ); - if (listSizeError != null) { - await exceptionLists.deleteExceptionListItemById({ - id: createdList.id, - namespaceType: 'agnostic', - }); - return siemResponse.error(listSizeError); + const createdList = await exceptionLists.createEndpointListItem({ + comments, + description, + entries, + itemId, + meta, + name, + osTypes, + tags, + type, + }); + const [validated, errors] = validate(createdList, createEndpointListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + const listSizeError = await validateExceptionListSize( + exceptionLists, + ENDPOINT_LIST_ID, + 'agnostic' + ); + if (listSizeError != null) { + await exceptionLists.deleteExceptionListItemById({ + id: createdList.id, + namespaceType: 'agnostic', + }); + return siemResponse.error(listSizeError); + } + return response.ok({ body: validated ?? {} }); } - return response.ok({ body: validated ?? {} }); } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index ed2f8b9873373..3029c052752f5 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -25,38 +25,43 @@ import { getExceptionListClient } from './utils/get_exception_list_client'; * @param router The router to use. */ export const createEndpointListRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-all'], }, path: ENDPOINT_LIST_URL, - validate: false, - }, - async (context, _, response) => { - const siemResponse = buildSiemResponse(response); - try { - const exceptionLists = await getExceptionListClient(context); - const createdList = await exceptionLists.createEndpointList(); - // We always return ok on a create endpoint list route but with an empty body as - // an additional fetch of the full list would be slower and the UI has everything hard coded - // within it to get the list if it needs details about it. Our goal is to be as fast as possible - // and block the least amount of time with this route since it could end up in various parts of the - // stack at some point such as repeatedly being called by endpoint agents. - const body = createdList ?? {}; - const [validated, errors] = validate(body, createEndpointListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); + }) + .addVersion( + { + validate: false, + version: '2023-10-31', + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = await getExceptionListClient(context); + const createdList = await exceptionLists.createEndpointList(); + // We always return ok on a create endpoint list route but with an empty body as + // an additional fetch of the full list would be slower and the UI has everything hard coded + // within it to get the list if it needs details about it. Our goal is to be as fast as possible + // and block the least amount of time with this route since it could end up in various parts of the + // stack at some point such as repeatedly being called by endpoint agents. + const body = createdList ?? {}; + const [validated, errors] = validate(body, createEndpointListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index dc6f8ce8804a4..cbe76bc4b3203 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -22,114 +22,121 @@ import { endpointDisallowedFields } from './endpoint_disallowed_fields'; import { validateEndpointExceptionItemEntries, validateExceptionListSize } from './validate'; export const createExceptionListItemRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-all'], }, path: EXCEPTION_LIST_ITEM_URL, - validate: { - body: buildRouteValidation< - typeof createExceptionListItemRequest, - CreateExceptionListItemRequestDecoded - >(createExceptionListItemRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation< + typeof createExceptionListItemRequest, + CreateExceptionListItemRequestDecoded + >(createExceptionListItemRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { - namespace_type: namespaceType, - name, - tags, - meta, - comments, - description, - entries, - item_id: itemId, - list_id: listId, - os_types: osTypes, - type, - expire_time: expireTime, - } = request.body; - const exceptionLists = await getExceptionListClient(context); - const exceptionList = await exceptionLists.getExceptionList({ - id: undefined, - listId, - namespaceType, - }); - if (exceptionList == null) { - return siemResponse.error({ - body: `exception list id: "${listId}" does not exist`, - statusCode: 404, - }); - } else { - const exceptionListItem = await exceptionLists.getExceptionListItem({ + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { + namespace_type: namespaceType, + name, + tags, + meta, + comments, + description, + entries, + item_id: itemId, + list_id: listId, + os_types: osTypes, + type, + expire_time: expireTime, + } = request.body; + const exceptionLists = await getExceptionListClient(context); + const exceptionList = await exceptionLists.getExceptionList({ id: undefined, - itemId, + listId, namespaceType, }); - if (exceptionListItem != null) { + if (exceptionList == null) { return siemResponse.error({ - body: `exception list item id: "${itemId}" already exists`, - statusCode: 409, + body: `exception list id: "${listId}" does not exist`, + statusCode: 404, }); } else { - if (exceptionList.type === 'endpoint') { - const error = validateEndpointExceptionItemEntries(request.body.entries); - if (error != null) { - return siemResponse.error(error); - } - for (const entry of entries) { - if (endpointDisallowedFields.includes(entry.field)) { - return siemResponse.error({ - body: `cannot add endpoint exception item on field ${entry.field}`, - statusCode: 400, - }); - } - } - } - const createdList = await exceptionLists.createExceptionListItem({ - comments, - description, - entries, - expireTime, + const exceptionListItem = await exceptionLists.getExceptionListItem({ + id: undefined, itemId, - listId, - meta, - name, namespaceType, - osTypes, - tags, - type, }); - const [validated, errors] = validate(createdList, createExceptionListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (exceptionListItem != null) { + return siemResponse.error({ + body: `exception list item id: "${itemId}" already exists`, + statusCode: 409, + }); } else { - const listSizeError = await validateExceptionListSize( - exceptionLists, + if (exceptionList.type === 'endpoint') { + const error = validateEndpointExceptionItemEntries(request.body.entries); + if (error != null) { + return siemResponse.error(error); + } + for (const entry of entries) { + if (endpointDisallowedFields.includes(entry.field)) { + return siemResponse.error({ + body: `cannot add endpoint exception item on field ${entry.field}`, + statusCode: 400, + }); + } + } + } + const createdList = await exceptionLists.createExceptionListItem({ + comments, + description, + entries, + expireTime, + itemId, listId, - namespaceType - ); - if (listSizeError != null) { - await exceptionLists.deleteExceptionListItemById({ - id: createdList.id, - namespaceType, - }); - return siemResponse.error(listSizeError); + meta, + name, + namespaceType, + osTypes, + tags, + type, + }); + const [validated, errors] = validate(createdList, createExceptionListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + const listSizeError = await validateExceptionListSize( + exceptionLists, + listId, + namespaceType + ); + if (listSizeError != null) { + await exceptionLists.deleteExceptionListItemById({ + id: createdList.id, + namespaceType, + }); + return siemResponse.error(listSizeError); + } + return response.ok({ body: validated ?? {} }); } - return response.ok({ body: validated ?? {} }); } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index 42c4568c74e94..f658178ea9193 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -15,30 +15,37 @@ import { createExceptionListHandler } from '../handlers/create_exception_list_ha import { buildRouteValidation, buildSiemResponse } from './utils'; export const createExceptionListRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-all'], }, path: EXCEPTION_LIST_URL, - validate: { - body: buildRouteValidation< - typeof createExceptionListRequest, - CreateExceptionListRequestDecoded - >(createExceptionListRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation< + typeof createExceptionListRequest, + CreateExceptionListRequestDecoded + >(createExceptionListRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - return await createExceptionListHandler(context, request, response, siemResponse); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + return await createExceptionListHandler(context, request, response, siemResponse); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 7f122030a19c2..58a8c1586a594 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -24,55 +24,62 @@ import { } from './utils'; export const deleteEndpointListItemRoute = (router: ListsPluginRouter): void => { - router.delete( - { + router.versioned + .delete({ + access: 'public', options: { tags: ['access:lists-all'], }, path: ENDPOINT_LIST_ITEM_URL, - validate: { - query: buildRouteValidation< - typeof deleteEndpointListItemRequestQuery, - DeleteEndpointListItemRequestQueryDecoded - >(deleteEndpointListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof deleteEndpointListItemRequestQuery, + DeleteEndpointListItemRequestQueryDecoded + >(deleteEndpointListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const exceptionLists = await getExceptionListClient(context); - const { item_id: itemId, id } = request.query; - if (itemId == null && id == null) { - return siemResponse.error({ - body: 'Either "item_id" or "id" needs to be defined in the request', - statusCode: 400, - }); - } else { - const deleted = await exceptionLists.deleteEndpointListItem({ - id, - itemId, - }); - if (deleted == null) { + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = await getExceptionListClient(context); + const { item_id: itemId, id } = request.query; + if (itemId == null && id == null) { return siemResponse.error({ - body: getErrorMessageExceptionListItem({ id, itemId }), - statusCode: 404, + body: 'Either "item_id" or "id" needs to be defined in the request', + statusCode: 400, }); } else { - const [validated, errors] = validate(deleted, deleteEndpointListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const deleted = await exceptionLists.deleteEndpointListItem({ + id, + itemId, + }); + if (deleted == null) { + return siemResponse.error({ + body: getErrorMessageExceptionListItem({ id, itemId }), + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(deleted, deleteEndpointListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index 1f529e2c1b40e..665795b389e21 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -24,56 +24,63 @@ import { } from './utils'; export const deleteExceptionListItemRoute = (router: ListsPluginRouter): void => { - router.delete( - { + router.versioned + .delete({ + access: 'public', options: { tags: ['access:lists-all'], }, path: EXCEPTION_LIST_ITEM_URL, - validate: { - query: buildRouteValidation< - typeof deleteExceptionListItemRequestQuery, - DeleteExceptionListItemRequestQueryDecoded - >(deleteExceptionListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof deleteExceptionListItemRequestQuery, + DeleteExceptionListItemRequestQueryDecoded + >(deleteExceptionListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const exceptionLists = await getExceptionListClient(context); - const { item_id: itemId, id, namespace_type: namespaceType } = request.query; - if (itemId == null && id == null) { - return siemResponse.error({ - body: 'Either "item_id" or "id" needs to be defined in the request', - statusCode: 400, - }); - } else { - const deleted = await exceptionLists.deleteExceptionListItem({ - id, - itemId, - namespaceType, - }); - if (deleted == null) { + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = await getExceptionListClient(context); + const { item_id: itemId, id, namespace_type: namespaceType } = request.query; + if (itemId == null && id == null) { return siemResponse.error({ - body: getErrorMessageExceptionListItem({ id, itemId }), - statusCode: 404, + body: 'Either "item_id" or "id" needs to be defined in the request', + statusCode: 400, }); } else { - const [validated, errors] = validate(deleted, deleteExceptionListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const deleted = await exceptionLists.deleteExceptionListItem({ + id, + itemId, + namespaceType, + }); + if (deleted == null) { + return siemResponse.error({ + body: getErrorMessageExceptionListItem({ id, itemId }), + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(deleted, deleteExceptionListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index e22acea02c4d9..371b5e8cb5da2 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -24,56 +24,63 @@ import { } from './utils'; export const deleteExceptionListRoute = (router: ListsPluginRouter): void => { - router.delete( - { + router.versioned + .delete({ + access: 'public', options: { tags: ['access:lists-all'], }, path: EXCEPTION_LIST_URL, - validate: { - query: buildRouteValidation< - typeof deleteExceptionListRequestQuery, - DeleteExceptionListRequestQueryDecoded - >(deleteExceptionListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof deleteExceptionListRequestQuery, + DeleteExceptionListRequestQueryDecoded + >(deleteExceptionListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const exceptionLists = await getExceptionListClient(context); - const { list_id: listId, id, namespace_type: namespaceType } = request.query; - if (listId == null && id == null) { - return siemResponse.error({ - body: 'Either "list_id" or "id" needs to be defined in the request', - statusCode: 400, - }); - } else { - const deleted = await exceptionLists.deleteExceptionList({ - id, - listId, - namespaceType, - }); - if (deleted == null) { + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = await getExceptionListClient(context); + const { list_id: listId, id, namespace_type: namespaceType } = request.query; + if (listId == null && id == null) { return siemResponse.error({ - body: getErrorMessageExceptionList({ id, listId }), - statusCode: 404, + body: 'Either "list_id" or "id" needs to be defined in the request', + statusCode: 400, }); } else { - const [validated, errors] = validate(deleted, deleteExceptionListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const deleted = await exceptionLists.deleteExceptionList({ + id, + listId, + namespaceType, + }); + if (deleted == null) { + return siemResponse.error({ + body: getErrorMessageExceptionList({ id, listId }), + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(deleted, deleteExceptionListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts b/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts index b5e44c5c18530..df096206e494c 100644 --- a/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts @@ -19,76 +19,83 @@ import { import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const duplicateExceptionsRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-all'], }, path: `${EXCEPTION_LIST_URL}/_duplicate`, - validate: { - query: buildRouteValidation< - typeof duplicateExceptionListRequestQuery, - DuplicateExceptionListRequestQueryDecoded - >(duplicateExceptionListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof duplicateExceptionListRequestQuery, + DuplicateExceptionListRequestQueryDecoded + >(duplicateExceptionListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); - try { - const { - list_id: listId, - namespace_type: namespaceType, - include_expired_exceptions: includeExpiredExceptionsString, - } = request.query; + try { + const { + list_id: listId, + namespace_type: namespaceType, + include_expired_exceptions: includeExpiredExceptionsString, + } = request.query; - const exceptionListsClient = await getExceptionListClient(context); + const exceptionListsClient = await getExceptionListClient(context); - // fetch list container - const listToDuplicate = await exceptionListsClient.getExceptionList({ - id: undefined, - listId, - namespaceType, - }); + // fetch list container + const listToDuplicate = await exceptionListsClient.getExceptionList({ + id: undefined, + listId, + namespaceType, + }); - if (listToDuplicate == null) { - return siemResponse.error({ - body: `exception list id: "${listId}" does not exist`, - statusCode: 404, + if (listToDuplicate == null) { + return siemResponse.error({ + body: `exception list id: "${listId}" does not exist`, + statusCode: 404, + }); + } + + // Defaults to including expired exceptions if query param is not present + const includeExpiredExceptions = + includeExpiredExceptionsString !== undefined + ? includeExpiredExceptionsString === 'true' + : true; + const duplicatedList = await exceptionListsClient.duplicateExceptionListAndItems({ + includeExpiredExceptions, + list: listToDuplicate, + namespaceType, }); - } - // Defaults to including expired exceptions if query param is not present - const includeExpiredExceptions = - includeExpiredExceptionsString !== undefined - ? includeExpiredExceptionsString === 'true' - : true; - const duplicatedList = await exceptionListsClient.duplicateExceptionListAndItems({ - includeExpiredExceptions, - list: listToDuplicate, - namespaceType, - }); + if (duplicatedList == null) { + return siemResponse.error({ + body: `unable to duplicate exception list with list_id: ${listId} - action not allowed`, + statusCode: 405, + }); + } - if (duplicatedList == null) { + const [validated, errors] = validate(duplicatedList, duplicateExceptionListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); return siemResponse.error({ - body: `unable to duplicate exception list with list_id: ${listId} - action not allowed`, - statusCode: 405, + body: error.message, + statusCode: error.statusCode, }); } - - const [validated, errors] = validate(duplicatedList, duplicateExceptionListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); - } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts index 0ba22841dcd23..5f1787a2de232 100644 --- a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts @@ -14,61 +14,68 @@ import { exportExceptionListRequestQuery } from '../../common/api'; import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const exportExceptionsRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-read'], }, path: `${EXCEPTION_LIST_URL}/_export`, - validate: { - query: buildRouteValidation(exportExceptionListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation(exportExceptionListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const { + id, + list_id: listId, + namespace_type: namespaceType, + include_expired_exceptions: includeExpiredExceptionsString, + } = request.query; + const exceptionListsClient = await getExceptionListClient(context); - try { - const { - id, - list_id: listId, - namespace_type: namespaceType, - include_expired_exceptions: includeExpiredExceptionsString, - } = request.query; - const exceptionListsClient = await getExceptionListClient(context); + // Defaults to including expired exceptions if query param is not present + const includeExpiredExceptions = + includeExpiredExceptionsString !== undefined + ? includeExpiredExceptionsString === 'true' + : true; + const exportContent = await exceptionListsClient.exportExceptionListAndItems({ + id, + includeExpiredExceptions, + listId, + namespaceType, + }); - // Defaults to including expired exceptions if query param is not present - const includeExpiredExceptions = - includeExpiredExceptionsString !== undefined - ? includeExpiredExceptionsString === 'true' - : true; - const exportContent = await exceptionListsClient.exportExceptionListAndItems({ - id, - includeExpiredExceptions, - listId, - namespaceType, - }); + if (exportContent == null) { + return siemResponse.error({ + body: `exception list with list_id: ${listId} or id: ${id} does not exist`, + statusCode: 400, + }); + } - if (exportContent == null) { + return response.ok({ + body: `${exportContent.exportData}${JSON.stringify(exportContent.exportDetails)}\n`, + headers: { + 'Content-Disposition': `attachment; filename="${listId}"`, + 'Content-Type': 'application/ndjson', + }, + }); + } catch (err) { + const error = transformError(err); return siemResponse.error({ - body: `exception list with list_id: ${listId} or id: ${id} does not exist`, - statusCode: 400, + body: error.message, + statusCode: error.statusCode, }); } - - return response.ok({ - body: `${exportContent.exportData}${JSON.stringify(exportContent.exportDetails)}\n`, - headers: { - 'Content-Disposition': `attachment; filename="${listId}"`, - 'Content-Type': 'application/ndjson', - }, - }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index ef72152a3bb8f..152b7f62153f1 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -19,62 +19,69 @@ import { import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const findEndpointListItemRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: `${ENDPOINT_LIST_ITEM_URL}/_find`, - validate: { - query: buildRouteValidation< - typeof findEndpointListItemRequestQuery, - FindEndpointListItemRequestQueryDecoded - >(findEndpointListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof findEndpointListItemRequestQuery, + FindEndpointListItemRequestQueryDecoded + >(findEndpointListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const exceptionLists = await getExceptionListClient(context); - const { - filter, - page, - per_page: perPage, - sort_field: sortField, - sort_order: sortOrder, - } = request.query; + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = await getExceptionListClient(context); + const { + filter, + page, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; - const exceptionListItems = await exceptionLists.findEndpointListItem({ - filter, - page, - perPage, - pit: undefined, - searchAfter: undefined, - sortField, - sortOrder, - }); - if (exceptionListItems == null) { - // Although I have this line of code here, this is an incredibly rare thing to have - // happen as the findEndpointListItem tries to auto-create the endpoint list if - // does not exist. + const exceptionListItems = await exceptionLists.findEndpointListItem({ + filter, + page, + perPage, + pit: undefined, + searchAfter: undefined, + sortField, + sortOrder, + }); + if (exceptionListItems == null) { + // Although I have this line of code here, this is an incredibly rare thing to have + // happen as the findEndpointListItem tries to auto-create the endpoint list if + // does not exist. + return siemResponse.error({ + body: `list id: "${ENDPOINT_LIST_ID}" does not exist`, + statusCode: 404, + }); + } + const [validated, errors] = validate(exceptionListItems, findEndpointListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); return siemResponse.error({ - body: `list id: "${ENDPOINT_LIST_ID}" does not exist`, - statusCode: 404, + body: error.message, + statusCode: error.statusCode, }); } - const [validated, errors] = validate(exceptionListItems, findEndpointListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); - } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index a48b188e8e3bc..c119b5bf166b9 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -19,77 +19,84 @@ import { import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const findExceptionListItemRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: `${EXCEPTION_LIST_ITEM_URL}/_find`, - validate: { - query: buildRouteValidation< - typeof findExceptionListItemRequestQuery, - FindExceptionListItemRequestQueryDecoded - >(findExceptionListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof findExceptionListItemRequestQuery, + FindExceptionListItemRequestQueryDecoded + >(findExceptionListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const exceptionLists = await getExceptionListClient(context); - const { - filter, - list_id: listId, - namespace_type: namespaceType, - page, - per_page: perPage, - search, - sort_field: sortField, - sort_order: sortOrder, - } = request.query; - - if (listId.length !== namespaceType.length) { - return siemResponse.error({ - body: `list_id and namespace_id need to have the same comma separated number of values. Expected list_id length: ${listId.length} to equal namespace_type length: ${namespaceType.length}`, - statusCode: 400, - }); - } else if (listId.length !== filter.length && filter.length !== 0) { - return siemResponse.error({ - body: `list_id and filter need to have the same comma separated number of values. Expected list_id length: ${listId.length} to equal filter length: ${filter.length}`, - statusCode: 400, - }); - } else { - const exceptionListItems = await exceptionLists.findExceptionListsItem({ + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = await getExceptionListClient(context); + const { filter, - listId, - namespaceType, + list_id: listId, + namespace_type: namespaceType, page, - perPage, - pit: undefined, + per_page: perPage, search, - searchAfter: undefined, - sortField, - sortOrder, - }); - if (exceptionListItems == null) { + sort_field: sortField, + sort_order: sortOrder, + } = request.query; + + if (listId.length !== namespaceType.length) { return siemResponse.error({ - body: `exception list id: "${listId}" does not exist`, - statusCode: 404, + body: `list_id and namespace_id need to have the same comma separated number of values. Expected list_id length: ${listId.length} to equal namespace_type length: ${namespaceType.length}`, + statusCode: 400, + }); + } else if (listId.length !== filter.length && filter.length !== 0) { + return siemResponse.error({ + body: `list_id and filter need to have the same comma separated number of values. Expected list_id length: ${listId.length} to equal filter length: ${filter.length}`, + statusCode: 400, }); - } - const [validated, errors] = validate(exceptionListItems, findExceptionListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); } else { - return response.ok({ body: validated ?? {} }); + const exceptionListItems = await exceptionLists.findExceptionListsItem({ + filter, + listId, + namespaceType, + page, + perPage, + pit: undefined, + search, + searchAfter: undefined, + sortField, + sortOrder, + }); + if (exceptionListItems == null) { + return siemResponse.error({ + body: `exception list id: "${listId}" does not exist`, + statusCode: 404, + }); + } + const [validated, errors] = validate(exceptionListItems, findExceptionListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index 5cd8535d84280..d5b950c35e95e 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -19,54 +19,61 @@ import { import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const findExceptionListRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: `${EXCEPTION_LIST_URL}/_find`, - validate: { - query: buildRouteValidation< - typeof findExceptionListRequestQuery, - FindExceptionListRequestQueryDecoded - >(findExceptionListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof findExceptionListRequestQuery, + FindExceptionListRequestQueryDecoded + >(findExceptionListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const exceptionLists = await getExceptionListClient(context); - const { - filter, - page, - namespace_type: namespaceType, - per_page: perPage, - sort_field: sortField, - sort_order: sortOrder, - } = request.query; - const exceptionListItems = await exceptionLists.findExceptionList({ - filter, - namespaceType, - page, - perPage, - pit: undefined, - searchAfter: undefined, - sortField, - sortOrder, - }); - const [validated, errors] = validate(exceptionListItems, findExceptionListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = await getExceptionListClient(context); + const { + filter, + page, + namespace_type: namespaceType, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; + const exceptionListItems = await exceptionLists.findExceptionList({ + filter, + namespaceType, + page, + perPage, + pit: undefined, + searchAfter: undefined, + sortField, + sortOrder, + }); + const [validated, errors] = validate(exceptionListItems, findExceptionListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/import_exceptions_route.ts b/x-pack/plugins/lists/server/routes/import_exceptions_route.ts index 71c269880057a..36ea934e4ea21 100644 --- a/x-pack/plugins/lists/server/routes/import_exceptions_route.ts +++ b/x-pack/plugins/lists/server/routes/import_exceptions_route.ts @@ -28,8 +28,9 @@ import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from * choice to overwrite any matching lists */ export const importExceptionsRoute = (router: ListsPluginRouter, config: ConfigType): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { body: { maxBytes: config.maxImportPayloadBytes, @@ -38,49 +39,55 @@ export const importExceptionsRoute = (router: ListsPluginRouter, config: ConfigT tags: ['access:lists-all'], }, path: `${EXCEPTION_LIST_URL}/_import`, - validate: { - body: schema.any(), // validation on file object is accomplished later in the handler. - query: buildRouteValidation< - typeof importExceptionsRequestQuery, - ImportExceptionsRequestQueryDecoded - >(importExceptionsRequestQuery), + }) + .addVersion( + { + validate: { + request: { + body: schema.any(), // validation on file object is accomplished later in the handler. + query: buildRouteValidation< + typeof importExceptionsRequestQuery, + ImportExceptionsRequestQueryDecoded + >(importExceptionsRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const exceptionListsClient = await getExceptionListClient(context); - const siemResponse = buildSiemResponse(response); + async (context, request, response) => { + const exceptionListsClient = await getExceptionListClient(context); + const siemResponse = buildSiemResponse(response); - try { - const { filename } = request.body.file.hapi; - const fileExtension = extname(filename).toLowerCase(); - if (fileExtension !== '.ndjson') { - return siemResponse.error({ - body: `Invalid file extension ${fileExtension}`, - statusCode: 400, - }); - } + try { + const { filename } = request.body.file.hapi; + const fileExtension = extname(filename).toLowerCase(); + if (fileExtension !== '.ndjson') { + return siemResponse.error({ + body: `Invalid file extension ${fileExtension}`, + statusCode: 400, + }); + } - const importsSummary = await exceptionListsClient.importExceptionListAndItems({ - exceptionsToImport: request.body.file, - generateNewListId: request.query.as_new_list, - maxExceptionsImportSize: config.maxExceptionsImportSize, - overwrite: request.query.overwrite, - }); + const importsSummary = await exceptionListsClient.importExceptionListAndItems({ + exceptionsToImport: request.body.file, + generateNewListId: request.query.as_new_list, + maxExceptionsImportSize: config.maxExceptionsImportSize, + overwrite: request.query.overwrite, + }); - const [validated, errors] = validate(importsSummary, importExceptionsResponse); + const [validated, errors] = validate(importsSummary, importExceptionsResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts b/x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts index 4fc6d2dfd402e..41c4e982a5b81 100644 --- a/x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts +++ b/x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts @@ -19,79 +19,86 @@ import { getExceptionFilterRequest } from '../../../common/api'; import { buildRouteValidation, buildSiemResponse } from '../utils'; export const getExceptionFilterRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'internal', options: { tags: ['access:securitySolution'], }, path: INTERNAL_EXCEPTION_FILTER, - validate: { - body: buildRouteValidation(getExceptionFilterRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation(getExceptionFilterRequest), + }, + }, + version: '1', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const ctx = await context.resolve(['lists']); - const listClient = ctx.lists?.getListClient(); - if (!listClient) { - return siemResponse.error({ body: 'Cannot retrieve list client', statusCode: 500 }); - } - const exceptionListClient = ctx.lists?.getExceptionListClient(); - const exceptionItems: Array = []; - const { - type, - alias = null, - exclude_exceptions: excludeExceptions = true, - chunk_size: chunkSize = 10, - } = request.body; - if (type === 'exception_list_ids') { - const listIds = request.body.exception_list_ids.map( - ({ exception_list_id: listId }) => listId - ); - const namespaceTypes = request.body.exception_list_ids.map( - ({ namespace_type: namespaceType }) => namespaceType - ); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const ctx = await context.resolve(['lists']); + const listClient = ctx.lists?.getListClient(); + if (!listClient) { + return siemResponse.error({ body: 'Cannot retrieve list client', statusCode: 500 }); + } + const exceptionListClient = ctx.lists?.getExceptionListClient(); + const exceptionItems: Array = []; + const { + type, + alias = null, + exclude_exceptions: excludeExceptions = true, + chunk_size: chunkSize = 10, + } = request.body; + if (type === 'exception_list_ids') { + const listIds = request.body.exception_list_ids.map( + ({ exception_list_id: listId }) => listId + ); + const namespaceTypes = request.body.exception_list_ids.map( + ({ namespace_type: namespaceType }) => namespaceType + ); - // Stream the results from the Point In Time (PIT) finder into this array - let items: ExceptionListItemSchema[] = []; - const executeFunctionOnStream = (responseBody: FoundExceptionListItemSchema): void => { - items = [...items, ...responseBody.data]; - }; + // Stream the results from the Point In Time (PIT) finder into this array + let items: ExceptionListItemSchema[] = []; + const executeFunctionOnStream = (responseBody: FoundExceptionListItemSchema): void => { + items = [...items, ...responseBody.data]; + }; - await exceptionListClient?.findExceptionListsItemPointInTimeFinder({ - executeFunctionOnStream, - filter: [], - listId: listIds, - maxSize: undefined, // NOTE: This is unbounded when it is "undefined" - namespaceType: namespaceTypes, - perPage: 1_000, // See https://github.com/elastic/kibana/issues/93770 for choice of 1k - sortField: undefined, - sortOrder: undefined, - }); - exceptionItems.push(...items); - } else { - const { exceptions } = request.body; - exceptionItems.push(...exceptions); - } + await exceptionListClient?.findExceptionListsItemPointInTimeFinder({ + executeFunctionOnStream, + filter: [], + listId: listIds, + maxSize: undefined, // NOTE: This is unbounded when it is "undefined" + namespaceType: namespaceTypes, + perPage: 1_000, // See https://github.com/elastic/kibana/issues/93770 for choice of 1k + sortField: undefined, + sortOrder: undefined, + }); + exceptionItems.push(...items); + } else { + const { exceptions } = request.body; + exceptionItems.push(...exceptions); + } - const { filter } = await buildExceptionFilter({ - alias, - chunkSize, - excludeExceptions, - listClient, - lists: exceptionItems, - startedAt: new Date(), - }); + const { filter } = await buildExceptionFilter({ + alias, + chunkSize, + excludeExceptions, + listClient, + lists: exceptionItems, + startedAt: new Date(), + }); - return response.ok({ body: { filter } ?? {} }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); + return response.ok({ body: { filter } ?? {} }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts b/x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts index 41b684b120da2..2ca2333333c7e 100644 --- a/x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts +++ b/x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts @@ -17,8 +17,9 @@ import type { ListsPluginRouter } from '../../types'; import { buildRouteValidation, buildSiemResponse } from '../utils'; export const internalCreateExceptionListRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'internal', options: { // Access control is set to `read` on purpose, as this route is internal and meant to // ensure we have lists created (if not already) for Endpoint artifacts in order to support @@ -26,26 +27,32 @@ export const internalCreateExceptionListRoute = (router: ListsPluginRouter): voi tags: ['access:lists-read'], }, path: INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL, - validate: { - body: buildRouteValidation< - typeof internalCreateExceptionListSchema, - InternalCreateExceptionListSchemaDecoded - >(internalCreateExceptionListSchema), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation< + typeof internalCreateExceptionListSchema, + InternalCreateExceptionListSchemaDecoded + >(internalCreateExceptionListSchema), + }, + }, + version: '1', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - return await createExceptionListHandler(context, request, response, siemResponse, { - ignoreExisting: true, - }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + return await createExceptionListHandler(context, request, response, siemResponse, { + ignoreExisting: true, + }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts b/x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts index 6f65894d9d559..3b0bc716cade6 100644 --- a/x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts +++ b/x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts @@ -20,141 +20,152 @@ import { findListsBySizeRequestQuery, findListsBySizeResponse } from '../../../c import { buildRouteValidation, buildSiemResponse, getListClient } from '../utils'; export const findListsBySizeRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'internal', options: { tags: ['access:lists-read'], }, path: INTERNAL_FIND_LISTS_BY_SIZE, - validate: { - query: buildRouteValidation(findListsBySizeRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation(findListsBySizeRequestQuery), + }, + }, + version: '1', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const listClient = await getListClient(context); - const { - cursor, - filter: filterOrUndefined, - page: pageOrUndefined, - per_page: perPageOrUndefined, - sort_field: sortField, - sort_order: sortOrder, - } = request.query; + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const listClient = await getListClient(context); + const { + cursor, + filter: filterOrUndefined, + page: pageOrUndefined, + per_page: perPageOrUndefined, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; - const page = pageOrUndefined ?? 1; - const perPage = perPageOrUndefined ?? 20; - const filter = filterOrUndefined ?? ''; - const { - isValid, - errorMessage, - cursor: [currentIndexPosition, searchAfter], - } = decodeCursor({ - cursor, - page, - perPage, - sortField, - }); - if (!isValid) { - return siemResponse.error({ - body: errorMessage, - statusCode: 400, - }); - } else { - const valueLists = await listClient.findList({ - currentIndexPosition, - filter, + const page = pageOrUndefined ?? 1; + const perPage = perPageOrUndefined ?? 20; + const filter = filterOrUndefined ?? ''; + const { + isValid, + errorMessage, + cursor: [currentIndexPosition, searchAfter], + } = decodeCursor({ + cursor, page, perPage, - runtimeMappings: undefined, - searchAfter, sortField, - sortOrder, }); + if (!isValid) { + return siemResponse.error({ + body: errorMessage, + statusCode: 400, + }); + } else { + const valueLists = await listClient.findList({ + currentIndexPosition, + filter, + page, + perPage, + runtimeMappings: undefined, + searchAfter, + sortField, + sortOrder, + }); - const listBooleans: boolean[] = []; - - const chunks = chunk(valueLists.data, 10); - for (const listChunk of chunks) { - const booleans = await Promise.all( - listChunk.map(async (valueList) => { - // Currently the only list types we support for exceptions - if ( - valueList.type !== 'ip_range' && - valueList.type !== 'ip' && - valueList.type !== 'keyword' - ) { - return false; - } + const listBooleans: boolean[] = []; - const list = await listClient.findListItem({ - currentIndexPosition: 0, - filter: '', - listId: valueList.id, - page: 0, - perPage: 0, - runtimeMappings: undefined, - searchAfter: [], - sortField: undefined, - sortOrder: undefined, - }); + const chunks = chunk(valueLists.data, 10); + for (const listChunk of chunks) { + const booleans = await Promise.all( + listChunk.map(async (valueList) => { + // Currently the only list types we support for exceptions + if ( + valueList.type !== 'ip_range' && + valueList.type !== 'ip' && + valueList.type !== 'keyword' + ) { + return false; + } - if ( - valueList.type === 'ip_range' && - list && - list.total < MAXIMUM_SMALL_VALUE_LIST_SIZE - ) { - const rangeList = await listClient.findListItem({ + const list = await listClient.findListItem({ currentIndexPosition: 0, - filter: 'is_cidr: false', + filter: '', listId: valueList.id, page: 0, perPage: 0, - runtimeMappings: { - is_cidr: { - script: ` + runtimeMappings: undefined, + searchAfter: [], + sortField: undefined, + sortOrder: undefined, + }); + + if ( + valueList.type === 'ip_range' && + list && + list.total < MAXIMUM_SMALL_VALUE_LIST_SIZE + ) { + const rangeList = await listClient.findListItem({ + currentIndexPosition: 0, + filter: 'is_cidr: false', + listId: valueList.id, + page: 0, + perPage: 0, + runtimeMappings: { + is_cidr: { + script: ` if (params._source["ip_range"] instanceof String) { emit(true); } else { emit(false); } `, - type: 'boolean', + type: 'boolean', + }, }, - }, - searchAfter: [], - sortField: undefined, - sortOrder: undefined, - }); + searchAfter: [], + sortField: undefined, + sortOrder: undefined, + }); - return rangeList && rangeList.total < MAXIMUM_SMALL_IP_RANGE_VALUE_LIST_DASH_SIZE - ? true - : false; - } - return list && list.total < MAXIMUM_SMALL_VALUE_LIST_SIZE ? true : false; - }) - ); - listBooleans.push(...booleans); - } + return rangeList && + rangeList.total < MAXIMUM_SMALL_IP_RANGE_VALUE_LIST_DASH_SIZE + ? true + : false; + } + return list && list.total < MAXIMUM_SMALL_VALUE_LIST_SIZE ? true : false; + }) + ); + listBooleans.push(...booleans); + } - const smallLists = valueLists.data.filter((valueList, index) => listBooleans[index]); - const largeLists = valueLists.data.filter((valueList, index) => !listBooleans[index]); + const smallLists = valueLists.data.filter((valueList, index) => listBooleans[index]); + const largeLists = valueLists.data.filter((valueList, index) => !listBooleans[index]); - const [validated, errors] = validate({ largeLists, smallLists }, findListsBySizeResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate( + { largeLists, smallLists }, + findListsBySizeResponse + ); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list/create_list_route.ts b/x-pack/plugins/lists/server/routes/list/create_list_route.ts index 7a1eef80aa16c..063056461c20c 100644 --- a/x-pack/plugins/lists/server/routes/list/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/create_list_route.ts @@ -19,71 +19,78 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const createListRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_URL, - validate: { - body: buildRouteValidation( - createListRequest - ), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation( + createListRequest + ), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { name, description, deserializer, id, serializer, type, meta, version } = - request.body; - const lists = await getListClient(context); - const dataStreamExists = await lists.getListDataStreamExists(); - const indexExists = await lists.getListIndexExists(); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, deserializer, id, serializer, type, meta, version } = + request.body; + const lists = await getListClient(context); + const dataStreamExists = await lists.getListDataStreamExists(); + const indexExists = await lists.getListIndexExists(); - if (!dataStreamExists && !indexExists) { - return siemResponse.error({ - body: `To create a list, the data stream must exist first. Data stream "${lists.getListName()}" does not exist`, - statusCode: 400, - }); - } else { - // needs to be migrated to data stream - if (!dataStreamExists && indexExists) { - await lists.migrateListIndexToDataStream(); - } - if (id != null) { - const list = await lists.getList({ id }); - if (list != null) { - return siemResponse.error({ - body: `list id: "${id}" already exists`, - statusCode: 409, - }); + if (!dataStreamExists && !indexExists) { + return siemResponse.error({ + body: `To create a list, the data stream must exist first. Data stream "${lists.getListName()}" does not exist`, + statusCode: 400, + }); + } else { + // needs to be migrated to data stream + if (!dataStreamExists && indexExists) { + await lists.migrateListIndexToDataStream(); + } + if (id != null) { + const list = await lists.getList({ id }); + if (list != null) { + return siemResponse.error({ + body: `list id: "${id}" already exists`, + statusCode: 409, + }); + } + } + const list = await lists.createList({ + description, + deserializer, + id, + immutable: false, + meta, + name, + serializer, + type, + version, + }); + const [validated, errors] = validate(list, createListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); } } - const list = await lists.createList({ - description, - deserializer, - id, - immutable: false, - meta, - name, - serializer, - type, - version, + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, }); - const [validated, errors] = validate(list, createListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); - } } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list/delete_list_route.ts b/x-pack/plugins/lists/server/routes/list/delete_list_route.ts index ba14a2240d7e2..f2571cd44acf9 100644 --- a/x-pack/plugins/lists/server/routes/list/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/delete_list_route.ts @@ -26,112 +26,122 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getExceptionListClient, getListClient } from '..'; export const deleteListRoute = (router: ListsPluginRouter): void => { - router.delete( - { + router.versioned + .delete({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_URL, - validate: { - query: buildRouteValidation(deleteListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation(deleteListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const lists = await getListClient(context); - const exceptionLists = await getExceptionListClient(context); - const { id, deleteReferences, ignoreReferences } = request.query; - let deleteExceptionItemResponses; + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = await getListClient(context); + const exceptionLists = await getExceptionListClient(context); + const { id, deleteReferences, ignoreReferences } = request.query; + let deleteExceptionItemResponses; - // ignoreReferences=true maintains pre-7.11 behavior of deleting value list without performing any additional checks - if (!ignoreReferences) { - // Stream the results from the Point In Time (PIT) finder into this array - let referencedExceptionListItems: ExceptionListItemSchema[] = []; - const executeFunctionOnStream = (foundResponse: FoundExceptionListItemSchema): void => { - referencedExceptionListItems = [...referencedExceptionListItems, ...foundResponse.data]; - }; + // ignoreReferences=true maintains pre-7.11 behavior of deleting value list without performing any additional checks + if (!ignoreReferences) { + // Stream the results from the Point In Time (PIT) finder into this array + let referencedExceptionListItems: ExceptionListItemSchema[] = []; + const executeFunctionOnStream = (foundResponse: FoundExceptionListItemSchema): void => { + referencedExceptionListItems = [ + ...referencedExceptionListItems, + ...foundResponse.data, + ]; + }; - await exceptionLists.findValueListExceptionListItemsPointInTimeFinder({ - executeFunctionOnStream, - maxSize: undefined, // NOTE: This is unbounded when it is "undefined" - perPage: 1_000, // See https://github.com/elastic/kibana/issues/93770 for choice of 1k - sortField: undefined, - sortOrder: undefined, - valueListId: id, - }); - if (referencedExceptionListItems.length) { - // deleteReferences=false to perform dry run and identify referenced exception lists/items - if (deleteReferences) { - // Delete referenced exception list items - // TODO: Create deleteListItems to delete in batch - deleteExceptionItemResponses = await Promise.all( - referencedExceptionListItems.map(async (listItem) => { - // Ensure only the single entry is deleted as there could be a separate value list referenced that is okay to keep // TODO: Add API to delete single entry - const remainingEntries = listItem.entries.filter( - (e) => e.type === 'list' && e.list.id !== id - ); - if (remainingEntries.length === 0) { - // All entries reference value list specified in request, delete entire exception list item - return deleteExceptionListItem(exceptionLists, listItem); - } else { - // Contains more entries than just value list specified in request , patch (doesn't exist yet :) exception list item to remove entry - return updateExceptionListItems(exceptionLists, listItem, remainingEntries); - } - }) - ); - } else { - const referencedExceptionLists = await getReferencedExceptionLists( - exceptionLists, - referencedExceptionListItems - ); - const refError = `Value list '${id}' is referenced in existing exception list(s)`; - const references = referencedExceptionListItems.map((item) => ({ - exception_item: item, - exception_list: referencedExceptionLists.find((l) => l.list_id === item.list_id), - })); + await exceptionLists.findValueListExceptionListItemsPointInTimeFinder({ + executeFunctionOnStream, + maxSize: undefined, // NOTE: This is unbounded when it is "undefined" + perPage: 1_000, // See https://github.com/elastic/kibana/issues/93770 for choice of 1k + sortField: undefined, + sortOrder: undefined, + valueListId: id, + }); + if (referencedExceptionListItems.length) { + // deleteReferences=false to perform dry run and identify referenced exception lists/items + if (deleteReferences) { + // Delete referenced exception list items + // TODO: Create deleteListItems to delete in batch + deleteExceptionItemResponses = await Promise.all( + referencedExceptionListItems.map(async (listItem) => { + // Ensure only the single entry is deleted as there could be a separate value list referenced that is okay to keep // TODO: Add API to delete single entry + const remainingEntries = listItem.entries.filter( + (e) => e.type === 'list' && e.list.id !== id + ); + if (remainingEntries.length === 0) { + // All entries reference value list specified in request, delete entire exception list item + return deleteExceptionListItem(exceptionLists, listItem); + } else { + // Contains more entries than just value list specified in request , patch (doesn't exist yet :) exception list item to remove entry + return updateExceptionListItems(exceptionLists, listItem, remainingEntries); + } + }) + ); + } else { + const referencedExceptionLists = await getReferencedExceptionLists( + exceptionLists, + referencedExceptionListItems + ); + const refError = `Value list '${id}' is referenced in existing exception list(s)`; + const references = referencedExceptionListItems.map((item) => ({ + exception_item: item, + exception_list: referencedExceptionLists.find((l) => l.list_id === item.list_id), + })); - return siemResponse.error({ - body: { - error: { - message: refError, - references, - value_list_id: id, + return siemResponse.error({ + body: { + error: { + message: refError, + references, + value_list_id: id, + }, }, + statusCode: 409, + }); + } + } + } + + const deleted = await lists.deleteList({ id }); + if (deleted == null) { + return siemResponse.error({ + body: `list id: "${id}" was not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, deleteListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ + body: validated ?? { + deleteItemResponses: deleteExceptionItemResponses, }, - statusCode: 409, }); } } - } - - const deleted = await lists.deleteList({ id }); - if (deleted == null) { + } catch (err) { + const error = transformError(err); return siemResponse.error({ - body: `list id: "${id}" was not found`, - statusCode: 404, + body: error.message, + statusCode: error.statusCode, }); - } else { - const [validated, errors] = validate(deleted, deleteListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ - body: validated ?? { - deleteItemResponses: deleteExceptionItemResponses, - }, - }); - } } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; /** diff --git a/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts index 60ecf15a07356..822a18fd13cf1 100644 --- a/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts @@ -18,8 +18,9 @@ import { createStreamFromBuffer } from '../utils/create_stream_from_buffer'; import { getListClient } from '..'; export const importListItemRoute = (router: ListsPluginRouter, config: ConfigType): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { body: { accepts: ['multipart/form-data'], @@ -32,103 +33,109 @@ export const importListItemRoute = (router: ListsPluginRouter, config: ConfigTyp }, }, path: `${LIST_ITEM_URL}/_import`, - validate: { - body: schema.buffer(), - query: buildRouteValidation(importListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + body: schema.buffer(), + query: buildRouteValidation(importListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const stream = createStreamFromBuffer(request.body); - const { deserializer, list_id: listId, serializer, type } = request.query; - const lists = await getListClient(context); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const stream = createStreamFromBuffer(request.body); + const { deserializer, list_id: listId, serializer, type } = request.query; + const lists = await getListClient(context); - const listDataExists = await lists.getListDataStreamExists(); - if (!listDataExists) { - const listIndexExists = await lists.getListIndexExists(); - if (!listIndexExists) { - return siemResponse.error({ - body: `To import a list item, the data steam must exist first. Data stream "${lists.getListName()}" does not exist`, - statusCode: 400, - }); + const listDataExists = await lists.getListDataStreamExists(); + if (!listDataExists) { + const listIndexExists = await lists.getListIndexExists(); + if (!listIndexExists) { + return siemResponse.error({ + body: `To import a list item, the data steam must exist first. Data stream "${lists.getListName()}" does not exist`, + statusCode: 400, + }); + } + // otherwise migration is needed + await lists.migrateListIndexToDataStream(); } - // otherwise migration is needed - await lists.migrateListIndexToDataStream(); - } - const listItemDataExists = await lists.getListItemDataStreamExists(); - if (!listItemDataExists) { - const listItemIndexExists = await lists.getListItemIndexExists(); - if (!listItemIndexExists) { - return siemResponse.error({ - body: `To import a list item, the data steam must exist first. Data stream "${lists.getListItemName()}" does not exist`, - statusCode: 400, - }); + const listItemDataExists = await lists.getListItemDataStreamExists(); + if (!listItemDataExists) { + const listItemIndexExists = await lists.getListItemIndexExists(); + if (!listItemIndexExists) { + return siemResponse.error({ + body: `To import a list item, the data steam must exist first. Data stream "${lists.getListItemName()}" does not exist`, + statusCode: 400, + }); + } + // otherwise migration is needed + await lists.migrateListItemIndexToDataStream(); } - // otherwise migration is needed - await lists.migrateListItemIndexToDataStream(); - } - if (listId != null) { - const list = await lists.getList({ id: listId }); - if (list == null) { - return siemResponse.error({ - body: `list id: "${listId}" does not exist`, - statusCode: 409, + if (listId != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 409, + }); + } + await lists.importListItemsToStream({ + deserializer: list.deserializer, + listId, + meta: undefined, + serializer: list.serializer, + stream, + type: list.type, + version: 1, }); - } - await lists.importListItemsToStream({ - deserializer: list.deserializer, - listId, - meta: undefined, - serializer: list.serializer, - stream, - type: list.type, - version: 1, - }); - const [validated, errors] = validate(list, importListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const [validated, errors] = validate(list, importListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else if (type != null) { + const importedList = await lists.importListItemsToStream({ + deserializer, + listId: undefined, + meta: undefined, + serializer, + stream, + type, + version: 1, + }); + if (importedList == null) { + return siemResponse.error({ + body: 'Unable to parse a valid fileName during import', + statusCode: 400, + }); + } + const [validated, errors] = validate(importedList, importListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } else { - return response.ok({ body: validated ?? {} }); - } - } else if (type != null) { - const importedList = await lists.importListItemsToStream({ - deserializer, - listId: undefined, - meta: undefined, - serializer, - stream, - type, - version: 1, - }); - if (importedList == null) { return siemResponse.error({ - body: 'Unable to parse a valid fileName during import', + body: 'Either type or list_id need to be defined in the query', statusCode: 400, }); } - const [validated, errors] = validate(importedList, importListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); - } - } else { + } catch (err) { + const error = transformError(err); return siemResponse.error({ - body: 'Either type or list_id need to be defined in the query', - statusCode: 400, + body: error.message, + statusCode: error.statusCode, }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list/patch_list_route.ts b/x-pack/plugins/lists/server/routes/list/patch_list_route.ts index 02edd41e4f074..336239854f70b 100644 --- a/x-pack/plugins/lists/server/routes/list/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/patch_list_route.ts @@ -15,52 +15,59 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const patchListRoute = (router: ListsPluginRouter): void => { - router.patch( - { + router.versioned + .patch({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_URL, - validate: { - body: buildRouteValidation(patchListRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation(patchListRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { name, description, id, meta, _version, version } = request.body; - const lists = await getListClient(context); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, meta, _version, version } = request.body; + const lists = await getListClient(context); - const dataStreamExists = await lists.getListDataStreamExists(); - // needs to be migrated to data stream if index exists - if (!dataStreamExists) { - const indexExists = await lists.getListIndexExists(); - if (indexExists) { - await lists.migrateListIndexToDataStream(); + const dataStreamExists = await lists.getListDataStreamExists(); + // needs to be migrated to data stream if index exists + if (!dataStreamExists) { + const indexExists = await lists.getListIndexExists(); + if (indexExists) { + await lists.migrateListIndexToDataStream(); + } } - } - const list = await lists.patchList({ _version, description, id, meta, name, version }); - if (list == null) { - return siemResponse.error({ - body: `list id: "${id}" not found`, - statusCode: 404, - }); - } else { - const [validated, errors] = validate(list, patchListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const list = await lists.patchList({ _version, description, id, meta, name, version }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" not found`, + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(list, patchListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list/read_list_route.ts b/x-pack/plugins/lists/server/routes/list/read_list_route.ts index d7a12a80ec7ef..ab403e933e53f 100644 --- a/x-pack/plugins/lists/server/routes/list/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/read_list_route.ts @@ -15,42 +15,49 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const readListRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: LIST_URL, - validate: { - query: buildRouteValidation(readListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation(readListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { id } = request.query; - const lists = await getListClient(context); - const list = await lists.getList({ id }); - if (list == null) { - return siemResponse.error({ - body: `list id: "${id}" does not exist`, - statusCode: 404, - }); - } else { - const [validated, errors] = validate(list, readListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id } = request.query; + const lists = await getListClient(context); + const list = await lists.getList({ id }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" does not exist`, + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(list, readListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list/update_list_route.ts b/x-pack/plugins/lists/server/routes/list/update_list_route.ts index a134341acd658..46b2d8e058857 100644 --- a/x-pack/plugins/lists/server/routes/list/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/update_list_route.ts @@ -15,52 +15,59 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const updateListRoute = (router: ListsPluginRouter): void => { - router.put( - { + router.versioned + .put({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_URL, - validate: { - body: buildRouteValidation(updateListRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation(updateListRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { name, description, id, meta, _version, version } = request.body; - const lists = await getListClient(context); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, meta, _version, version } = request.body; + const lists = await getListClient(context); - const dataStreamExists = await lists.getListDataStreamExists(); - // needs to be migrated to data stream if index exists - if (!dataStreamExists) { - const indexExists = await lists.getListIndexExists(); - if (indexExists) { - await lists.migrateListIndexToDataStream(); + const dataStreamExists = await lists.getListDataStreamExists(); + // needs to be migrated to data stream if index exists + if (!dataStreamExists) { + const indexExists = await lists.getListIndexExists(); + if (indexExists) { + await lists.migrateListIndexToDataStream(); + } } - } - const list = await lists.updateList({ _version, description, id, meta, name, version }); - if (list == null) { - return siemResponse.error({ - body: `list id: "${id}" not found`, - statusCode: 404, - }); - } else { - const [validated, errors] = validate(list, updateListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const list = await lists.updateList({ _version, description, id, meta, name, version }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" not found`, + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(list, updateListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts index 9116e5d338e5e..e3cab179c2f40 100644 --- a/x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts @@ -11,19 +11,19 @@ import { LIST_INDEX } from '@kbn/securitysolution-list-constants'; import { createListIndexResponse } from '../../../common/api'; import type { ListsPluginRouter } from '../../types'; -import { buildSiemResponse, removeLegacyTemplatesIfExist } from '../utils'; +import { buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const createListIndexRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_INDEX, - validate: false, - }, - async (context, _, response) => { + }) + .addVersion({ validate: false, version: '2023-10-31' }, async (context, _, response) => { const siemResponse = buildSiemResponse(response); try { @@ -43,8 +43,6 @@ export const createListIndexRoute = (router: ListsPluginRouter): void => { await lists.setListItemTemplate(); } - await removeLegacyTemplatesIfExist(lists); - if (listDataStreamExists && listItemDataStreamExists) { return siemResponse.error({ body: `data stream: "${lists.getListName()}" and "${lists.getListItemName()}" already exists`, @@ -79,6 +77,5 @@ export const createListIndexRoute = (router: ListsPluginRouter): void => { statusCode: error.statusCode, }); } - } - ); + }); }; diff --git a/x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts index 2c8a2fb3212ce..a9683f4636151 100644 --- a/x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts @@ -12,7 +12,7 @@ import { LIST_INDEX } from '@kbn/securitysolution-list-constants'; import { ListClient } from '../../services/lists/list_client'; import type { ListsPluginRouter } from '../../types'; import { deleteListIndexResponse } from '../../../common/api'; -import { buildSiemResponse, removeLegacyTemplatesIfExist } from '../utils'; +import { buildSiemResponse } from '../utils'; import { getListClient } from '..'; /** @@ -32,61 +32,72 @@ import { getListClient } from '..'; * And ensuring they're all gone */ export const deleteListIndexRoute = (router: ListsPluginRouter): void => { - router.delete( - { + router.versioned + .delete({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_INDEX, - validate: false, - }, - async (context, _, response) => { - const siemResponse = buildSiemResponse(response); - try { - const lists = await getListClient(context); - const listIndexExists = await lists.getListIndexExists(); - const listItemIndexExists = await lists.getListItemIndexExists(); + }) + .addVersion( + { + validate: false, + version: '2023-10-31', + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = await getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); - const listDataStreamExists = await lists.getListDataStreamExists(); - const listItemDataStreamExists = await lists.getListItemDataStreamExists(); + const listDataStreamExists = await lists.getListDataStreamExists(); + const listItemDataStreamExists = await lists.getListItemDataStreamExists(); - // return early if no data stream or indices exist - if ( - !listDataStreamExists && - !listItemDataStreamExists && - !listIndexExists && - !listItemIndexExists - ) { - return siemResponse.error({ - body: `index and data stream: "${lists.getListName()}" and "${lists.getListItemName()}" does not exist`, - statusCode: 404, - }); - } + // return early if no data stream or indices exist + if ( + !listDataStreamExists && + !listItemDataStreamExists && + !listIndexExists && + !listItemIndexExists + ) { + return siemResponse.error({ + body: `index and data stream: "${lists.getListName()}" and "${lists.getListItemName()}" does not exist`, + statusCode: 404, + }); + } - // ensure data streams deleted if exist - await deleteDataStreams(lists, listDataStreamExists, listItemDataStreamExists); + // ensure data streams deleted if exist + await deleteDataStreams(lists, listDataStreamExists, listItemDataStreamExists); - // ensure indices deleted if exist and were not migrated - await deleteIndices(lists, listIndexExists, listItemIndexExists); + // we need to call this section only if any of these indices exist + // to delete indices, ILM policies and legacy templates + // ILM policies and legacy templates do not exist on serverless, + // so by checking if any of index exists we ensure it is stateful + if (listIndexExists || listItemIndexExists) { + await deleteIndices(lists, listIndexExists, listItemIndexExists); + await lists.deleteLegacyListTemplateIfExists(); + await lists.deleteLegacyListItemTemplateIfExists(); + } - await deleteIndexTemplates(lists); - await removeLegacyTemplatesIfExist(lists); + await deleteIndexTemplates(lists); - const [validated, errors] = validate({ acknowledged: true }, deleteListIndexResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate({ acknowledged: true }, deleteListIndexResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; /** diff --git a/x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts index 079a25ab24b02..1d43d4fd8c181 100644 --- a/x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts @@ -16,48 +16,55 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const exportListItemRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-read'], }, path: `${LIST_ITEM_URL}/_export`, - validate: { - query: buildRouteValidation(exportListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation(exportListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { list_id: listId } = request.query; - const lists = await getListClient(context); - const list = await lists.getList({ id: listId }); - if (list == null) { - return siemResponse.error({ - body: `list_id: ${listId} does not exist`, - statusCode: 400, - }); - } else { - // TODO: Allow the API to override the name of the file to export - const fileName = list.name; + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { list_id: listId } = request.query; + const lists = await getListClient(context); + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list_id: ${listId} does not exist`, + statusCode: 400, + }); + } else { + // TODO: Allow the API to override the name of the file to export + const fileName = list.name; - const stream = new Stream.PassThrough(); - lists.exportListItemsToStream({ listId, stream, stringToAppend: '\n' }); - return response.ok({ - body: stream, - headers: { - 'Content-Disposition': `attachment; filename="${fileName}"`, - 'Content-Type': 'application/ndjson', - }, + const stream = new Stream.PassThrough(); + lists.exportListItemsToStream({ listId, stream, stringToAppend: '\n' }); + return response.ok({ + body: stream, + headers: { + 'Content-Disposition': `attachment; filename="${fileName}"`, + 'Content-Type': 'application/ndjson', + }, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_index/find_list_route.ts b/x-pack/plugins/lists/server/routes/list_index/find_list_route.ts index ab717a5f05ffb..9b492006e0605 100644 --- a/x-pack/plugins/lists/server/routes/list_index/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/find_list_route.ts @@ -15,72 +15,79 @@ import { findListRequestQuery, findListResponse } from '../../../common/api'; import { buildRouteValidation, buildSiemResponse, getListClient } from '../utils'; export const findListRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: `${LIST_URL}/_find`, - validate: { - query: buildRouteValidation(findListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation(findListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const lists = await getListClient(context); - const { - cursor, - filter: filterOrUndefined, - page: pageOrUndefined, - per_page: perPageOrUndefined, - sort_field: sortField, - sort_order: sortOrder, - } = request.query; + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = await getListClient(context); + const { + cursor, + filter: filterOrUndefined, + page: pageOrUndefined, + per_page: perPageOrUndefined, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; - const page = pageOrUndefined ?? 1; - const perPage = perPageOrUndefined ?? 20; - const filter = filterOrUndefined ?? ''; - const { - isValid, - errorMessage, - cursor: [currentIndexPosition, searchAfter], - } = decodeCursor({ - cursor, - page, - perPage, - sortField, - }); - if (!isValid) { - return siemResponse.error({ - body: errorMessage, - statusCode: 400, - }); - } else { - const exceptionList = await lists.findList({ - currentIndexPosition, - filter, + const page = pageOrUndefined ?? 1; + const perPage = perPageOrUndefined ?? 20; + const filter = filterOrUndefined ?? ''; + const { + isValid, + errorMessage, + cursor: [currentIndexPosition, searchAfter], + } = decodeCursor({ + cursor, page, perPage, - runtimeMappings: undefined, - searchAfter, sortField, - sortOrder, }); - const [validated, errors] = validate(exceptionList, findListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (!isValid) { + return siemResponse.error({ + body: errorMessage, + statusCode: 400, + }); } else { - return response.ok({ body: validated ?? {} }); + const exceptionList = await lists.findList({ + currentIndexPosition, + filter, + page, + perPage, + runtimeMappings: undefined, + searchAfter, + sortField, + sortOrder, + }); + const [validated, errors] = validate(exceptionList, findListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts index 3f1ffa8b95f5d..6e9fdb29e55b8 100644 --- a/x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts @@ -15,55 +15,60 @@ import { buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const readListIndexRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: LIST_INDEX, - validate: false, - }, - async (context, _, response) => { - const siemResponse = buildSiemResponse(response); + }) + .addVersion( + { + validate: false, + version: '2023-10-31', + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); - try { - const lists = await getListClient(context); - const listDataStreamExists = await lists.getListDataStreamExists(); - const listItemDataStreamExists = await lists.getListItemDataStreamExists(); + try { + const lists = await getListClient(context); + const listDataStreamExists = await lists.getListDataStreamExists(); + const listItemDataStreamExists = await lists.getListItemDataStreamExists(); - if (listDataStreamExists && listItemDataStreamExists) { - const [validated, errors] = validate( - { list_index: listDataStreamExists, list_item_index: listItemDataStreamExists }, - readListIndexResponse - ); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (listDataStreamExists && listItemDataStreamExists) { + const [validated, errors] = validate( + { list_index: listDataStreamExists, list_item_index: listItemDataStreamExists }, + readListIndexResponse + ); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else if (!listDataStreamExists && listItemDataStreamExists) { + return siemResponse.error({ + body: `data stream ${lists.getListName()} does not exist`, + statusCode: 404, + }); + } else if (!listItemDataStreamExists && listDataStreamExists) { + return siemResponse.error({ + body: `data stream ${lists.getListItemName()} does not exist`, + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + return siemResponse.error({ + body: `data stream ${lists.getListName()} and data stream ${lists.getListItemName()} does not exist`, + statusCode: 404, + }); } - } else if (!listDataStreamExists && listItemDataStreamExists) { - return siemResponse.error({ - body: `data stream ${lists.getListName()} does not exist`, - statusCode: 404, - }); - } else if (!listItemDataStreamExists && listDataStreamExists) { - return siemResponse.error({ - body: `data stream ${lists.getListItemName()} does not exist`, - statusCode: 404, - }); - } else { + } catch (err) { + const error = transformError(err); return siemResponse.error({ - body: `data stream ${lists.getListName()} and data stream ${lists.getListItemName()} does not exist`, - statusCode: 404, + body: error.message, + statusCode: error.statusCode, }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts index 9a06a9dc2c4e0..1f9041930a00d 100644 --- a/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts @@ -15,67 +15,74 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const createListItemRoute = (router: ListsPluginRouter): void => { - router.post( - { + router.versioned + .post({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_ITEM_URL, - validate: { - body: buildRouteValidation(createListItemRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation(createListItemRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { id, list_id: listId, value, meta } = request.body; - const lists = await getListClient(context); - const list = await lists.getList({ id: listId }); - if (list == null) { - return siemResponse.error({ - body: `list id: "${listId}" does not exist`, - statusCode: 404, - }); - } else { - if (id != null) { - const listItem = await lists.getListItem({ id }); - if (listItem != null) { + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value, meta } = request.body; + const lists = await getListClient(context); + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + if (id != null) { + const listItem = await lists.getListItem({ id }); + if (listItem != null) { + return siemResponse.error({ + body: `list item id: "${id}" already exists`, + statusCode: 409, + }); + } + } + const createdListItem = await lists.createListItem({ + deserializer: list.deserializer, + id, + listId, + meta, + serializer: list.serializer, + type: list.type, + value, + }); + if (createdListItem != null) { + const [validated, errors] = validate(createdListItem, createListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else { return siemResponse.error({ - body: `list item id: "${id}" already exists`, - statusCode: 409, + body: 'list item invalid', + statusCode: 400, }); } } - const createdListItem = await lists.createListItem({ - deserializer: list.deserializer, - id, - listId, - meta, - serializer: list.serializer, - type: list.type, - value, + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, }); - if (createdListItem != null) { - const [validated, errors] = validate(createdListItem, createListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); - } - } else { - return siemResponse.error({ - body: 'list item invalid', - statusCode: 400, - }); - } } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts index a334d7012b461..9b82eb3d5cdeb 100644 --- a/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts @@ -19,76 +19,83 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const deleteListItemRoute = (router: ListsPluginRouter): void => { - router.delete( - { + router.versioned + .delete({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_ITEM_URL, - validate: { - query: buildRouteValidation(deleteListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation(deleteListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { id, list_id: listId, value } = request.query; - const lists = await getListClient(context); - if (id != null) { - const deleted = await lists.deleteListItem({ id }); - if (deleted == null) { - return siemResponse.error({ - body: `list item with id: "${id}" not found`, - statusCode: 404, - }); - } else { - const [validated, errors] = validate(deleted, deleteListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); - } - } - } else if (listId != null && value != null) { - const list = await lists.getList({ id: listId }); - if (list == null) { - return siemResponse.error({ - body: `list_id: "${listId}" does not exist`, - statusCode: 404, - }); - } else { - const deleted = await lists.deleteListItemByValue({ - listId, - type: list.type, - value, - }); - if (deleted == null || deleted.length === 0) { + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value } = request.query; + const lists = await getListClient(context); + if (id != null) { + const deleted = await lists.deleteListItem({ id }); + if (deleted == null) { return siemResponse.error({ - body: `list_id: "${listId}" with ${value} was not found`, + body: `list item with id: "${id}" not found`, statusCode: 404, }); } else { - const [validated, errors] = validate(deleted, deleteListItemArrayResponse); + const [validated, errors] = validate(deleted, deleteListItemResponse); if (errors != null) { return siemResponse.error({ body: errors, statusCode: 500 }); } else { return response.ok({ body: validated ?? {} }); } } + } else if (listId != null && value != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list_id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const deleted = await lists.deleteListItemByValue({ + listId, + type: list.type, + value, + }); + if (deleted == null || deleted.length === 0) { + return siemResponse.error({ + body: `list_id: "${listId}" with ${value} was not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, deleteListItemArrayResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } else { + return siemResponse.error({ + body: 'Either "list_id" or "id" needs to be defined in the request', + statusCode: 400, + }); } - } else { + } catch (err) { + const error = transformError(err); return siemResponse.error({ - body: 'Either "list_id" or "id" needs to be defined in the request', - statusCode: 400, + body: error.message, + statusCode: error.statusCode, }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts index acd1bbd0f834f..0fd4300ea51dc 100644 --- a/x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts @@ -19,85 +19,92 @@ import { import { buildRouteValidation, buildSiemResponse, getListClient } from '../utils'; export const findListItemRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: `${LIST_ITEM_URL}/_find`, - validate: { - query: buildRouteValidation< - typeof findListItemRequestQuery, - FindListItemRequestQueryDecoded - >(findListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof findListItemRequestQuery, + FindListItemRequestQueryDecoded + >(findListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const lists = await getListClient(context); - const { - cursor, - filter: filterOrUndefined, - list_id: listId, - page: pageOrUndefined, - per_page: perPageOrUndefined, - sort_field: sortField, - sort_order: sortOrder, - } = request.query; + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = await getListClient(context); + const { + cursor, + filter: filterOrUndefined, + list_id: listId, + page: pageOrUndefined, + per_page: perPageOrUndefined, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; - const page = pageOrUndefined ?? 1; - const perPage = perPageOrUndefined ?? 20; - const filter = filterOrUndefined ?? ''; - const { - isValid, - errorMessage, - cursor: [currentIndexPosition, searchAfter = []], - } = decodeCursor({ - cursor, - page, - perPage, - sortField, - }); - - if (!isValid) { - return siemResponse.error({ - body: errorMessage, - statusCode: 400, - }); - } else { - const exceptionList = await lists.findListItem({ - currentIndexPosition, - filter, - listId, + const page = pageOrUndefined ?? 1; + const perPage = perPageOrUndefined ?? 20; + const filter = filterOrUndefined ?? ''; + const { + isValid, + errorMessage, + cursor: [currentIndexPosition, searchAfter = []], + } = decodeCursor({ + cursor, page, perPage, - runtimeMappings: undefined, - searchAfter, sortField, - sortOrder, }); - if (exceptionList == null) { + + if (!isValid) { return siemResponse.error({ - body: `list id: "${listId}" does not exist`, - statusCode: 404, + body: errorMessage, + statusCode: 400, }); } else { - const [validated, errors] = validate(exceptionList, findListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const exceptionList = await lists.findListItem({ + currentIndexPosition, + filter, + listId, + page, + perPage, + runtimeMappings: undefined, + searchAfter, + sortField, + sortOrder, + }); + if (exceptionList == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(exceptionList, findListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts index b71c949242546..e378843064190 100644 --- a/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts @@ -15,57 +15,64 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const patchListItemRoute = (router: ListsPluginRouter): void => { - router.patch( - { + router.versioned + .patch({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_ITEM_URL, - validate: { - body: buildRouteValidation(patchListItemRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation(patchListItemRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { value, id, meta, _version } = request.body; - const lists = await getListClient(context); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { value, id, meta, _version } = request.body; + const lists = await getListClient(context); - const dataStreamExists = await lists.getListItemDataStreamExists(); - // needs to be migrated to data stream if index exists - if (!dataStreamExists) { - const indexExists = await lists.getListItemIndexExists(); - if (indexExists) { - await lists.migrateListItemIndexToDataStream(); + const dataStreamExists = await lists.getListItemDataStreamExists(); + // needs to be migrated to data stream if index exists + if (!dataStreamExists) { + const indexExists = await lists.getListItemIndexExists(); + if (indexExists) { + await lists.migrateListItemIndexToDataStream(); + } } - } - const listItem = await lists.patchListItem({ - _version, - id, - meta, - value, - }); - if (listItem == null) { - return siemResponse.error({ - body: `list item id: "${id}" not found`, - statusCode: 404, + const listItem = await lists.patchListItem({ + _version, + id, + meta, + value, }); - } else { - const [validated, errors] = validate(listItem, patchListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(listItem, patchListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts index 796be1ae2983d..67c4793736483 100644 --- a/x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts @@ -19,76 +19,83 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const readListItemRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: LIST_ITEM_URL, - validate: { - query: buildRouteValidation(readListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation(readListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { id, list_id: listId, value } = request.query; - const lists = await getListClient(context); - if (id != null) { - const listItem = await lists.getListItem({ id }); - if (listItem == null) { - return siemResponse.error({ - body: `list item id: "${id}" does not exist`, - statusCode: 404, - }); - } else { - const [validated, errors] = validate(listItem, readListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); - } - } - } else if (listId != null && value != null) { - const list = await lists.getList({ id: listId }); - if (list == null) { - return siemResponse.error({ - body: `list id: "${listId}" does not exist`, - statusCode: 404, - }); - } else { - const listItem = await lists.getListItemByValue({ - listId, - type: list.type, - value, - }); - if (listItem.length === 0) { + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value } = request.query; + const lists = await getListClient(context); + if (id != null) { + const listItem = await lists.getListItem({ id }); + if (listItem == null) { return siemResponse.error({ - body: `list_id: "${listId}" item of ${value} does not exist`, + body: `list item id: "${id}" does not exist`, statusCode: 404, }); } else { - const [validated, errors] = validate(listItem, readListItemArrayResponse); + const [validated, errors] = validate(listItem, readListItemResponse); if (errors != null) { return siemResponse.error({ body: errors, statusCode: 500 }); } else { return response.ok({ body: validated ?? {} }); } } + } else if (listId != null && value != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const listItem = await lists.getListItemByValue({ + listId, + type: list.type, + value, + }); + if (listItem.length === 0) { + return siemResponse.error({ + body: `list_id: "${listId}" item of ${value} does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, readListItemArrayResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } else { + return siemResponse.error({ + body: 'Either "list_id" or "id" needs to be defined in the request', + statusCode: 400, + }); } - } else { + } catch (err) { + const error = transformError(err); return siemResponse.error({ - body: 'Either "list_id" or "id" needs to be defined in the request', - statusCode: 400, + body: error.message, + statusCode: error.statusCode, }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts index f2ea42c538f61..c627f9e9e95ea 100644 --- a/x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts @@ -15,57 +15,64 @@ import { buildRouteValidation, buildSiemResponse } from '../utils'; import { getListClient } from '..'; export const updateListItemRoute = (router: ListsPluginRouter): void => { - router.put( - { + router.versioned + .put({ + access: 'public', options: { tags: ['access:lists-all'], }, path: LIST_ITEM_URL, - validate: { - body: buildRouteValidation(updateListItemRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation(updateListItemRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { value, id, meta, _version } = request.body; - const lists = await getListClient(context); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { value, id, meta, _version } = request.body; + const lists = await getListClient(context); - const dataStreamExists = await lists.getListItemDataStreamExists(); - // needs to be migrated to data stream if index exists - if (!dataStreamExists) { - const indexExists = await lists.getListItemIndexExists(); - if (indexExists) { - await lists.migrateListItemIndexToDataStream(); + const dataStreamExists = await lists.getListItemDataStreamExists(); + // needs to be migrated to data stream if index exists + if (!dataStreamExists) { + const indexExists = await lists.getListItemIndexExists(); + if (indexExists) { + await lists.migrateListItemIndexToDataStream(); + } } - } - const listItem = await lists.updateListItem({ - _version, - id, - meta, - value, - }); - if (listItem == null) { - return siemResponse.error({ - body: `list item id: "${id}" not found`, - statusCode: 404, + const listItem = await lists.updateListItem({ + _version, + id, + meta, + value, }); - } else { - const [validated, errors] = validate(listItem, updateListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(listItem, updateListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts b/x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts index 8a6c404220bbb..94c171a2ec79c 100644 --- a/x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts +++ b/x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts @@ -13,38 +13,46 @@ import type { ListsPluginRouter } from '../../types'; import { buildSiemResponse, getListClient } from '../utils'; export const readPrivilegesRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: LIST_PRIVILEGES_URL, - validate: false, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const lists = await getListClient(context); - const clusterPrivilegesLists = await readPrivileges(esClient, lists.getListName()); - const clusterPrivilegesListItems = await readPrivileges(esClient, lists.getListItemName()); - const privileges = merge( - { - listItems: clusterPrivilegesListItems, - lists: clusterPrivilegesLists, - }, - { - is_authenticated: request.auth.isAuthenticated ?? false, - } - ); - return response.ok({ body: privileges }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); + }) + .addVersion( + { + validate: false, + version: '2023-10-31', + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + const lists = await getListClient(context); + const clusterPrivilegesLists = await readPrivileges(esClient, lists.getListName()); + const clusterPrivilegesListItems = await readPrivileges( + esClient, + lists.getListItemName() + ); + const privileges = merge( + { + listItems: clusterPrivilegesListItems, + lists: clusterPrivilegesLists, + }, + { + is_authenticated: request.auth.isAuthenticated ?? false, + } + ); + return response.ok({ body: privileges }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index 3db0aaf377893..b09d34f90c04a 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -24,52 +24,59 @@ import { } from './utils'; export const readEndpointListItemRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: ENDPOINT_LIST_ITEM_URL, - validate: { - query: buildRouteValidation< - typeof readEndpointListItemRequestQuery, - ReadEndpointListItemRequestQueryDecoded - >(readEndpointListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof readEndpointListItemRequestQuery, + ReadEndpointListItemRequestQueryDecoded + >(readEndpointListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { id, item_id: itemId } = request.query; - const exceptionLists = await getExceptionListClient(context); - if (id != null || itemId != null) { - const exceptionListItem = await exceptionLists.getEndpointListItem({ - id, - itemId, - }); - if (exceptionListItem == null) { - return siemResponse.error({ - body: getErrorMessageExceptionListItem({ id, itemId }), - statusCode: 404, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, item_id: itemId } = request.query; + const exceptionLists = await getExceptionListClient(context); + if (id != null || itemId != null) { + const exceptionListItem = await exceptionLists.getEndpointListItem({ + id, + itemId, }); - } else { - const [validated, errors] = validate(exceptionListItem, readEndpointListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (exceptionListItem == null) { + return siemResponse.error({ + body: getErrorMessageExceptionListItem({ id, itemId }), + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(exceptionListItem, readEndpointListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } else { + return siemResponse.error({ body: 'id or item_id required', statusCode: 400 }); } - } else { - return siemResponse.error({ body: 'id or item_id required', statusCode: 400 }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index 244587b4a06bb..7e74e928a717c 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -24,53 +24,63 @@ import { } from './utils'; export const readExceptionListItemRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: EXCEPTION_LIST_ITEM_URL, - validate: { - query: buildRouteValidation< - typeof readExceptionListItemRequestQuery, - ReadExceptionListItemRequestQueryDecoded - >(readExceptionListItemRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof readExceptionListItemRequestQuery, + ReadExceptionListItemRequestQueryDecoded + >(readExceptionListItemRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { id, item_id: itemId, namespace_type: namespaceType } = request.query; - const exceptionLists = await getExceptionListClient(context); - if (id != null || itemId != null) { - const exceptionListItem = await exceptionLists.getExceptionListItem({ - id, - itemId, - namespaceType, - }); - if (exceptionListItem == null) { - return siemResponse.error({ - body: getErrorMessageExceptionListItem({ id, itemId }), - statusCode: 404, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, item_id: itemId, namespace_type: namespaceType } = request.query; + const exceptionLists = await getExceptionListClient(context); + if (id != null || itemId != null) { + const exceptionListItem = await exceptionLists.getExceptionListItem({ + id, + itemId, + namespaceType, }); - } else { - const [validated, errors] = validate(exceptionListItem, readExceptionListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (exceptionListItem == null) { + return siemResponse.error({ + body: getErrorMessageExceptionListItem({ id, itemId }), + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate( + exceptionListItem, + readExceptionListItemResponse + ); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } else { + return siemResponse.error({ body: 'id or item_id required', statusCode: 400 }); } - } else { - return siemResponse.error({ body: 'id or item_id required', statusCode: 400 }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index b35dde0ae0b49..5da7395cf4875 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -24,53 +24,60 @@ import { } from './utils'; export const readExceptionListRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-read'], }, path: EXCEPTION_LIST_URL, - validate: { - query: buildRouteValidation< - typeof readExceptionListRequestQuery, - ReadExceptionListRequestQueryDecoded - >(readExceptionListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof readExceptionListRequestQuery, + ReadExceptionListRequestQueryDecoded + >(readExceptionListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { id, list_id: listId, namespace_type: namespaceType } = request.query; - const exceptionLists = await getExceptionListClient(context); - if (id != null || listId != null) { - const exceptionList = await exceptionLists.getExceptionList({ - id, - listId, - namespaceType, - }); - if (exceptionList == null) { - return siemResponse.error({ - body: getErrorMessageExceptionList({ id, listId }), - statusCode: 404, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, namespace_type: namespaceType } = request.query; + const exceptionLists = await getExceptionListClient(context); + if (id != null || listId != null) { + const exceptionList = await exceptionLists.getExceptionList({ + id, + listId, + namespaceType, }); - } else { - const [validated, errors] = validate(exceptionList, readExceptionListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + if (exceptionList == null) { + return siemResponse.error({ + body: getErrorMessageExceptionList({ id, listId }), + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(exceptionList, readExceptionListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } else { + return siemResponse.error({ body: 'id or list_id required', statusCode: 400 }); } - } else { - return siemResponse.error({ body: 'id or list_id required', statusCode: 400 }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts b/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts index b9c1814485b22..c927a1e947145 100644 --- a/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts @@ -24,57 +24,64 @@ import { } from './utils'; export const summaryExceptionListRoute = (router: ListsPluginRouter): void => { - router.get( - { + router.versioned + .get({ + access: 'public', options: { tags: ['access:lists-summary'], }, path: `${EXCEPTION_LIST_URL}/summary`, - validate: { - query: buildRouteValidation< - typeof summaryExceptionListRequestQuery, - SummaryExceptionListRequestQueryDecoded - >(summaryExceptionListRequestQuery), + }) + .addVersion( + { + validate: { + request: { + query: buildRouteValidation< + typeof summaryExceptionListRequestQuery, + SummaryExceptionListRequestQueryDecoded + >(summaryExceptionListRequestQuery), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { id, list_id: listId, namespace_type: namespaceType, filter } = request.query; - const exceptionLists = await getExceptionListClient(context); - if (id != null || listId != null) { - const exceptionListSummary = await exceptionLists.getExceptionListSummary({ - filter, - id, - listId, - namespaceType, - }); - if (exceptionListSummary == null) { - return siemResponse.error({ - body: getErrorMessageExceptionList({ id, listId }), - statusCode: 404, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, namespace_type: namespaceType, filter } = request.query; + const exceptionLists = await getExceptionListClient(context); + if (id != null || listId != null) { + const exceptionListSummary = await exceptionLists.getExceptionListSummary({ + filter, + id, + listId, + namespaceType, }); - } else { - const [validated, errors] = validate( - exceptionListSummary, - summaryExceptionListResponse - ); - if (errors != null) { - return response.ok({ body: exceptionListSummary }); + if (exceptionListSummary == null) { + return siemResponse.error({ + body: getErrorMessageExceptionList({ id, listId }), + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate( + exceptionListSummary, + summaryExceptionListResponse + ); + if (errors != null) { + return response.ok({ body: exceptionListSummary }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } else { + return siemResponse.error({ body: 'id or list_id required', statusCode: 400 }); } - } else { - return siemResponse.error({ body: 'id or list_id required', statusCode: 400 }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index f7af2aa3bff48..73d57941f75e5 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -21,76 +21,83 @@ import { buildRouteValidation, buildSiemResponse } from './utils'; import { getExceptionListClient } from '.'; export const updateEndpointListItemRoute = (router: ListsPluginRouter): void => { - router.put( - { + router.versioned + .put({ + access: 'public', options: { tags: ['access:lists-all'], }, path: ENDPOINT_LIST_ITEM_URL, - validate: { - body: buildRouteValidation< - typeof updateEndpointListItemRequest, - UpdateEndpointListItemRequestDecoded - >(updateEndpointListItemRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation< + typeof updateEndpointListItemRequest, + UpdateEndpointListItemRequestDecoded + >(updateEndpointListItemRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { - description, - id, - name, - os_types: osTypes, - meta, - type, - _version, - comments, - entries, - item_id: itemId, - tags, - } = request.body; - const exceptionLists = await getExceptionListClient(context); - const exceptionListItem = await exceptionLists.updateEndpointListItem({ - _version, - comments, - description, - entries, - id, - itemId, - meta, - name, - osTypes, - tags, - type, - }); - if (exceptionListItem == null) { - if (id != null) { - return siemResponse.error({ - body: `list item id: "${id}" not found`, - statusCode: 404, - }); + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { + description, + id, + name, + os_types: osTypes, + meta, + type, + _version, + comments, + entries, + item_id: itemId, + tags, + } = request.body; + const exceptionLists = await getExceptionListClient(context); + const exceptionListItem = await exceptionLists.updateEndpointListItem({ + _version, + comments, + description, + entries, + id, + itemId, + meta, + name, + osTypes, + tags, + type, + }); + if (exceptionListItem == null) { + if (id != null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `list item item_id: "${itemId}" not found`, + statusCode: 404, + }); + } } else { - return siemResponse.error({ - body: `list item item_id: "${itemId}" not found`, - statusCode: 404, - }); - } - } else { - const [validated, errors] = validate(exceptionListItem, updateEndpointListItemResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(exceptionListItem, updateEndpointListItemResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 55c1105b63c2d..da3b6849fe143 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -22,95 +22,102 @@ import { buildRouteValidation, buildSiemResponse } from './utils'; import { getExceptionListClient } from '.'; export const updateExceptionListItemRoute = (router: ListsPluginRouter): void => { - router.put( - { + router.versioned + .put({ + access: 'public', options: { tags: ['access:lists-all'], }, path: EXCEPTION_LIST_ITEM_URL, - validate: { - body: buildRouteValidation< - typeof updateExceptionListItemRequest, - UpdateExceptionListItemRequestDecoded - >(updateExceptionListItemRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation< + typeof updateExceptionListItemRequest, + UpdateExceptionListItemRequestDecoded + >(updateExceptionListItemRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - const validationErrors = updateExceptionListItemValidate(request.body); - if (validationErrors.length) { - return siemResponse.error({ body: validationErrors, statusCode: 400 }); - } + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + const validationErrors = updateExceptionListItemValidate(request.body); + if (validationErrors.length) { + return siemResponse.error({ body: validationErrors, statusCode: 400 }); + } - try { - const { - description, - id, - name, - meta, - type, - _version, - comments, - entries, - item_id: itemId, - namespace_type: namespaceType, - os_types: osTypes, - tags, - expire_time: expireTime, - } = request.body; - if (id == null && itemId == null) { - return siemResponse.error({ - body: 'either id or item_id need to be defined', - statusCode: 404, - }); - } else { - const exceptionLists = await getExceptionListClient(context); - const exceptionListItem = await exceptionLists.updateOverwriteExceptionListItem({ - _version, - comments, + try { + const { description, - entries, - expireTime, id, - itemId, - meta, name, - namespaceType, - osTypes, - tags, + meta, type, - }); - if (exceptionListItem == null) { - if (id != null) { - return siemResponse.error({ - body: `exception list item id: "${id}" does not exist`, - statusCode: 404, - }); - } else { - return siemResponse.error({ - body: `exception list item item_id: "${itemId}" does not exist`, - statusCode: 404, - }); - } + _version, + comments, + entries, + item_id: itemId, + namespace_type: namespaceType, + os_types: osTypes, + tags, + expire_time: expireTime, + } = request.body; + if (id == null && itemId == null) { + return siemResponse.error({ + body: 'either id or item_id need to be defined', + statusCode: 404, + }); } else { - const [validated, errors] = validate( - exceptionListItem, - updateExceptionListItemResponse - ); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const exceptionLists = await getExceptionListClient(context); + const exceptionListItem = await exceptionLists.updateOverwriteExceptionListItem({ + _version, + comments, + description, + entries, + expireTime, + id, + itemId, + meta, + name, + namespaceType, + osTypes, + tags, + type, + }); + if (exceptionListItem == null) { + if (id != null) { + return siemResponse.error({ + body: `exception list item id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `exception list item item_id: "${itemId}" does not exist`, + statusCode: 404, + }); + } } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate( + exceptionListItem, + updateExceptionListItemResponse + ); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 0d739d89f24d3..636f77d9f2eba 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -24,76 +24,83 @@ import { } from './utils'; export const updateExceptionListRoute = (router: ListsPluginRouter): void => { - router.put( - { + router.versioned + .put({ + access: 'public', options: { tags: ['access:lists-all'], }, path: EXCEPTION_LIST_URL, - validate: { - body: buildRouteValidation< - typeof updateExceptionListRequest, - UpdateExceptionListRequestDecoded - >(updateExceptionListRequest), + }) + .addVersion( + { + validate: { + request: { + body: buildRouteValidation< + typeof updateExceptionListRequest, + UpdateExceptionListRequestDecoded + >(updateExceptionListRequest), + }, + }, + version: '2023-10-31', }, - }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - try { - const { - _version, - tags, - name, - description, - id, - list_id: listId, - meta, - namespace_type: namespaceType, - os_types: osTypes, - type, - version, - } = request.body; - const exceptionLists = await getExceptionListClient(context); - if (id == null && listId == null) { - return siemResponse.error({ - body: 'either id or list_id need to be defined', - statusCode: 404, - }); - } else { - const list = await exceptionLists.updateExceptionList({ + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { _version, + tags, + name, description, id, - listId, + list_id: listId, meta, - name, - namespaceType, - osTypes, - tags, + namespace_type: namespaceType, + os_types: osTypes, type, version, - }); - if (list == null) { + } = request.body; + const exceptionLists = await getExceptionListClient(context); + if (id == null && listId == null) { return siemResponse.error({ - body: getErrorMessageExceptionList({ id, listId }), + body: 'either id or list_id need to be defined', statusCode: 404, }); } else { - const [validated, errors] = validate(list, updateExceptionListResponse); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const list = await exceptionLists.updateExceptionList({ + _version, + description, + id, + listId, + meta, + name, + namespaceType, + osTypes, + tags, + type, + version, + }); + if (list == null) { + return siemResponse.error({ + body: getErrorMessageExceptionList({ id, listId }), + statusCode: 404, + }); } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(list, updateExceptionListResponse); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); } - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); } - } - ); + ); }; diff --git a/x-pack/plugins/lists/server/routes/utils/index.ts b/x-pack/plugins/lists/server/routes/utils/index.ts index dd349bc5ec6e9..f035ae5dbfe9b 100644 --- a/x-pack/plugins/lists/server/routes/utils/index.ts +++ b/x-pack/plugins/lists/server/routes/utils/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export * from './remove_templates_if_exist'; export * from './get_error_message_exception_list_item'; export * from './get_error_message_exception_list'; export * from './get_list_client'; diff --git a/x-pack/plugins/lists/server/routes/utils/remove_templates_if_exist.ts b/x-pack/plugins/lists/server/routes/utils/remove_templates_if_exist.ts deleted file mode 100644 index ac761b5779048..0000000000000 --- a/x-pack/plugins/lists/server/routes/utils/remove_templates_if_exist.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 { ListClient } from '../../services/lists/list_client'; - -export const removeLegacyTemplatesIfExist = async (lists: ListClient): Promise => { - try { - const legacyTemplateExists = await lists.getLegacyListTemplateExists(); - const legacyTemplateListItemsExists = await lists.getLegacyListItemTemplateExists(); - - // Check if the old legacy lists and items template exists and remove it - if (legacyTemplateExists) { - await lists.deleteLegacyListTemplate(); - } - if (legacyTemplateListItemsExists) { - await lists.deleteLegacyListItemTemplate(); - } - } catch (err) { - // 410 error is for ES serverless, this API doesn't exist there any more, so _template request returns 410 error - if (err.statusCode !== 404 && err.statusCode !== 410) { - throw err; - } - } -}; diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts index d0b72313b7fd6..826e4a545d80d 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.ts @@ -331,6 +331,10 @@ export class ListClient { if (await this.getListPolicyExists()) { await this.deleteListPolicy(); } + + // as migration will be called eventually for every instance of Kibana, it's more efficient to delete + // legacy index template if it exists during migration + await this.deleteLegacyListTemplateIfExists(); }; /** @@ -353,6 +357,10 @@ export class ListClient { if (await this.getListItemPolicyExists()) { await this.deleteListItemPolicy(); } + + // as migration will be called eventually for every instance of Kibana, it's more efficient to delete + // legacy index template if it exists during migration + await this.deleteLegacyListItemTemplateIfExists(); }; /** @@ -588,6 +596,23 @@ export class ListClient { return deleteTemplate(esClient, listName); }; + /** + * Checks if legacy lists template exists and delete it + */ + public deleteLegacyListTemplateIfExists = async (): Promise => { + try { + const legacyTemplateExists = await this.getLegacyListTemplateExists(); + + if (legacyTemplateExists) { + await this.deleteLegacyListTemplate(); + } + } catch (err) { + if (err.statusCode !== 404) { + throw err; + } + } + }; + /** * Delete the list item boot strap index for ILM policies. * @returns The contents of the bootstrap response from Elasticsearch @@ -598,6 +623,23 @@ export class ListClient { return deleteTemplate(esClient, listItemName); }; + /** + * Checks if legacy list item template exists and delete it + */ + public deleteLegacyListItemTemplateIfExists = async (): Promise => { + try { + const legacyTemplateListItemsExists = await this.getLegacyListItemTemplateExists(); + + if (legacyTemplateListItemsExists) { + await this.deleteLegacyListItemTemplate(); + } + } catch (err) { + if (err.statusCode !== 404) { + throw err; + } + } + }; + /** * Given a list item id, this will delete the single list item * @returns The list item if found, otherwise null diff --git a/x-pack/plugins/discover_log_explorer/.storybook/__mocks__/package_icon.tsx b/x-pack/plugins/log_explorer/.storybook/__mocks__/package_icon.tsx similarity index 100% rename from x-pack/plugins/discover_log_explorer/.storybook/__mocks__/package_icon.tsx rename to x-pack/plugins/log_explorer/.storybook/__mocks__/package_icon.tsx diff --git a/x-pack/plugins/discover_log_explorer/.storybook/main.js b/x-pack/plugins/log_explorer/.storybook/main.js similarity index 100% rename from x-pack/plugins/discover_log_explorer/.storybook/main.js rename to x-pack/plugins/log_explorer/.storybook/main.js diff --git a/x-pack/plugins/discover_log_explorer/.storybook/preview.js b/x-pack/plugins/log_explorer/.storybook/preview.js similarity index 100% rename from x-pack/plugins/discover_log_explorer/.storybook/preview.js rename to x-pack/plugins/log_explorer/.storybook/preview.js diff --git a/x-pack/plugins/log_explorer/README.md b/x-pack/plugins/log_explorer/README.md new file mode 100755 index 0000000000000..bb4a266988a33 --- /dev/null +++ b/x-pack/plugins/log_explorer/README.md @@ -0,0 +1,8 @@ +# Log Explorer + +This plugin provides a `LogExplorer` component using the Discover customization framework, offering several affordances specifically designed for log consumption. + +The plugin enhances the capabilities of Discover in the following ways: + +- **Dataset selector**: this customization replaces the DataViews picker with a Logs dataset selector built ad-hoc to provide a better experience when navigating throught all the available datasets. + diff --git a/x-pack/plugins/discover_log_explorer/common/constants.ts b/x-pack/plugins/log_explorer/common/constants.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/constants.ts rename to x-pack/plugins/log_explorer/common/constants.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/errors.ts b/x-pack/plugins/log_explorer/common/datasets/errors.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/errors.ts rename to x-pack/plugins/log_explorer/common/datasets/errors.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/index.ts b/x-pack/plugins/log_explorer/common/datasets/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/index.ts rename to x-pack/plugins/log_explorer/common/datasets/index.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/models/dataset.ts b/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/models/dataset.ts rename to x-pack/plugins/log_explorer/common/datasets/models/dataset.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/models/integration.ts b/x-pack/plugins/log_explorer/common/datasets/models/integration.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/models/integration.ts rename to x-pack/plugins/log_explorer/common/datasets/models/integration.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/types.ts b/x-pack/plugins/log_explorer/common/datasets/types.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/types.ts rename to x-pack/plugins/log_explorer/common/datasets/types.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/v1/common.ts b/x-pack/plugins/log_explorer/common/datasets/v1/common.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/v1/common.ts rename to x-pack/plugins/log_explorer/common/datasets/v1/common.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/v1/find_datasets.ts b/x-pack/plugins/log_explorer/common/datasets/v1/find_datasets.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/v1/find_datasets.ts rename to x-pack/plugins/log_explorer/common/datasets/v1/find_datasets.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/v1/find_integrations.ts b/x-pack/plugins/log_explorer/common/datasets/v1/find_integrations.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/v1/find_integrations.ts rename to x-pack/plugins/log_explorer/common/datasets/v1/find_integrations.ts diff --git a/x-pack/plugins/discover_log_explorer/common/datasets/v1/index.ts b/x-pack/plugins/log_explorer/common/datasets/v1/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/datasets/v1/index.ts rename to x-pack/plugins/log_explorer/common/datasets/v1/index.ts diff --git a/x-pack/plugins/discover_log_explorer/common/hashed_cache.ts b/x-pack/plugins/log_explorer/common/hashed_cache.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/hashed_cache.ts rename to x-pack/plugins/log_explorer/common/hashed_cache.ts diff --git a/x-pack/plugins/discover_log_explorer/common/latest.ts b/x-pack/plugins/log_explorer/common/latest.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/common/latest.ts rename to x-pack/plugins/log_explorer/common/latest.ts diff --git a/x-pack/plugins/log_explorer/common/plugin_config.ts b/x-pack/plugins/log_explorer/common/plugin_config.ts new file mode 100644 index 0000000000000..b9a9274392d61 --- /dev/null +++ b/x-pack/plugins/log_explorer/common/plugin_config.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 @typescript-eslint/no-empty-interface +export interface LogExplorerConfig {} diff --git a/x-pack/plugins/log_explorer/common/runtime_types.ts b/x-pack/plugins/log_explorer/common/runtime_types.ts new file mode 100644 index 0000000000000..00043671edff4 --- /dev/null +++ b/x-pack/plugins/log_explorer/common/runtime_types.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 { createPlainError, decodeOrThrow, formatErrors, throwErrors } from '@kbn/io-ts-utils'; diff --git a/x-pack/plugins/discover_log_explorer/jest.config.js b/x-pack/plugins/log_explorer/jest.config.js similarity index 66% rename from x-pack/plugins/discover_log_explorer/jest.config.js rename to x-pack/plugins/log_explorer/jest.config.js index 988de065d4013..fea1fd32ee5d9 100644 --- a/x-pack/plugins/discover_log_explorer/jest.config.js +++ b/x-pack/plugins/log_explorer/jest.config.js @@ -8,10 +8,8 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', - roots: ['/x-pack/plugins/discover_log_explorer'], - coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/discover_log_explorer', + roots: ['/x-pack/plugins/log_explorer'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/log_explorer', coverageReporters: ['text', 'html'], - collectCoverageFrom: [ - '/x-pack/plugins/discover_log_explorer/{common,public}/**/*.{ts,tsx}', - ], + collectCoverageFrom: ['/x-pack/plugins/log_explorer/{common,public}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/log_explorer/kibana.jsonc b/x-pack/plugins/log_explorer/kibana.jsonc new file mode 100644 index 0000000000000..612bd34859b98 --- /dev/null +++ b/x-pack/plugins/log_explorer/kibana.jsonc @@ -0,0 +1,27 @@ +{ + "type": "plugin", + "id": "@kbn/log-explorer-plugin", + "owner": "@elastic/infra-monitoring-ui", + "description": "This plugin provides a LogExplorer component using the Discover customization framework, offering several affordances specifically designed for log consumption.", + "plugin": { + "id": "logExplorer", + "server": true, + "browser": true, + "configPath": [ + "xpack", + "logExplorer" + ], + "requiredPlugins": [ + "data", + "dataViews", + "discover", + "fleet", + "kibanaReact", + "kibanaUtils", + "controls", + "embeddable" + ], + "optionalPlugins": [], + "requiredBundles": [] + } +} diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/constants.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx similarity index 57% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/constants.tsx rename to x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx index a3ea4b85bbb00..5890b92f3bf1a 100644 --- a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/constants.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx @@ -16,59 +16,53 @@ export const DATA_VIEW_POPOVER_CONTENT_WIDTH = 300; export const contextMenuStyles = { maxHeight: 440 }; export const selectDatasetLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.selectDataset', + 'xpack.logExplorer.datasetSelector.selectDataset', { defaultMessage: 'Select dataset' } ); -export const integrationsLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.integrations', - { defaultMessage: 'Integrations' } -); +export const integrationsLabel = i18n.translate('xpack.logExplorer.datasetSelector.integrations', { + defaultMessage: 'Integrations', +}); export const uncategorizedLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.uncategorized', + 'xpack.logExplorer.datasetSelector.uncategorized', { defaultMessage: 'Uncategorized' } ); -export const sortOrdersLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.sortOrders', - { defaultMessage: 'Sort directions' } -); +export const sortOrdersLabel = i18n.translate('xpack.logExplorer.datasetSelector.sortOrders', { + defaultMessage: 'Sort directions', +}); -export const noDatasetsLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.noDatasets', - { defaultMessage: 'No data streams found' } -); +export const noDatasetsLabel = i18n.translate('xpack.logExplorer.datasetSelector.noDatasets', { + defaultMessage: 'No data streams found', +}); export const noDatasetsDescriptionLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.noDatasetsDescription', + 'xpack.logExplorer.datasetSelector.noDatasetsDescription', { defaultMessage: 'No datasets or search results found.', } ); export const noIntegrationsLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.noIntegrations', + 'xpack.logExplorer.datasetSelector.noIntegrations', { defaultMessage: 'No integrations found' } ); export const noIntegrationsDescriptionLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.noIntegrationsDescription', + 'xpack.logExplorer.datasetSelector.noIntegrationsDescription', { defaultMessage: 'No integrations or search results found.', } ); -export const errorLabel = i18n.translate('xpack.discoverLogExplorer.datasetSelector.error', { +export const errorLabel = i18n.translate('xpack.logExplorer.datasetSelector.error', { defaultMessage: 'error', }); -export const noDataRetryLabel = i18n.translate( - 'xpack.discoverLogExplorer.datasetSelector.noDataRetry', - { - defaultMessage: 'Retry', - } -); +export const noDataRetryLabel = i18n.translate('xpack.logExplorer.datasetSelector.noDataRetry', { + defaultMessage: 'Retry', +}); export const sortOptions = [ { diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx similarity index 99% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx rename to x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx index d96cdc07bd9bc..c1549bc899ab4 100644 --- a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx @@ -22,7 +22,7 @@ import { const meta: Meta = { component: DatasetSelector, - title: 'discover_log_explorer/DatasetSelector', + title: 'log_explorer/DatasetSelector', decorators: [(wrappedStory) => {wrappedStory()}], argTypes: { datasetsError: { diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.tsx rename to x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/index.ts b/x-pack/plugins/log_explorer/public/components/dataset_selector/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/index.ts rename to x-pack/plugins/log_explorer/public/components/dataset_selector/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/defaults.ts b/x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/defaults.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/defaults.ts rename to x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/defaults.ts diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/index.ts b/x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/index.ts rename to x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/state_machine.ts b/x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/state_machine.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/state_machine.ts rename to x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/state_machine.ts diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/types.ts b/x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/types.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/types.ts rename to x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/types.ts diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/use_dataset_selector.ts b/x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/use_dataset_selector.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/state_machine/use_dataset_selector.ts rename to x-pack/plugins/log_explorer/public/components/dataset_selector/state_machine/use_dataset_selector.ts diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx similarity index 97% rename from x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx rename to x-pack/plugins/log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx index 134bd7616ac8a..8664a428c9c3f 100644 --- a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx @@ -52,7 +52,7 @@ export const DatasetsList = ({ titleSize="s" body={ { + const logExplorerCustomizations = [createLogExplorerProfileCustomizations({ core, data })]; + + const overrideServices = { + data: createDataServiceProxy(data), + }; + + return ({ scopedHistory }: LogExplorerProps) => { + return ( + + ); + }; +}; + +/** + * Create proxy for the data service, in which session service enablement calls + * are no-ops. + */ +const createDataServiceProxy = (data: DataPublicPluginStart) => { + return createPropertyGetProxy(data, { + search: (searchService: ISearchStart) => + createPropertyGetProxy(searchService, { + session: (sessionService: ISessionService) => + createPropertyGetProxy(sessionService, { + enableStorage: () => () => {}, + }), + }), + }); +}; diff --git a/x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_filters.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx similarity index 91% rename from x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_filters.tsx rename to x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx index a669ebea33942..1f27f4a12b05f 100644 --- a/x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_filters.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx @@ -45,16 +45,6 @@ const ControlGroupContainer = euiStyled.div` .controlGroup { min-height: unset; } - - .euiFormLabel { - padding-top: 0; - padding-bottom: 0; - line-height: 32px !important; - } - - .euiFormControlLayout { - height: 32px; - } `; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_selector.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_selector.tsx rename to x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx diff --git a/x-pack/plugins/discover_log_explorer/public/customizations/log_explorer_profile.tsx b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx similarity index 97% rename from x-pack/plugins/discover_log_explorer/public/customizations/log_explorer_profile.tsx rename to x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx index d9b7a15269cb5..7a468d064de08 100644 --- a/x-pack/plugins/discover_log_explorer/public/customizations/log_explorer_profile.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx @@ -14,7 +14,7 @@ import { dynamic } from '../utils/dynamic'; const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector')); const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters')); -interface CreateLogExplorerProfileCustomizationsDeps { +export interface CreateLogExplorerProfileCustomizationsDeps { core: CoreStart; data: DataPublicPluginStart; } diff --git a/x-pack/plugins/discover_log_explorer/public/hooks/use_control_panels.tsx b/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/hooks/use_control_panels.tsx rename to x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx diff --git a/x-pack/plugins/discover_log_explorer/public/hooks/use_dataset_selection.ts b/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/hooks/use_dataset_selection.ts rename to x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts diff --git a/x-pack/plugins/discover_log_explorer/public/hooks/use_datasets.ts b/x-pack/plugins/log_explorer/public/hooks/use_datasets.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/hooks/use_datasets.ts rename to x-pack/plugins/log_explorer/public/hooks/use_datasets.ts diff --git a/x-pack/plugins/discover_log_explorer/public/hooks/use_integrations.ts b/x-pack/plugins/log_explorer/public/hooks/use_integrations.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/hooks/use_integrations.ts rename to x-pack/plugins/log_explorer/public/hooks/use_integrations.ts diff --git a/x-pack/plugins/discover_log_explorer/public/hooks/use_intersection_ref.ts b/x-pack/plugins/log_explorer/public/hooks/use_intersection_ref.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/hooks/use_intersection_ref.ts rename to x-pack/plugins/log_explorer/public/hooks/use_intersection_ref.ts diff --git a/x-pack/plugins/log_explorer/public/index.ts b/x-pack/plugins/log_explorer/public/index.ts new file mode 100644 index 0000000000000..c145f6fd88864 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PluginInitializerContext } from '@kbn/core/public'; +import type { LogExplorerConfig } from '../common/plugin_config'; +import { LogExplorerPlugin } from './plugin'; +export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types'; + +export function plugin(context: PluginInitializerContext) { + return new LogExplorerPlugin(context); +} diff --git a/x-pack/plugins/log_explorer/public/plugin.ts b/x-pack/plugins/log_explorer/public/plugin.ts new file mode 100644 index 0000000000000..5807e8260f326 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/plugin.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; +import { createLogExplorer } from './components/log_explorer'; +import { + LogExplorerPluginSetup, + LogExplorerPluginStart, + LogExplorerSetupDeps, + LogExplorerStartDeps, +} from './types'; + +export class LogExplorerPlugin implements Plugin { + constructor(context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: LogExplorerSetupDeps) {} + + public start(core: CoreStart, plugins: LogExplorerStartDeps) { + const { data, discover } = plugins; + + const LogExplorer = createLogExplorer({ + core, + data, + discover, + }); + + return { + LogExplorer, + }; + } +} diff --git a/x-pack/plugins/discover_log_explorer/public/services/datasets/datasets_client.mock.ts b/x-pack/plugins/log_explorer/public/services/datasets/datasets_client.mock.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/services/datasets/datasets_client.mock.ts rename to x-pack/plugins/log_explorer/public/services/datasets/datasets_client.mock.ts diff --git a/x-pack/plugins/discover_log_explorer/public/services/datasets/datasets_client.ts b/x-pack/plugins/log_explorer/public/services/datasets/datasets_client.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/services/datasets/datasets_client.ts rename to x-pack/plugins/log_explorer/public/services/datasets/datasets_client.ts diff --git a/x-pack/plugins/discover_log_explorer/public/services/datasets/datasets_service.mock.ts b/x-pack/plugins/log_explorer/public/services/datasets/datasets_service.mock.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/services/datasets/datasets_service.mock.ts rename to x-pack/plugins/log_explorer/public/services/datasets/datasets_service.mock.ts diff --git a/x-pack/plugins/discover_log_explorer/public/services/datasets/datasets_service.ts b/x-pack/plugins/log_explorer/public/services/datasets/datasets_service.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/services/datasets/datasets_service.ts rename to x-pack/plugins/log_explorer/public/services/datasets/datasets_service.ts diff --git a/x-pack/plugins/discover_log_explorer/public/services/datasets/index.ts b/x-pack/plugins/log_explorer/public/services/datasets/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/services/datasets/index.ts rename to x-pack/plugins/log_explorer/public/services/datasets/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/services/datasets/types.ts b/x-pack/plugins/log_explorer/public/services/datasets/types.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/services/datasets/types.ts rename to x-pack/plugins/log_explorer/public/services/datasets/types.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/datasets/index.ts b/x-pack/plugins/log_explorer/public/state_machines/datasets/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/datasets/index.ts rename to x-pack/plugins/log_explorer/public/state_machines/datasets/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/datasets/src/defaults.ts b/x-pack/plugins/log_explorer/public/state_machines/datasets/src/defaults.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/datasets/src/defaults.ts rename to x-pack/plugins/log_explorer/public/state_machines/datasets/src/defaults.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/datasets/src/index.ts b/x-pack/plugins/log_explorer/public/state_machines/datasets/src/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/datasets/src/index.ts rename to x-pack/plugins/log_explorer/public/state_machines/datasets/src/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/datasets/src/state_machine.ts b/x-pack/plugins/log_explorer/public/state_machines/datasets/src/state_machine.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/datasets/src/state_machine.ts rename to x-pack/plugins/log_explorer/public/state_machines/datasets/src/state_machine.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/datasets/src/types.ts b/x-pack/plugins/log_explorer/public/state_machines/datasets/src/types.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/datasets/src/types.ts rename to x-pack/plugins/log_explorer/public/state_machines/datasets/src/types.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/integrations/index.ts b/x-pack/plugins/log_explorer/public/state_machines/integrations/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/integrations/index.ts rename to x-pack/plugins/log_explorer/public/state_machines/integrations/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/integrations/src/defaults.ts b/x-pack/plugins/log_explorer/public/state_machines/integrations/src/defaults.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/integrations/src/defaults.ts rename to x-pack/plugins/log_explorer/public/state_machines/integrations/src/defaults.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/integrations/src/index.ts b/x-pack/plugins/log_explorer/public/state_machines/integrations/src/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/integrations/src/index.ts rename to x-pack/plugins/log_explorer/public/state_machines/integrations/src/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/integrations/src/state_machine.ts b/x-pack/plugins/log_explorer/public/state_machines/integrations/src/state_machine.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/integrations/src/state_machine.ts rename to x-pack/plugins/log_explorer/public/state_machines/integrations/src/state_machine.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/integrations/src/types.ts b/x-pack/plugins/log_explorer/public/state_machines/integrations/src/types.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/integrations/src/types.ts rename to x-pack/plugins/log_explorer/public/state_machines/integrations/src/types.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/index.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/index.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/index.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/index.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/notifications.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/notifications.ts similarity index 57% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/notifications.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/notifications.ts index e4dc78b056814..1c2cd471b9f6e 100644 --- a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/notifications.ts +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/notifications.ts @@ -11,23 +11,21 @@ import { i18n } from '@kbn/i18n'; export const createDatasetSelectionRestoreFailedNotifier = (toasts: IToasts) => () => toasts.addWarning({ title: i18n.translate( - 'xpack.discoverLogExplorer.datasetSelection.restoreDatasetSelectionFailedToastTitle', + 'xpack.logExplorer.datasetSelection.restoreDatasetSelectionFailedToastTitle', { defaultMessage: "We couldn't restore your datasets selection." } ), text: i18n.translate( - 'xpack.discoverLogExplorer.datasetSelection.restoreDatasetSelectionFailedToastMessage', + 'xpack.logExplorer.datasetSelection.restoreDatasetSelectionFailedToastMessage', { defaultMessage: 'We switched to "All log datasets" as the default selection.' } ), }); export const createCreateDataViewFailedNotifier = (toasts: IToasts) => () => toasts.addWarning({ - title: i18n.translate( - 'xpack.discoverLogExplorer.datasetSelection.createDataViewFailedToastTitle', - { defaultMessage: "We couldn't create a data view for your selection." } - ), - text: i18n.translate( - 'xpack.discoverLogExplorer.datasetSelection.createDataViewFailedToastMessage', - { defaultMessage: 'We switched to "All log datasets" as the default selection.' } - ), + title: i18n.translate('xpack.logExplorer.datasetSelection.createDataViewFailedToastTitle', { + defaultMessage: "We couldn't create a data view for your selection.", + }), + text: i18n.translate('xpack.logExplorer.datasetSelection.createDataViewFailedToastMessage', { + defaultMessage: 'We switched to "All log datasets" as the default selection.', + }), }); diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/types.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/types.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/types.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/types.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts diff --git a/x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/utils.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/utils.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/state_machines/log_explorer_profile/src/utils.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/utils.ts diff --git a/x-pack/plugins/log_explorer/public/types.ts b/x-pack/plugins/log_explorer/public/types.ts new file mode 100644 index 0000000000000..d0b488950fee4 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DiscoverStart } from '@kbn/discover-plugin/public'; +import type { ComponentType } from 'react'; +import type { LogExplorerProps } from './components/log_explorer'; + +export type LogExplorerPluginSetup = void; +export interface LogExplorerPluginStart { + LogExplorer: ComponentType; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface LogExplorerSetupDeps {} + +export interface LogExplorerStartDeps { + data: DataPublicPluginStart; + discover: DiscoverStart; +} diff --git a/x-pack/plugins/discover_log_explorer/public/utils/comparator_by_field.ts b/x-pack/plugins/log_explorer/public/utils/comparator_by_field.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/comparator_by_field.ts rename to x-pack/plugins/log_explorer/public/utils/comparator_by_field.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/all_dataset_selection.ts b/x-pack/plugins/log_explorer/public/utils/dataset_selection/all_dataset_selection.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/all_dataset_selection.ts rename to x-pack/plugins/log_explorer/public/utils/dataset_selection/all_dataset_selection.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/encoding.test.ts b/x-pack/plugins/log_explorer/public/utils/dataset_selection/encoding.test.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/encoding.test.ts rename to x-pack/plugins/log_explorer/public/utils/dataset_selection/encoding.test.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/encoding.ts b/x-pack/plugins/log_explorer/public/utils/dataset_selection/encoding.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/encoding.ts rename to x-pack/plugins/log_explorer/public/utils/dataset_selection/encoding.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/errors.ts b/x-pack/plugins/log_explorer/public/utils/dataset_selection/errors.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/errors.ts rename to x-pack/plugins/log_explorer/public/utils/dataset_selection/errors.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/hydrate_dataset_selection.ts.ts b/x-pack/plugins/log_explorer/public/utils/dataset_selection/hydrate_dataset_selection.ts.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/hydrate_dataset_selection.ts.ts rename to x-pack/plugins/log_explorer/public/utils/dataset_selection/hydrate_dataset_selection.ts.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/index.ts b/x-pack/plugins/log_explorer/public/utils/dataset_selection/index.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/index.ts rename to x-pack/plugins/log_explorer/public/utils/dataset_selection/index.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/single_dataset_selection.ts b/x-pack/plugins/log_explorer/public/utils/dataset_selection/single_dataset_selection.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/single_dataset_selection.ts rename to x-pack/plugins/log_explorer/public/utils/dataset_selection/single_dataset_selection.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/types.ts b/x-pack/plugins/log_explorer/public/utils/dataset_selection/types.ts similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dataset_selection/types.ts rename to x-pack/plugins/log_explorer/public/utils/dataset_selection/types.ts diff --git a/x-pack/plugins/discover_log_explorer/public/utils/dynamic.tsx b/x-pack/plugins/log_explorer/public/utils/dynamic.tsx similarity index 100% rename from x-pack/plugins/discover_log_explorer/public/utils/dynamic.tsx rename to x-pack/plugins/log_explorer/public/utils/dynamic.tsx diff --git a/x-pack/plugins/log_explorer/public/utils/proxies.ts b/x-pack/plugins/log_explorer/public/utils/proxies.ts new file mode 100644 index 0000000000000..5599f061f4d67 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/utils/proxies.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. + */ + +/** + * Creates a Proxy in which certain property accesses are redirected to + * replacement factories. + * + * @param target the object to proxy + * @param replacements a map of keys to replacement factories + * @returns a proxy of the object + */ +export const createPropertyGetProxy = ( + target: Target, + replacements: { + [key in Key]: (value: Target[Key]) => Target[Key]; + } +) => + new Proxy(target, { + get(accessedTarget, accessedKey, ...rest) { + const value = Reflect.get(accessedTarget, accessedKey, ...rest); + if (hasKey(replacements, accessedKey)) { + return replacements[accessedKey](value); + } else { + return value; + } + }, + }); + +const hasKey = ( + obj: T, + key: string | number | symbol +): key is K => obj.hasOwnProperty(key); diff --git a/x-pack/plugins/log_explorer/server/index.ts b/x-pack/plugins/log_explorer/server/index.ts new file mode 100644 index 0000000000000..634e4cfe02566 --- /dev/null +++ b/x-pack/plugins/log_explorer/server/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. + */ + +import { LogExplorerServerPlugin } from './plugin'; + +export const plugin = () => new LogExplorerServerPlugin(); diff --git a/x-pack/plugins/discover_log_explorer/server/plugin.ts b/x-pack/plugins/log_explorer/server/plugin.ts similarity index 83% rename from x-pack/plugins/discover_log_explorer/server/plugin.ts rename to x-pack/plugins/log_explorer/server/plugin.ts index a1e971fc2b502..140d32a564ca4 100644 --- a/x-pack/plugins/discover_log_explorer/server/plugin.ts +++ b/x-pack/plugins/log_explorer/server/plugin.ts @@ -7,7 +7,7 @@ import { Plugin } from '@kbn/core/server'; -export class DiscoverLogExplorerServerPlugin implements Plugin { +export class LogExplorerServerPlugin implements Plugin { setup() {} start() {} diff --git a/x-pack/plugins/discover_log_explorer/tsconfig.json b/x-pack/plugins/log_explorer/tsconfig.json similarity index 94% rename from x-pack/plugins/discover_log_explorer/tsconfig.json rename to x-pack/plugins/log_explorer/tsconfig.json index 61ea70ece2d38..756f4bd6b156a 100644 --- a/x-pack/plugins/discover_log_explorer/tsconfig.json +++ b/x-pack/plugins/log_explorer/tsconfig.json @@ -19,7 +19,7 @@ "@kbn/kibana-react-plugin", "@kbn/data-plugin", "@kbn/unified-field-list", - "@kbn/config-schema", + "@kbn/core-application-browser", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 5c71696cb280b..51b61f2dbc649 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -191,8 +191,13 @@ export class MbMap extends Component { } }); mbMap.on('load', () => { - emptyImage = new Image(); + // Map instance automatically resizes when container size changes. + // However, issues may arise if container resizes before map finishes loading. + // This is occuring when by-value maps are used in dashboard. + // To prevent issues, resize container after load + mbMap.resize(); + emptyImage = new Image(); emptyImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; emptyImage.crossOrigin = 'anonymous'; diff --git a/x-pack/plugins/ml/public/application/access_denied/access_denied.tsx b/x-pack/plugins/ml/public/application/access_denied/access_denied.tsx index 73eacf41f06ea..28c00e2794269 100644 --- a/x-pack/plugins/ml/public/application/access_denied/access_denied.tsx +++ b/x-pack/plugins/ml/public/application/access_denied/access_denied.tsx @@ -9,13 +9,7 @@ import React, { type FC } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiPageContent_Deprecated as EuiPageContent, - EuiSpacer, -} from '@elastic/eui'; +import { EuiPageTemplate } from '@elastic/eui'; import { createPermissionFailureMessage } from '../capabilities/check_capabilities'; import { MlCapabilitiesKey } from '../../../common/types/capabilities'; import { HelpMenu } from '../components/help_menu'; @@ -35,39 +29,34 @@ export const AccessDeniedCallout: FC = ({ missingCapab return ( <> - - - - - - - - } - body={ -
- - {errorMessages ? ( -
    - {errorMessages.map((v) => ( -
  • {v}
  • - ))} -
- ) : null} -
- } + + -
-
-
+ + } + body={ +
+ + {errorMessages ? ( +
    + {errorMessages.map((v) => ( +
  • {v}
  • + ))} +
+ ) : null} +
+ } + /> ); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx index f0214718814f8..a197dfa9cf6e8 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx @@ -72,6 +72,7 @@ interface CustomUrlEditorProps { dataViewListItems: DataViewListItem[]; showCustomTimeRangeSelector: boolean; job: Job | DataFrameAnalyticsConfig; + isPartialDFAJob?: boolean; } /* @@ -85,6 +86,7 @@ export const CustomUrlEditor: FC = ({ dataViewListItems, showCustomTimeRangeSelector, job, + isPartialDFAJob, }) => { const [queryEntityFieldNames, setQueryEntityFieldNames] = useState([]); const [hasTimefield, setHasTimefield] = useState(false); @@ -111,7 +113,12 @@ export const CustomUrlEditor: FC = ({ if (dataViewToUse && dataViewToUse.timeFieldName) { setHasTimefield(true); } - const dropDownOptions = await getDropDownOptions(isFirst.current, job, dataViewToUse); + const dropDownOptions = await getDropDownOptions( + isFirst.current, + job, + dataViewToUse, + isPartialDFAJob + ); setQueryEntityFieldNames(dropDownOptions); if (isFirst.current) { @@ -122,7 +129,7 @@ export const CustomUrlEditor: FC = ({ if (job !== undefined) { getQueryEntityDropdownOptions(); } - }, [dataViews, job, customUrl?.kibanaSettings?.discoverIndexPatternId]); + }, [dataViews, job, customUrl?.kibanaSettings?.discoverIndexPatternId, isPartialDFAJob]); useEffect(() => { if (addIntervalTimerange === false) { diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/get_dropdown_options.ts b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/get_dropdown_options.ts index 2b93c58dbbcd0..3caab1b371543 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/get_dropdown_options.ts +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/get_dropdown_options.ts @@ -16,11 +16,15 @@ import { getQueryEntityFieldNames, getSupportedFieldNames } from './utils'; export function getDropDownOptions( isFirstRender: boolean, job: Job | DataFrameAnalyticsConfig, - dataView?: DataView + dataView?: DataView, + isPartialDFAJob?: boolean ) { if (isAnomalyDetectionJob(job) && isFirstRender) { return getQueryEntityFieldNames(job); - } else if ((isDataFrameAnalyticsConfigs(job) || !isFirstRender) && dataView !== undefined) { + } else if ( + (isDataFrameAnalyticsConfigs(job) || isPartialDFAJob || !isFirstRender) && + dataView !== undefined + ) { return getSupportedFieldNames(job, dataView); } return []; diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx index 80be4f681f28f..3ee39cb611764 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx @@ -51,6 +51,7 @@ export interface CustomUrlListProps { customUrls: MlUrlConfig[]; onChange: (customUrls: MlUrlConfig[]) => void; dataViewListItems?: DataViewListItem[]; + isPartialDFAJob?: boolean; } /* @@ -62,6 +63,7 @@ export const CustomUrlList: FC = ({ customUrls, onChange: setCustomUrls, dataViewListItems, + isPartialDFAJob, }) => { const { services: { @@ -124,16 +126,21 @@ export const CustomUrlList: FC = ({ if ( index < customUrls.length && - isDataFrameAnalyticsConfigs(job) && + (isDataFrameAnalyticsConfigs(job) || isPartialDFAJob) && customUrl.time_range !== undefined && customUrl.time_range !== TIME_RANGE_TYPE.AUTO ) { + // Ensure cast as dfaJob if it's just a partial from the wizard + const dfaJob = job as DataFrameAnalyticsConfig; let dataViewId; - // DFA job url - need the timefield to test the URL. Get it from the job config. + // DFA job url - need the timefield to test the URL. Get it from the job config. Use source index when partial job since dest index does not exist yet. if (customUrl.url_value.includes('dashboards')) { + const sourceIndex = Array.isArray(dfaJob.source.index) + ? dfaJob.source.index.join() + : dfaJob.source.index; // need to get the dataview from the dashboard to get timefield - const indexName = job.dest.index; - const backupIndexName = job.source.index[0]; + const indexName = isPartialDFAJob ? sourceIndex : dfaJob.dest.index; + const backupIndexName = sourceIndex; dataViewId = dataViewListItems?.find((item) => item.title === indexName)?.id; if (!dataViewId) { dataViewId = dataViewListItems?.find((item) => item.title === backupIndexName)?.id; @@ -151,7 +158,7 @@ export const CustomUrlList: FC = ({ if (index < customUrls.length) { try { - const testUrl = await getTestUrl(job, customUrl, timefieldName); + const testUrl = await getTestUrl(job, customUrl, timefieldName, undefined, isPartialDFAJob); openCustomUrlWindow(testUrl, customUrl, http.basePath.get()); } catch (error) { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts index 158c334ae6f5d..338ec69ee8d4b 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts @@ -76,7 +76,8 @@ export interface CustomUrlSettings { export function getNewCustomUrlDefaults( job: Job | DataFrameAnalyticsConfig, dashboards: DashboardItems, - dataViews: DataViewListItem[] + dataViews: DataViewListItem[], + isPartialDFAJob?: boolean ): CustomUrlSettings { // Returns the settings object in the format used by the custom URL editor // for a new custom URL. @@ -112,11 +113,21 @@ export function getNewCustomUrlDefaults( indicesName = job.datafeed_config.indices.join(); query = job.datafeed_config?.query ?? {}; jobId = job.job_id; - } else if (isDataFrameAnalyticsConfigs(job) && dataViews !== undefined && dataViews.length > 0) { - indicesName = job.dest.index; - backupIndicesName = job.source.index[0]; - query = job.source?.query ?? {}; - jobId = job.id; + } else if ( + (isDataFrameAnalyticsConfigs(job) || isPartialDFAJob) && + dataViews !== undefined && + dataViews.length > 0 + ) { + // Ensure cast as dfaJob if it's just a partial from the wizard + const dfaJob = job as DataFrameAnalyticsConfig; + const sourceIndex = Array.isArray(dfaJob.source.index) + ? dfaJob.source.index.join() + : dfaJob.source.index; + + indicesName = isPartialDFAJob ? sourceIndex : dfaJob.dest.index; + backupIndicesName = sourceIndex; + query = dfaJob.source?.query ?? {}; + jobId = dfaJob.id; } const defaultDataViewId = dataViews.find((dv) => dv.title === indicesName)?.id; @@ -246,7 +257,8 @@ function getUrlRangeFromSettings(settings: CustomUrlSettings) { async function buildDashboardUrlFromSettings( dashboardService: DashboardStart, - settings: CustomUrlSettings + settings: CustomUrlSettings, + isPartialDFAJob?: boolean ): Promise { // Get the complete list of attributes for the selected dashboard (query, filters). const { dashboardId, queryFieldNames } = settings.kibanaSettings ?? {}; @@ -355,7 +367,6 @@ function buildDiscoverUrlFromSettings(settings: CustomUrlSettings) { index: discoverIndexPatternId, filters, }; - // If partitioning field entities have been configured add tokens // to the URL to use in the Discover page search. @@ -509,21 +520,26 @@ async function getDataFrameAnalyticsTestUrl( job: DataFrameAnalyticsConfig, customUrl: MlKibanaUrlConfig, timeFieldName: string | null, - currentTimeFilter?: EsQueryTimeRange + currentTimeFilter?: EsQueryTimeRange, + isPartialDFAJob?: boolean ): Promise { // By default, return configured url_value. Look to substitute any dollar-delimited // tokens with values from a sample doc in the destination index + const sourceIndex = Array.isArray(job.source.index) ? job.source.index.join() : job.source.index; let testUrl = customUrl.url_value; let record; let resp; try { - resp = await ml.esSearch({ - index: job.dest.index, + const body = { + // Use source index for partial job as there is no dest index yet + index: isPartialDFAJob ? sourceIndex : job.dest.index, body: { size: 1, }, - }); + }; + + resp = await ml.esSearch(body); if (resp && resp.hits.total.value > 0) { record = resp.hits.hits[0]._source; @@ -577,10 +593,17 @@ export function getTestUrl( job: Job | DataFrameAnalyticsConfig, customUrl: MlUrlConfig, timeFieldName: string | null, - currentTimeFilter?: EsQueryTimeRange + currentTimeFilter?: EsQueryTimeRange, + isPartialDFAJob?: boolean ) { - if (isDataFrameAnalyticsConfigs(job)) { - return getDataFrameAnalyticsTestUrl(job, customUrl, timeFieldName, currentTimeFilter); + if (isDataFrameAnalyticsConfigs(job) || isPartialDFAJob) { + return getDataFrameAnalyticsTestUrl( + job as DataFrameAnalyticsConfig, + customUrl, + timeFieldName, + currentTimeFilter, + isPartialDFAJob + ); } return getAnomalyDetectionJobTestUrl(job, customUrl); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx index 812a177b8bcfb..b020a87b36c1b 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx @@ -54,6 +54,7 @@ interface CustomUrlsProps extends CustomUrlsWrapperProps { kibana: MlKibanaReactContextValue; currentTimeFilter?: EsQueryTimeRange; dashboardService: DashboardService; + isPartialDFAJob?: boolean; } class CustomUrlsUI extends Component { @@ -122,7 +123,12 @@ class CustomUrlsUI extends Component { return { editorOpen: true, - editorSettings: getNewCustomUrlDefaults(this.props.job, dashboards, dataViewListItems), + editorSettings: getNewCustomUrlDefaults( + this.props.job, + dashboards, + dataViewListItems, + this.props.isPartialDFAJob + ), }; }); }; @@ -167,7 +173,6 @@ class CustomUrlsUI extends Component { } = this.props.kibana.services; const dataViewId = this.state?.editorSettings?.kibanaSettings?.discoverIndexPatternId; const job = this.props.job; - dataViews .get(dataViewId ?? '') .catch((error) => { @@ -179,7 +184,13 @@ class CustomUrlsUI extends Component { const timefieldName = dataView?.timeFieldName ?? null; buildCustomUrlFromSettings(dashboard, this.state.editorSettings as CustomUrlSettings).then( (customUrl) => { - getTestUrl(job, customUrl, timefieldName, this.props.currentTimeFilter) + getTestUrl( + job, + customUrl, + timefieldName, + this.props.currentTimeFilter, + this.props.isPartialDFAJob + ) .then((testUrl) => { openCustomUrlWindow(testUrl, customUrl, basePath.get()); }) @@ -224,13 +235,16 @@ class CustomUrlsUI extends Component { const editMode = this.props.editMode ?? 'inline'; const editor = ( ); @@ -340,6 +354,7 @@ class CustomUrlsUI extends Component { customUrls={customUrls} onChange={this.props.setCustomUrls} dataViewListItems={this.state.dataViewListItems} + isPartialDFAJob={this.props.isPartialDFAJob} /> ); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls_wrapper.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls_wrapper.tsx index 6053ae01c4b4b..aac3c5d362e25 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls_wrapper.tsx @@ -18,6 +18,7 @@ export interface CustomUrlsWrapperProps { jobCustomUrls: MlUrlConfig[]; setCustomUrls: (customUrls: MlUrlConfig[]) => void; editMode?: 'inline' | 'modal'; + isPartialDFAJob?: boolean; } export const CustomUrlsWrapper: FC = (props) => { diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx index f72fb8d00f173..02c1a2c70aa92 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx @@ -76,7 +76,7 @@ export const FieldStatsFlyoutProvider: FC<{ fields: ['*'], _source: false, ...queryAndRunTimeMappings, - size: 1000, + size: 500, }, }; const cacheKey = stringHash(JSON.stringify(esSearchRequestParams)).toString(); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/additional_section.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/additional_section.tsx new file mode 100644 index 0000000000000..4b3faeb00ea1c --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/additional_section.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, { FC, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiSpacer } from '@elastic/eui'; +import type { MlUrlConfig } from '@kbn/ml-anomaly-utils'; +import type { DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-utils'; +import { DeepPartial } from '../../../../../../../common/types/common'; +import { Description } from './description'; +import { CustomUrlsWrapper } from '../../../../../components/custom_urls'; +import { + getJobConfigFromFormState, + type State, +} from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ActionDispatchers } from '../../../analytics_management/hooks/use_create_analytics_form/actions'; + +const buttonContent = i18n.translate( + 'xpack.ml.dataframe.analytics.create.detailsStep.additionalSectionButton', + { + defaultMessage: 'Additional settings', + } +); + +interface Props { + formState: State['form']; + setFormState: ActionDispatchers['setFormState']; +} + +export const AdditionalSection: FC = ({ formState, setFormState }) => { + const [additionalExpanded, setAdditionalExpanded] = useState(false); + const { _meta: formMeta } = formState; + + const analyticsJob = useMemo( + () => getJobConfigFromFormState(formState) as DeepPartial, + [formState] + ); + const setCustomUrls = (urls: MlUrlConfig[]) => { + setFormState({ _meta: { ...formMeta, custom_urls: urls } }); + }; + + return ( + <> + + +
+ + + + +
+
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/description.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/description.tsx new file mode 100644 index 0000000000000..67c49b16e87fa --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/description.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, { memo, FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { useMlKibana } from '../../../../../contexts/kibana'; + +export const Description: FC = memo(({ children }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const docsUrl = docLinks.links.ml.customUrls; + const title = i18n.translate( + 'xpack.ml.dataframe.analytics.create.detailsStep.additionalSection.customUrls.title', + { + defaultMessage: 'Custom URLs', + } + ); + + const cssOverride = css({ + '> .euiFlexGroup': { + '> .euiFlexItem': { + '&:last-child': { + flexBasis: '50%', + }, + }, + }, + }); + + return ( + {title}} + description={ + + + + ), + }} + /> + } + > + + <>{children} + + + ); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 8120be933c74f..064b182200f45 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -19,6 +19,7 @@ import { ANALYTICS_STEPS } from '../../page'; import { ml } from '../../../../../services/ml_api_service'; import { useDataSource } from '../../../../../contexts/ml'; import { DetailsStepTimeField } from './details_step_time_field'; +import { AdditionalSection } from './additional_section'; const DEFAULT_RESULTS_FIELD = 'ml'; @@ -385,6 +386,8 @@ export const DetailsStepForm: FC = ({ onTimeFieldChanged={onTimeFieldChanged} /> ) : null} + + { return (
- + {isCcsCallOut && ( <> { uiSettings, }} /> - +
); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx index f0043ca3ea47b..6932a61cfbba9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx @@ -42,7 +42,7 @@ export const Description: FC = memo(({ children }) => { description={ 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 d29654f8e9f8d..d9c0200961caa 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 @@ -6,7 +6,7 @@ */ import React, { FC } from 'react'; -import { EuiPageBody, EuiPageContent_Deprecated as EuiPageContent } from '@elastic/eui'; +import { 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'; @@ -39,7 +39,7 @@ export const Page: FC = ({ nextStepPath }) => { defaultMessage="Select data view or saved search" /> - + = ({ nextStepPath }) => { uiSettings, }} /> - + ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index 29b6c781c7d24..62c5841a94ff1 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -6,10 +6,7 @@ */ import React, { FC, useEffect, Fragment, useMemo } from 'react'; -import { - EuiPageContentHeader_Deprecated as EuiPageContentHeader, - EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection, -} from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { getTimeFilterRange, useTimefilter } from '@kbn/ml-date-picker'; @@ -234,15 +231,13 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => {
- - - - - + + + ( - <> - - - - - } - body={ -

- -

- } - /> -
- + + + + } + body={ +

+ +

+ } + /> ); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 6941e53cd68b2..2ce2e9ddd9d56 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useState, FC, useCallback, useMemo } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Router } from '@kbn/shared-ux-router'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -13,11 +13,10 @@ import { CoreStart } from '@kbn/core/public'; import { EuiButtonEmpty, - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, - EuiSpacer, EuiFlexGroup, EuiFlexItem, + EuiPageTemplate, + EuiSpacer, } from '@elastic/eui'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; @@ -29,7 +28,7 @@ import { RedirectAppLinks, } from '@kbn/kibana-react-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; -import type { SpacesPluginStart, SpacesContextProps } from '@kbn/spaces-plugin/public'; +import type { SpacesContextProps, SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { PLUGIN_ID } from '../../../../../../common/constants/app'; @@ -132,7 +131,7 @@ export const JobsListPage: FC<{ > - ]} bottomBorder + paddingSize={'none'} /> - @@ -183,7 +184,7 @@ export const JobsListPage: FC<{ - + diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/data_comparison.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/data_comparison.tsx index b8efd6b7df529..46f58318c1b92 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/data_comparison.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/data_comparison.tsx @@ -52,7 +52,7 @@ export const dataComparisonRouteFactory = ( }); const PageWrapper: FC = () => { - const { context } = useRouteResolver('basic', [], basicResolvers()); + const { context } = useRouteResolver('full', [], basicResolvers()); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index bc93f13f01a70..cb12cd22ab6c2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -199,7 +199,7 @@ export const dataComparisonIndexOrSearchRouteFactory = ( {...props} nextStepPath={createPath(ML_PAGES.DATA_COMPARISON)} deps={deps} - mode={MODE.DATAVISUALIZER} + mode={MODE.NEW_JOB} /> ), breadcrumbs: getDataVisBreadcrumbs(navigateToPath, basePath), diff --git a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx index e068a29763fa0..4a19bc96c8c67 100644 --- a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx +++ b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx @@ -12,7 +12,6 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiPageContentHeader_Deprecated as EuiPageContentHeader, EuiSpacer, EuiText, EuiTextColor, @@ -78,16 +77,14 @@ export const AnomalyDetectionSettings: FC = () => { return ( - - -

- -

-
-
+ +

+ +

+
diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts index 73688aebc94f8..57db3c66a7c3b 100644 --- a/x-pack/plugins/ml/public/shared.ts +++ b/x-pack/plugins/ml/public/shared.ts @@ -16,3 +16,5 @@ export * from '../common/util/validators'; export * from './application/formatters/metric_change_description'; export * from './application/components/field_stats_flyout'; export * from './application/data_frame_analytics/common'; + +export { useFieldStatsFlyoutContext } from './application/components/field_stats_flyout/use_field_stats_flytout_context'; diff --git a/x-pack/plugins/observability/common/locators/paths.ts b/x-pack/plugins/observability/common/locators/paths.ts index 234e53f37c3ad..a69062321dd29 100644 --- a/x-pack/plugins/observability/common/locators/paths.ts +++ b/x-pack/plugins/observability/common/locators/paths.ts @@ -13,6 +13,7 @@ export const ALERTS_PATH = '/alerts' as const; export const ALERT_DETAIL_PATH = '/alerts/:alertId' as const; export const EXPLORATORY_VIEW_PATH = '/exploratory-view' as const; // has been moved to its own app. Keeping around for redirecting purposes. export const RULES_PATH = '/alerts/rules' as const; +export const RULES_LOGS_PATH = '/alerts/rules/logs' as const; export const RULE_DETAIL_PATH = '/alerts/rules/:ruleId' as const; export const SLOS_PATH = '/slos' as const; export const SLOS_WELCOME_PATH = '/slos/welcome' as const; @@ -42,3 +43,9 @@ export const paths = { : `${OBSERVABILITY_BASE_PATH}${SLOS_PATH}/${encodeURI(sloId)}`, }, }; + +export const relativePaths = { + observability: { + ruleDetails: (ruleId: string) => `${RULES_PATH}/${encodeURI(ruleId)}`, + }, +}; diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.json b/x-pack/plugins/observability/docs/openapi/slo/bundled.json index 653b8e72e3bfd..374d35c9dd212 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.json +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.json @@ -1334,6 +1334,7 @@ "enabled", "groupBy", "instanceId", + "tags", "createdAt", "updatedAt" ], @@ -1417,6 +1418,13 @@ "type": "string", "example": "host-abcde" }, + "tags": { + "description": "List of tags", + "type": "array", + "items": { + "type": "string" + } + }, "createdAt": { "description": "The creation date", "type": "string", @@ -1607,6 +1615,13 @@ "description": "optional group by field to use to generate an SLO per distinct value", "type": "string", "example": "some.field" + }, + "tags": { + "description": "List of tags", + "type": "array", + "items": { + "type": "string" + } } } }, @@ -1689,6 +1704,13 @@ }, "settings": { "$ref": "#/components/schemas/settings" + }, + "tags": { + "description": "List of tags", + "type": "array", + "items": { + "type": "string" + } } } }, diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml index c23ba97556c95..a6cdf5c376485 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml @@ -924,6 +924,7 @@ components: - enabled - groupBy - instanceId + - tags - createdAt - updatedAt properties: @@ -980,6 +981,11 @@ components: description: the value derived from the groupBy field, if present, otherwise '*' type: string example: host-abcde + tags: + description: List of tags + type: array + items: + type: string createdAt: description: The creation date type: string @@ -1117,6 +1123,11 @@ components: description: optional group by field to use to generate an SLO per distinct value type: string example: some.field + tags: + description: List of tags + type: array + items: + type: string create_slo_response: title: Create SLO response type: object @@ -1170,6 +1181,11 @@ components: $ref: '#/components/schemas/objective' settings: $ref: '#/components/schemas/settings' + tags: + description: List of tags + type: array + items: + type: string historical_summary_request: title: Historical summary request type: object diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/create_slo_request.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/create_slo_request.yaml index 754d9103d7e9f..f14a1a134abd8 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/create_slo_request.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/create_slo_request.yaml @@ -39,3 +39,8 @@ properties: description: optional group by field to use to generate an SLO per distinct value type: string example: "some.field" + tags: + description: List of tags + type: array + items: + type: string diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml index b4e5eb85f3264..da81009bc20b3 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml @@ -14,6 +14,7 @@ required: - enabled - groupBy - instanceId + - tags - createdAt - updatedAt properties: @@ -70,6 +71,11 @@ properties: description: the value derived from the groupBy field, if present, otherwise '*' type: string example: 'host-abcde' + tags: + description: List of tags + type: array + items: + type: string createdAt: description: The creation date type: string diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/update_slo_request.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/update_slo_request.yaml index 0ba9a79ac2ac1..ddeb2e39159a3 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/update_slo_request.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/update_slo_request.yaml @@ -25,3 +25,8 @@ properties: $ref: "objective.yaml" settings: $ref: "settings.yaml" + tags: + description: List of tags + type: array + items: + type: string diff --git a/x-pack/plugins/observability/kibana.jsonc b/x-pack/plugins/observability/kibana.jsonc index 2ced468eb1f98..a88f6543871e4 100644 --- a/x-pack/plugins/observability/kibana.jsonc +++ b/x-pack/plugins/observability/kibana.jsonc @@ -11,6 +11,7 @@ "alerting", "cases", "charts", + "contentManagement", "data", "dataViews", "dataViewEditor", diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/alert_summary.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/alert_summary.tsx index a4ca36772217b..bef504ed330ea 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/alert_summary.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/alert_summary.tsx @@ -11,7 +11,7 @@ export interface AlertSummaryField { label: ReactNode | string; value: ReactNode | string | number; } -export interface AlertSummaryProps { +interface AlertSummaryProps { alertSummaryFields?: AlertSummaryField[]; } diff --git a/x-pack/plugins/observability/public/pages/overview/components/date_picker/date_picker.tsx b/x-pack/plugins/observability/public/pages/overview/components/date_picker/date_picker.tsx index 1de12b64e6dfb..e3bb21a460067 100644 --- a/x-pack/plugins/observability/public/pages/overview/components/date_picker/date_picker.tsx +++ b/x-pack/plugins/observability/public/pages/overview/components/date_picker/date_picker.tsx @@ -26,7 +26,7 @@ export interface TimePickerTimeDefaults { to: string; } -export interface DatePickerProps { +interface DatePickerProps { rangeFrom?: string; rangeTo?: string; refreshPaused?: boolean; diff --git a/x-pack/plugins/observability/public/pages/rules/global_logs_tab.tsx b/x-pack/plugins/observability/public/pages/rules/global_logs_tab.tsx new file mode 100644 index 0000000000000..83b79b11c6cab --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/global_logs_tab.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { relativePaths } from '../../../common/locators/paths'; +import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; +import { useKibana } from '../../utils/kibana_react'; + +export function GlobalLogsTab() { + const { + triggersActionsUi: { getGlobalRuleEventLogList: GlobalRuleEventLogList }, + } = useKibana().services; + const filteredRuleTypes = useGetFilteredRuleTypes(); + + return ( + + ); +} + +// eslint-disable-next-line import/no-default-export +export { GlobalLogsTab as default }; diff --git a/x-pack/plugins/observability/public/pages/rules/rules.tsx b/x-pack/plugins/observability/public/pages/rules/rules.tsx index 011f0500e8efa..2c2ef3463b0ea 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules.tsx @@ -5,34 +5,40 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { lazy, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { RuleStatus, useLoadRuleTypes } from '@kbn/triggers-actions-ui-plugin/public'; +import { useLoadRuleTypes } from '@kbn/triggers-actions-ui-plugin/public'; import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; -import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import { RULES_LOGS_PATH, RULES_PATH } from '../../../common/locators/paths'; import { useKibana } from '../../utils/kibana_react'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; import { observabilityRuleCreationValidConsumers } from '../../../common/constants'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; +import { RulesTab } from './rules_tab'; -export function RulesPage() { +const GlobalLogsTab = lazy(() => import('./global_logs_tab')); + +const RULES_TAB_NAME = 'rules'; + +interface RulesPageProps { + activeTab?: string; +} +export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { const { http, docLinks, - triggersActionsUi: { - getAddRuleFlyout: AddRuleFlyout, - getRulesList: RuleList, - getRulesSettingsLink: RulesSettingsLink, - }, + triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout, getRulesSettingsLink: RulesSettingsLink }, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); const history = useHistory(); + const [addRuleFlyoutVisibility, setAddRuleFlyoutVisibility] = useState(false); + const [stateRefresh, setRefresh] = useState(new Date()); useBreadcrumbs([ { @@ -58,63 +64,29 @@ export function RulesPage() { (ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all ); - const urlStateStorage = createKbnUrlStateStorage({ - history, - useHash: false, - useHashQuery: false, - }); - - const { lastResponse, params, search, status, type } = urlStateStorage.get<{ - lastResponse: string[]; - params: Record; - search: string; - status: RuleStatus[]; - type: string[]; - }>('_a') || { lastResponse: [], params: {}, search: '', status: [], type: [] }; - - const [stateLastResponse, setLastResponse] = useState(lastResponse); - const [stateParams, setParams] = useState>(params); - const [stateSearch, setSearch] = useState(search); - const [stateStatus, setStatus] = useState(status); - const [stateType, setType] = useState(type); - - const [stateRefresh, setRefresh] = useState(new Date()); - - const [addRuleFlyoutVisibility, setAddRuleFlyoutVisibility] = useState(false); - - const handleStatusFilterChange = (newStatus: RuleStatus[]) => { - setStatus(newStatus); - urlStateStorage.set('_a', { lastResponse, params, search, status: newStatus, type }); - }; - - const handleLastRunOutcomeFilterChange = (newLastResponse: string[]) => { - setRefresh(new Date()); - setLastResponse(newLastResponse); - urlStateStorage.set('_a', { lastResponse: newLastResponse, params, search, status, type }); - }; - - const handleTypeFilterChange = (newType: string[]) => { - setType(newType); - urlStateStorage.set('_a', { lastResponse, params, search, status, type: newType }); - }; - - const handleSearchFilterChange = (newSearch: string) => { - setSearch(newSearch); - urlStateStorage.set('_a', { lastResponse, params, search: newSearch, status, type }); - }; - - const handleRuleParamFilterChange = (newParams: Record) => { - setParams(newParams); - urlStateStorage.set('_a', { lastResponse, params: newParams, search, status, type }); - }; + const tabs = [ + { + name: 'rules', + label: ( + + ), + onClick: () => history.push(RULES_PATH), + isSelected: activeTab === RULES_TAB_NAME, + }, + { + name: 'logs', + label: ( + + ), + onClick: () => history.push(RULES_LOGS_PATH), + ['data-test-subj']: 'ruleLogsTab', + isSelected: activeTab !== RULES_TAB_NAME, + }, + ]; - return ( - , - , - - - , - ], + ] + : []), + , + + + , + ]; + + return ( + - + {activeTab === RULES_TAB_NAME ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx b/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx new file mode 100644 index 0000000000000..317091e81fb29 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; + +import { useHistory } from 'react-router-dom'; +import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { RuleStatus } from '@kbn/triggers-actions-ui-plugin/public'; +import { useKibana } from '../../utils/kibana_react'; +import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; + +interface RulesTabProps { + setRefresh: React.Dispatch>; + stateRefresh: Date; +} + +export function RulesTab({ setRefresh, stateRefresh }: RulesTabProps) { + const { + triggersActionsUi: { getRulesList: RuleList }, + } = useKibana().services; + const history = useHistory(); + + const urlStateStorage = createKbnUrlStateStorage({ + history, + useHash: false, + useHashQuery: false, + }); + + const filteredRuleTypes = useGetFilteredRuleTypes(); + + const { lastResponse, params, search, status, type } = urlStateStorage.get<{ + lastResponse: string[]; + params: Record; + search: string; + status: RuleStatus[]; + type: string[]; + }>('_a') || { lastResponse: [], params: {}, search: '', status: [], type: [] }; + + const [stateLastResponse, setLastResponse] = useState(lastResponse); + const [stateParams, setParams] = useState>(params); + const [stateSearch, setSearch] = useState(search); + const [stateStatus, setStatus] = useState(status); + const [stateType, setType] = useState(type); + + const handleStatusFilterChange = (newStatus: RuleStatus[]) => { + setStatus(newStatus); + urlStateStorage.set('_a', { lastResponse, params, search, status: newStatus, type }); + }; + + const handleLastRunOutcomeFilterChange = (newLastResponse: string[]) => { + setRefresh(new Date()); + setLastResponse(newLastResponse); + urlStateStorage.set('_a', { lastResponse: newLastResponse, params, search, status, type }); + }; + + const handleTypeFilterChange = (newType: string[]) => { + setType(newType); + urlStateStorage.set('_a', { lastResponse, params, search, status, type: newType }); + }; + + const handleSearchFilterChange = (newSearch: string) => { + setSearch(newSearch); + urlStateStorage.set('_a', { lastResponse, params, search: newSearch, status, type }); + }; + + const handleRuleParamFilterChange = (newParams: Record) => { + setParams(newParams); + urlStateStorage.set('_a', { lastResponse, params: newParams, search, status, type }); + }; + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx index 5c6f06051eb3d..bfb629f3c8071 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx @@ -9,13 +9,19 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { CreateSLOForm } from '../../types'; import { FieldSelector } from '../apm_common/field_selector'; import { DataPreviewChart } from '../common/data_preview_chart'; +import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; export function ApmAvailabilityIndicatorTypeForm() { const { watch } = useFormContext(); + const index = watch('indicator.params.index'); + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = + useFetchIndexPatternFields(index); + const partitionByFields = indexFields.filter((field) => field.aggregatable); return ( @@ -119,6 +125,29 @@ export function ApmAvailabilityIndicatorTypeForm() { + + {i18n.translate('xpack.observability.slo.sloEdit.groupBy.label', { + defaultMessage: 'Partition by', + })}{' '} + + + } + placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { + defaultMessage: 'Select an optional field to partition by', + })} + isLoading={!!index && isIndexFieldsLoading} + isDisabled={!index} + /> + ); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx index e91080e2910dd..8d21a0c7d2546 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx @@ -9,13 +9,19 @@ import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip } fro import { i18n } from '@kbn/i18n'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { CreateSLOForm } from '../../types'; import { FieldSelector } from '../apm_common/field_selector'; import { DataPreviewChart } from '../common/data_preview_chart'; +import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; export function ApmLatencyIndicatorTypeForm() { const { control, watch, getFieldState } = useFormContext(); + const index = watch('indicator.params.index'); + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = + useFetchIndexPatternFields(index); + const partitionByFields = indexFields.filter((field) => field.aggregatable); return ( @@ -162,6 +168,29 @@ export function ApmLatencyIndicatorTypeForm() { + + {i18n.translate('xpack.observability.slo.sloEdit.groupBy.label', { + defaultMessage: 'Partition by', + })}{' '} + + + } + placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { + defaultMessage: 'Select an optional field to partition by', + })} + isLoading={!!index && isIndexFieldsLoading} + isDisabled={!index} + /> + ); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/group_by_field_selector.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/group_by_field_selector.tsx deleted file mode 100644 index 0733e682e9ba7..0000000000000 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/common/group_by_field_selector.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiComboBox, - EuiComboBoxOptionOption, - EuiFlexItem, - EuiFormRow, - EuiIconTip, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ALL_VALUE } from '@kbn/slo-schema'; -import React from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; -import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; -import { createOptionsFromFields } from '../../helpers/create_options'; -import { CreateSLOForm } from '../../types'; - -interface Props { - index?: string; -} -export function GroupByFieldSelector({ index }: Props) { - const { control, getFieldState } = useFormContext(); - const { isLoading, data: indexFields = [] } = useFetchIndexPatternFields(index); - const groupableFields = indexFields.filter((field) => field.aggregatable); - - const label = i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { - defaultMessage: 'Select an optional field to partition by', - }); - - return ( - - - {i18n.translate('xpack.observability.slo.sloEdit.groupBy.label', { - defaultMessage: 'Partition by', - })}{' '} - - - } - isInvalid={getFieldState('groupBy').invalid} - > - ( - { - if (selected.length) { - return field.onChange(selected[0].value); - } - - field.onChange(ALL_VALUE); - }} - options={createOptionsFromFields(groupableFields)} - selectedOptions={ - !!index && - !!field.value && - groupableFields.some((groupableField) => groupableField.name === field.value) - ? [{ value: field.value, label: field.value }] - : [] - } - singleSelection - /> - )} - /> - - - ); -} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/index_field_selector.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/index_field_selector.tsx new file mode 100644 index 0000000000000..6bb6996f95b2a --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/common/index_field_selector.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { ALL_VALUE } from '@kbn/slo-schema'; +import React, { useEffect, useState } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; +import { createOptionsFromFields, Option } from '../../helpers/create_options'; +import { CreateSLOForm } from '../../types'; + +interface Props { + indexFields: Field[]; + name: 'groupBy' | 'indicator.params.timestampField'; + label: React.ReactNode | string; + placeholder: string; + isDisabled: boolean; + isLoading: boolean; + isRequired?: boolean; +} +export function IndexFieldSelector({ + indexFields, + name, + label, + placeholder, + isDisabled, + isLoading, + isRequired = false, +}: Props) { + const { control, getFieldState } = useFormContext(); + const [options, setOptions] = useState(createOptionsFromFields(indexFields)); + + useEffect(() => { + setOptions(createOptionsFromFields(indexFields)); + }, [indexFields]); + + return ( + + + ( + + {...field} + async + placeholder={placeholder} + aria-label={placeholder} + isClearable + isDisabled={isLoading || isDisabled} + isInvalid={fieldState.invalid} + isLoading={isLoading} + onChange={(selected: EuiComboBoxOptionOption[]) => { + if (selected.length) { + return field.onChange(selected[0].value); + } + + field.onChange(ALL_VALUE); + }} + options={options} + onSearchChange={(searchValue: string) => { + setOptions( + createOptionsFromFields(indexFields, ({ value }) => value.includes(searchValue)) + ); + }} + selectedOptions={ + !!indexFields && + !!field.value && + indexFields.some((indexField) => indexField.name === field.value) + ? [{ value: field.value, label: field.value }] + : [] + } + singleSelection + /> + )} + /> + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx index 6d7ae6c012a2e..8555750fd6ff1 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx @@ -5,31 +5,24 @@ * 2.0. */ -import { - EuiComboBox, - EuiComboBoxOptionOption, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiIconTip, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { useFormContext } from 'react-hook-form'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; -import { createOptionsFromFields } from '../../helpers/create_options'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; -import { GroupByFieldSelector } from '../common/group_by_field_selector'; +import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; import { IndexSelection } from '../custom_common/index_selection'; export function CustomKqlIndicatorTypeForm() { - const { control, watch, getFieldState } = useFormContext(); - + const { watch } = useFormContext(); const index = watch('indicator.params.index'); - const { isLoading, data: indexFields } = useFetchIndexPatternFields(index); - const timestampFields = (indexFields ?? []).filter((field) => field.type === 'date'); + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = + useFetchIndexPatternFields(index); + const timestampFields = indexFields.filter((field) => field.type === 'date'); + const partitionByFields = indexFields.filter((field) => field.aggregatable); return ( @@ -37,56 +30,22 @@ export function CustomKqlIndicatorTypeForm() { + - - ( - { - if (selected.length) { - return field.onChange(selected[0].value); - } - - field.onChange(''); - }} - options={createOptionsFromFields(timestampFields)} - selectedOptions={ - !!index && - !!field.value && - timestampFields.some((timestampField) => timestampField.name === field.value) - ? [{ value: field.value, label: field.value }] - : [] - } - singleSelection - /> - )} - /> - + isLoading={!!index && isIndexFieldsLoading} + isDisabled={!index} + isRequired + /> @@ -176,7 +135,28 @@ export function CustomKqlIndicatorTypeForm() { /> - + + {i18n.translate('xpack.observability.slo.sloEdit.groupBy.label', { + defaultMessage: 'Partition by', + })}{' '} + + + } + placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { + defaultMessage: 'Select an optional field to partition by', + })} + isLoading={!!index && isIndexFieldsLoading} + isDisabled={!index} + /> diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx index 25de4568dc06c..c7fe7999f9be4 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx @@ -6,37 +6,34 @@ */ import { - EuiComboBox, - EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiHorizontalRule, EuiIconTip, EuiSpacer, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { useFormContext } from 'react-hook-form'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; -import { createOptionsFromFields } from '../../helpers/create_options'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; +import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; import { IndexSelection } from '../custom_common/index_selection'; import { MetricIndicator } from './metric_indicator'; -import { GroupByFieldSelector } from '../common/group_by_field_selector'; export { NEW_CUSTOM_METRIC } from './metric_indicator'; export function CustomMetricIndicatorTypeForm() { - const { control, watch, getFieldState } = useFormContext(); - + const { watch } = useFormContext(); const index = watch('indicator.params.index'); - const { isLoading, data: indexFields } = useFetchIndexPatternFields(index); - const timestampFields = (indexFields ?? []).filter((field) => field.type === 'date'); + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = + useFetchIndexPatternFields(index); + const timestampFields = indexFields.filter((field) => field.type === 'date'); + const partitionByFields = indexFields.filter((field) => field.aggregatable); return ( <> @@ -55,61 +52,20 @@ export function CustomMetricIndicatorTypeForm() { - - ( - { - if (selected.length) { - return field.onChange(selected[0].value); - } - - field.onChange(''); - }} - options={createOptionsFromFields(timestampFields)} - selectedOptions={ - !!watch('indicator.params.index') && - !!field.value && - timestampFields.some((timestampField) => timestampField.name === field.value) - ? [ - { - value: field.value, - label: field.value, - 'data-test-subj': `customMetricIndicatorFormTimestampFieldSelectedValue`, - }, - ] - : [] - } - singleSelection={{ asPlainText: true }} - /> - )} - /> - + isLoading={!!index && isIndexFieldsLoading} + isDisabled={!index} + isRequired + /> @@ -157,7 +113,11 @@ export function CustomMetricIndicatorTypeForm() { - + @@ -174,14 +134,39 @@ export function CustomMetricIndicatorTypeForm() { - + - + + {i18n.translate('xpack.observability.slo.sloEdit.groupBy.label', { + defaultMessage: 'Partition by', + })}{' '} + + + } + placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { + defaultMessage: 'Select an optional field to partition by', + })} + isLoading={!!index && isIndexFieldsLoading} + isDisabled={!index} + /> diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx index 794726db2c711..f451d5d98007c 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx @@ -28,7 +28,7 @@ import { QueryBuilder } from '../common/query_builder'; interface MetricIndicatorProps { type: 'good' | 'total'; - indexFields: Field[] | undefined; + indexFields: Field[]; isLoadingIndex: boolean; } @@ -91,9 +91,7 @@ export function MetricIndicator({ type, indexFields, isLoadingIndex }: MetricInd ); const { control, watch, setValue, register } = useFormContext(); - const metricFields = (indexFields ?? []).filter((field) => - SUPPORTED_FIELD_TYPES.includes(field.type) - ); + const metricFields = indexFields.filter((field) => SUPPORTED_FIELD_TYPES.includes(field.type)); const { fields, append, remove } = useFieldArray({ control, diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator.tsx index e966d0f3fb221..2b6f983470f82 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator.tsx @@ -25,7 +25,7 @@ import { createOptionsFromFields } from '../../helpers/create_options'; interface HistogramIndicatorProps { type: 'good' | 'total'; - indexFields: Field[] | undefined; + indexFields: Field[]; isLoadingIndex: boolean; } @@ -49,7 +49,7 @@ const AGGREGATION_OPTIONS = Object.values(AGGREGATIONS); export function HistogramIndicator({ type, indexFields, isLoadingIndex }: HistogramIndicatorProps) { const { control, watch } = useFormContext(); - const histogramFields = (indexFields ?? []).filter((field) => field.type === 'histogram'); + const histogramFields = indexFields.filter((field) => field.type === 'histogram'); const indexPattern = watch('indicator.params.index'); const aggregation = watch(`indicator.params.${type}.aggregation`); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx index 56a867ab2e332..dfbf305235b41 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx @@ -6,35 +6,33 @@ */ import { - EuiComboBox, - EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiHorizontalRule, EuiIconTip, EuiSpacer, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { useFormContext } from 'react-hook-form'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; -import { createOptionsFromFields } from '../../helpers/create_options'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; +import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; import { IndexSelection } from '../custom_common/index_selection'; import { HistogramIndicator } from './histogram_indicator'; -import { GroupByFieldSelector } from '../common/group_by_field_selector'; export function HistogramIndicatorTypeForm() { - const { control, watch, getFieldState } = useFormContext(); - + const { watch } = useFormContext(); const index = watch('indicator.params.index'); - const { isLoading, data: indexFields } = useFetchIndexPatternFields(index); - const timestampFields = (indexFields ?? []).filter((field) => field.type === 'date'); + + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = + useFetchIndexPatternFields(index); + const timestampFields = indexFields.filter((field) => field.type === 'date'); + const partitionByFields = indexFields.filter((field) => field.aggregatable); return ( <> @@ -53,55 +51,20 @@ export function HistogramIndicatorTypeForm() { - - ( - { - if (selected.length) { - return field.onChange(selected[0].value); - } - - field.onChange(''); - }} - options={createOptionsFromFields(timestampFields)} - selectedOptions={ - !!index && - !!field.value && - timestampFields.some((timestampField) => timestampField.name === field.value) - ? [{ value: field.value, label: field.value }] - : [] - } - singleSelection={{ asPlainText: true }} - /> - )} - /> - + isLoading={!!index && isIndexFieldsLoading} + isDisabled={!index} + isRequired + /> @@ -144,7 +107,11 @@ export function HistogramIndicatorTypeForm() { - + @@ -159,13 +126,38 @@ export function HistogramIndicatorTypeForm() { - + - + + {i18n.translate('xpack.observability.slo.sloEdit.groupBy.label', { + defaultMessage: 'Partition by', + })}{' '} + + + } + placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { + defaultMessage: 'Select an optional field to partition by', + })} + isLoading={!!index && isIndexFieldsLoading} + isDisabled={!index} + /> diff --git a/x-pack/plugins/observability/public/pages/slo_edit/helpers/create_options.ts b/x-pack/plugins/observability/public/pages/slo_edit/helpers/create_options.ts index c76b881ccc8aa..f090e92026176 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/helpers/create_options.ts +++ b/x-pack/plugins/observability/public/pages/slo_edit/helpers/create_options.ts @@ -7,13 +7,22 @@ import { Field } from '../../../hooks/slo/use_fetch_index_pattern_fields'; -interface Option { +export interface Option { label: string; value: string; } -export function createOptionsFromFields(fields: Field[]): Option[] { - return fields +export function createOptionsFromFields( + fields: Field[], + filterFn?: (option: Option) => boolean +): Option[] { + const options = fields .map((field) => ({ label: field.name, value: field.name })) .sort((a, b) => String(a.label).localeCompare(b.label)); + + if (filterFn) { + return options.filter(filterFn); + } + + return options; } diff --git a/x-pack/plugins/observability/public/plugin.mock.tsx b/x-pack/plugins/observability/public/plugin.mock.tsx index 81c3aa70cbd37..e30d5254e7a07 100644 --- a/x-pack/plugins/observability/public/plugin.mock.tsx +++ b/x-pack/plugins/observability/public/plugin.mock.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; const triggersActionsUiStartMock = { createStart() { @@ -94,6 +95,7 @@ export const observabilityPublicPluginsStartMock = { return { cases: mockCasesContract(), charts: chartPluginMock.createStartContract(), + contentManagement: contentManagementMock.createStartContract(), triggersActionsUi: triggersActionsUiStartMock.createStart(), data: dataPluginMock.createStartContract(), dataViews: dataViews.createStart(), diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 0a5b238023b4e..77407c9bd25e8 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -39,6 +39,7 @@ import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { @@ -115,6 +116,7 @@ export interface ObservabilityPublicPluginsStart { actionTypeRegistry: ActionTypeRegistryContract; cases: CasesUiStart; charts: ChartsPluginStart; + contentManagement: ContentManagementPublicStart; data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; dataViewEditor: DataViewEditorStart; diff --git a/x-pack/plugins/observability/public/routes/routes.tsx b/x-pack/plugins/observability/public/routes/routes.tsx index f04b98227e78a..e1559050056f0 100644 --- a/x-pack/plugins/observability/public/routes/routes.tsx +++ b/x-pack/plugins/observability/public/routes/routes.tsx @@ -28,6 +28,7 @@ import { LANDING_PATH, OVERVIEW_PATH, ROOT_PATH, + RULES_LOGS_PATH, RULES_PATH, RULE_DETAIL_PATH, SLOS_PATH, @@ -108,6 +109,13 @@ export const routes = { params: {}, exact: true, }, + [RULES_LOGS_PATH]: { + handler: () => { + return ; + }, + params: {}, + exact: true, + }, [RULE_DETAIL_PATH]: { handler: () => { return ; diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 3fecda34a9ee5..73543b3a89453 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -84,7 +84,8 @@ "@kbn/core-capabilities-common", "@kbn/deeplinks-analytics", "@kbn/observability-ai-assistant-plugin", - "@kbn/osquery-plugin" + "@kbn/osquery-plugin", + "@kbn/content-management-plugin" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/observability_ai_assistant/README.md b/x-pack/plugins/observability_ai_assistant/README.md index 8487cbf13ba10..11e6d7d38d659 100644 --- a/x-pack/plugins/observability_ai_assistant/README.md +++ b/x-pack/plugins/observability_ai_assistant/README.md @@ -66,8 +66,8 @@ The knowledge base is an Elasticsearch index, with an inference processor powere Both the user and the LLM are able to suggest functions, that are executed on behalf (and with the privileges of) the user. Functions allow both the user and the LLM to include relevant context into the conversation. This context can be text, data, or a visual component, like a timeseries graph. Some of the functions that are available are: -- `recall` and `summarise`: these functions query (with a semantic search) or write to (with a summarisation) the knowledge database. This allows the LLM to create a (partly) user-specific working memory, and access predefined embeddings that help improve its understanding of the Elastic platform. -- `lens`: a function that can be used to create Lens visualisations using Formulas. +- `recall` and `summarize`: these functions query (with a semantic search) or write to (with a summarisation) the knowledge database. This allows the LLM to create a (partly) user-specific working memory, and access predefined embeddings that help improve its understanding of the Elastic platform. +- `lens`: a function that can be used to create Lens vizualisations using Formulas. - `get_apm_timeseries`, `get_apm_service_summary`, `get_apm_downstream_dependencies` and `get_apm_error_document`: a set of APM functions, some with visual components, that are helpful in performing root cause analysis. -Function calling is completely transparent to the user - they can edit function suggestions from the LLM, or inspect a function response (but not edit it), or they can request a function themselves. +Function calling is completely transparent to the user - they can edit function suggestions from the LLM, or inspect a function response (but not edit it), or they can request a function themselves. diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index 1d8be57fd53b3..b14137dbaebf5 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -88,7 +88,7 @@ interface FunctionOptions = ( - options: { arguments: TArguments }, + options: { arguments: TArguments; messages: Message[] }, signal: AbortSignal ) => Promise; @@ -99,7 +99,10 @@ type RenderFunction = (options: export interface FunctionDefinition { options: FunctionOptions; - respond: (options: { arguments: any }, signal: AbortSignal) => Promise; + respond: ( + options: { arguments: any; messages: Message[] }, + signal: AbortSignal + ) => Promise; render?: RenderFunction; } diff --git a/x-pack/plugins/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_ai_assistant/kibana.jsonc index e0299246377a3..ebe28df3a1141 100644 --- a/x-pack/plugins/observability_ai_assistant/kibana.jsonc +++ b/x-pack/plugins/observability_ai_assistant/kibana.jsonc @@ -7,8 +7,20 @@ "server": true, "browser": true, "configPath": ["xpack", "observabilityAIAssistant"], - "requiredPlugins": ["triggersActionsUi", "actions", "security", "features", "observabilityShared", "taskManager", "lens", "dataViews", "ruleRegistry"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "fieldFormats"], + "requiredPlugins": [ + "actions", + "dataViews", + "features", + "lens", + "licensing", + "observabilityShared", + "ruleRegistry", + "security", + "share", + "taskManager", + "triggersActionsUi" + ], + "requiredBundles": ["fieldFormats", "kibanaReact", "kibanaUtils"], "optionalPlugins": [], "extraPublicDirs": [] } diff --git a/x-pack/plugins/observability_ai_assistant/public/assets/elastic_ai_assistant.png b/x-pack/plugins/observability_ai_assistant/public/assets/elastic_ai_assistant.png new file mode 100644 index 0000000000000..63bd19136f512 Binary files /dev/null and b/x-pack/plugins/observability_ai_assistant/public/assets/elastic_ai_assistant.png differ diff --git a/x-pack/plugins/observability_ai_assistant/public/assets/illustration.png b/x-pack/plugins/observability_ai_assistant/public/assets/illustration.png new file mode 100644 index 0000000000000..6717f5f3e7509 Binary files /dev/null and b/x-pack/plugins/observability_ai_assistant/public/assets/illustration.png differ diff --git a/x-pack/plugins/observability_ai_assistant/public/assets/illustration.svg b/x-pack/plugins/observability_ai_assistant/public/assets/illustration.svg new file mode 100644 index 0000000000000..363c492fec84b --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/assets/illustration.svg @@ -0,0 +1,920 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx index ee180740937a0..ca47c242df495 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx @@ -34,11 +34,12 @@ export function ObservabilityAIAssistantActionMenuItem() { const [conversationId, setConversationId] = useState(); - const { conversation, displayedMessages, setDisplayedMessages, save } = useConversation({ - conversationId, - connectorId: connectors.selectedConnector, - chatService: chatService.value, - }); + const { conversation, displayedMessages, setDisplayedMessages, save, saveTitle } = + useConversation({ + conversationId, + connectorId: connectors.selectedConnector, + chatService: chatService.value, + }); if (!service.isEnabled()) { return null; @@ -48,6 +49,7 @@ export function ObservabilityAIAssistantActionMenuItem() { <> { setIsOpen(() => true); }} @@ -74,6 +76,7 @@ export function ObservabilityAIAssistantActionMenuItem() { title={conversation.value?.conversation.title ?? EMPTY_CONVERSATION_TITLE} messages={displayedMessages} conversationId={conversationId} + startedFrom="appTopNavbar" onClose={() => { setIsOpen(() => false); }} @@ -87,6 +90,9 @@ export function ObservabilityAIAssistantActionMenuItem() { onChatUpdate={(nextMessages) => { setDisplayedMessages(nextMessages); }} + onChatTitleSave={(newTitle) => { + saveTitle(newTitle); + }} /> ) : null} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/buttons/ask_assistant_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/buttons/ask_assistant_button.tsx index cbd95b9b80fe5..f3ac2d2a735ad 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/buttons/ask_assistant_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/buttons/ask_assistant_button.tsx @@ -56,14 +56,26 @@ export function AskAssistantButton({ switch (variant) { case 'basic': return ( - + {buttonLabel} ); case 'empty': return ( - + {buttonLabel} ); @@ -83,8 +95,15 @@ export function AskAssistantButton({ )} > = ( + props: HideExpandConversationListButtonProps +) => ; + +const defaultProps = { + isExpanded: true, +}; + +export const HideExpandConversationListButton = Template.bind({}); +HideExpandConversationListButton.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/buttons/hide_expand_conversation_list_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/buttons/hide_expand_conversation_list_button.tsx index 8921380c801e1..8347298161dd3 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/buttons/hide_expand_conversation_list_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/buttons/hide_expand_conversation_list_button.tsx @@ -8,11 +8,18 @@ import React from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -export function HideExpandConversationListButton( - props: React.ComponentProps & { isExpanded: boolean } -) { +export type HideExpandConversationListButtonProps = React.ComponentProps & { + isExpanded: boolean; +}; + +export function HideExpandConversationListButton(props: HideExpandConversationListButtonProps) { return ( - + {props.isExpanded ? i18n.translate('xpack.observabilityAiAssistant.hideExpandConversationButton.hide', { defaultMessage: 'Hide chats', diff --git a/x-pack/plugins/observability_ai_assistant/public/components/buttons/new_chat_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/buttons/new_chat_button.tsx index 453c8da351119..d11451c78eaba 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/buttons/new_chat_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/buttons/new_chat_button.tsx @@ -10,7 +10,12 @@ import { i18n } from '@kbn/i18n'; export function NewChatButton(props: React.ComponentProps) { return ( - + {i18n.translate('xpack.observabilityAiAssistant.newChatButton', { defaultMessage: 'New chat', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/buttons/regenerate_response_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/buttons/regenerate_response_button.tsx index b0439c854e2ca..77d66081f7868 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/buttons/regenerate_response_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/buttons/regenerate_response_button.tsx @@ -11,7 +11,12 @@ import { i18n } from '@kbn/i18n'; export function RegenerateResponseButton(props: Partial) { return ( - + {i18n.translate('xpack.observabilityAiAssistant.regenerateResponseButtonLabel', { defaultMessage: 'Regenerate', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/buttons/start_chat_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/buttons/start_chat_button.tsx index dedbc827af4b2..15042a251302e 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/buttons/start_chat_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/buttons/start_chat_button.tsx @@ -10,7 +10,13 @@ import { i18n } from '@kbn/i18n'; export function StartChatButton(props: React.ComponentProps) { return ( - + {i18n.translate('xpack.observabilityAiAssistant.insight.response.startChat', { defaultMessage: 'Start chat', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/buttons/stop_generating_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/buttons/stop_generating_button.tsx index 3008ee884ae64..41023e276fcc5 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/buttons/stop_generating_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/buttons/stop_generating_button.tsx @@ -11,7 +11,13 @@ import { i18n } from '@kbn/i18n'; export function StopGeneratingButton(props: Partial) { return ( - + {i18n.translate('xpack.observabilityAiAssistant.stopGeneratingButtonLabel', { defaultMessage: 'Stop generating', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx new file mode 100644 index 0000000000000..d33d622d4a0f8 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiButtonIcon, + EuiContextMenu, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiLoadingSpinner, + EuiPanel, + EuiPopover, + EuiSpacer, + EuiSwitch, + EuiText, +} from '@elastic/eui'; +import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base'; +import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import type { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; + +export function ChatActionsMenu({ + connectors, + connectorsManagementHref, + conversationId, + disabled, + knowledgeBase, + modelsManagementHref, + startedFrom, + onCopyConversationClick, +}: { + connectors: UseGenAIConnectorsResult; + connectorsManagementHref: string; + conversationId?: string; + disabled: boolean; + knowledgeBase: UseKnowledgeBaseResult; + modelsManagementHref: string; + startedFrom?: StartedFrom; + onCopyConversationClick: () => void; +}) { + const [isOpen, setIsOpen] = useState(false); + + const toggleActionsMenu = () => { + setIsOpen(!isOpen); + }; + + return ( + + } + panelPaddingSize="none" + closePopover={toggleActionsMenu} + > + + {i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.connector', { + defaultMessage: 'Connector', + })}{' '} + + { + connectors.connectors?.find(({ id }) => id === connectors.selectedConnector) + ?.name + } + +
+ ), + panel: 1, + }, + { + name: ( + + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase', + { + defaultMessage: 'Knowledge base', + } + )} + + + {knowledgeBase.status.loading || knowledgeBase.isInstalling ? ( + + ) : knowledgeBase.status.value?.ready ? ( + + ) : ( + + )} + + + ), + panel: 2, + }, + { + name: i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.copyConversation', + { + defaultMessage: 'Copy conversation', + } + ), + disabled: !conversationId, + onClick: () => { + toggleActionsMenu(); + onCopyConversationClick(); + }, + }, + ], + }, + { + id: 1, + width: 256, + title: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.connector', { + defaultMessage: 'Connector', + }), + content: ( + + + + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.connectorManagement.button', + { + defaultMessage: 'Manage connectors', + } + )} + + + ), + }, + { + id: 2, + width: 256, + title: i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.title', + { + defaultMessage: 'Knowledge base', + } + ), + content: ( + + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.description.paragraph', + { + defaultMessage: + 'Using a knowledge base is optional but improves the experience of using the Assistant significantly.', + } + )}{' '} + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.elser.learnMore', + { + defaultMessage: 'Learn more', + } + )} + +

+
+ + + {knowledgeBase.isInstalling || knowledgeBase.status.loading ? ( + + ) : ( + <> + { + if (e.target.checked) { + knowledgeBase.install(); + } + }} + /> + + + + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.connectorManagement', + { + defaultMessage: 'Go to Machine Learning', + } + )} + + + )} +
+ ), + }, + ]} + /> + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx index 0f1039f1c5c93..8a6227829c855 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import React, { useEffect, useRef, useState } from 'react'; +import { last } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, @@ -14,23 +16,21 @@ import { EuiSpacer, } from '@elastic/eui'; import { css } from '@emotion/css'; +import { euiThemeVars } from '@kbn/ui-theme'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; -import React, { useEffect, useRef } from 'react'; -import { last } from 'lodash'; import type { Message } from '../../../common/types'; import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; import { useTimeline } from '../../hooks/use_timeline'; +import { useLicense } from '../../hooks/use_license'; import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; -import { MissingCredentialsCallout } from '../missing_credentials_callout'; +import { ExperimentalFeatureBanner } from './experimental_feature_banner'; +import { InitialSetupPanel } from './initial_setup_panel'; +import { IncorrectLicensePanel } from './incorrect_license_panel'; import { ChatHeader } from './chat_header'; import { ChatPromptEditor } from './chat_prompt_editor'; import { ChatTimeline } from './chat_timeline'; - -const containerClassName = css` - max-height: 100%; - max-width: 800px; -`; +import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; const timelineClassName = css` overflow-y: auto; @@ -40,6 +40,11 @@ const loadingSpinnerContainerClassName = css` align-self: center; `; +const incorrectLicenseContainer = css` + height: 100%; + padding: ${euiThemeVars.euiPanelPaddingModifiers.paddingMedium}; +`; + export function ChatBody({ title, loading, @@ -47,7 +52,10 @@ export function ChatBody({ connectors, knowledgeBase, connectorsManagementHref, + modelsManagementHref, + conversationId, currentUser, + startedFrom, onChatUpdate, onChatComplete, onSaveTitle, @@ -58,19 +66,25 @@ export function ChatBody({ connectors: UseGenAIConnectorsResult; knowledgeBase: UseKnowledgeBaseResult; connectorsManagementHref: string; + modelsManagementHref: string; conversationId?: string; currentUser?: Pick; + startedFrom?: StartedFrom; onChatUpdate: (messages: Message[]) => void; onChatComplete: (messages: Message[]) => void; onSaveTitle: (title: string) => void; }) { + const license = useLicense(); + const hasCorrectLicense = license?.hasAtLeast('enterprise'); + const chatService = useObservabilityAIAssistantChatService(); const timeline = useTimeline({ - messages, + chatService, connectors, currentUser, - chatService, + messages, + startedFrom, onChatUpdate, onChatComplete, }); @@ -83,61 +97,84 @@ export function ChatBody({ connectors.loading || knowledgeBase.status.loading || last(timeline.items)?.loading ); + const containerClassName = css` + max-height: 100%; + max-width: ${startedFrom === 'conversationView' + ? 1200 - 250 + 'px' // page template max width - conversation list width. + : '100%'}; + `; + + const [stickToBottom, setStickToBottom] = useState(true); + + const isAtBottom = (parent: HTMLElement) => + parent.scrollTop + parent.clientHeight >= parent.scrollHeight; + useEffect(() => { const parent = timelineContainerRef.current?.parentElement; if (!parent) { return; } - let rafId: number | undefined; - - const isAtBottom = () => parent.scrollTop >= parent.scrollHeight - parent.offsetHeight; - - const stick = () => { - if (!isAtBottom()) { - parent.scrollTop = parent.scrollHeight - parent.offsetHeight; - } - rafId = requestAnimationFrame(stick); - }; - - const unstick = () => { - if (rafId) { - cancelAnimationFrame(rafId); - rafId = undefined; - } - }; - - const onScroll = (event: Event) => { - if (isAtBottom()) { - stick(); - } else { - unstick(); - } - }; + function onScroll() { + setStickToBottom(isAtBottom(parent!)); + } parent.addEventListener('scroll', onScroll); - stick(); - return () => { - unstick(); parent.removeEventListener('scroll', onScroll); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [timelineContainerRef.current]); - if (connectors.loading || knowledgeBase.status.loading) { + useEffect(() => { + const parent = timelineContainerRef.current?.parentElement; + if (!parent) { + return; + } + + if (stickToBottom) { + parent.scrollTop = parent.scrollHeight; + } + }); + + const handleCopyConversation = () => { + const content = JSON.stringify({ title, messages }); + + navigator.clipboard?.writeText(content || ''); + }; + + if (!hasCorrectLicense && !conversationId) { + footer = ( + <> + + + + + + + + + + + + + + ); + } else if (connectors.loading || knowledgeBase.status.loading) { footer = ( ); - } else if (connectors.connectors?.length === 0) { + } else if (connectors.connectors?.length === 0 && !conversationId) { footer = ( - <> - - - + ); } else { footer = ( @@ -147,6 +184,7 @@ export function ChatBody({ { + setStickToBottom(true); + return timeline.onSubmit(message); + }} /> @@ -174,12 +215,24 @@ export function ChatBody({ return ( + {connectors.selectedConnector ? ( + + + + ) : null} + diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.stories.tsx index 835d49b7bde6a..30f56a6ab63a0 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.stories.tsx @@ -31,7 +31,11 @@ const defaultProps: ChatFlyoutProps = { isOpen: true, title: 'How is this working', messages: [getAssistantSetupMessage({ contexts: [] })], + startedFrom: 'appTopNavbar', onClose: () => {}, + onChatComplete: () => {}, + onChatTitleSave: () => {}, + onChatUpdate: () => {}, }; export const ChatFlyout = Template.bind({}); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx index 9a26c327a1eed..ef4635d873e8a 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx @@ -9,15 +9,14 @@ import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; import React from 'react'; import type { Message } from '../../../common/types'; -import { useAbortableAsync } from '../../hooks/use_abortable_async'; -import { useConversation } from '../../hooks/use_conversation'; 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 { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href'; +import { getModelsManagementHref } from '../../utils/get_models_management_href'; +import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; import { ChatBody } from './chat_body'; const containerClassName = css` @@ -33,17 +32,21 @@ export function ChatFlyout({ messages, conversationId, isOpen, + startedFrom, onClose, onChatUpdate, onChatComplete, + onChatTitleSave, }: { title: string; messages: Message[]; conversationId?: string; isOpen: boolean; + startedFrom: StartedFrom; onClose: () => void; - onChatUpdate?: (messages: Message[]) => void; - onChatComplete?: (messages: Message[]) => void; + onChatUpdate: (messages: Message[]) => void; + onChatComplete: (messages: Message[]) => void; + onChatTitleSave: (title: string) => void; }) { const { euiTheme } = useEuiTheme(); const { @@ -54,25 +57,10 @@ export function ChatFlyout({ const connectors = useGenAIConnectors(); - const service = useObservabilityAIAssistant(); - - const chatService = useAbortableAsync( - ({ signal }) => { - return service.start({ signal }); - }, - [service] - ); - const router = useObservabilityAIAssistantRouter(); const knowledgeBase = useKnowledgeBase(); - const { saveTitle } = useConversation({ - conversationId, - chatService: chatService.value, - connectorId: connectors.selectedConnector, - }); - return isOpen ? ( {conversationId ? ( ) : ( - + {i18n.translate('xpack.observabilityAiAssistant.conversationListDeepLinkLabel', { defaultMessage: 'Go to conversations', })} @@ -115,7 +107,10 @@ export function ChatFlyout({ messages={messages} currentUser={currentUser} connectorsManagementHref={getConnectorsManagementHref(http)} + modelsManagementHref={getModelsManagementHref(http)} + conversationId={conversationId} knowledgeBase={knowledgeBase} + startedFrom={startedFrom} onChatUpdate={(nextMessages) => { if (onChatUpdate) { onChatUpdate(nextMessages); @@ -127,7 +122,7 @@ export function ChatFlyout({ } }} onSaveTitle={(newTitle) => { - saveTitle(newTitle); + onChatTitleSave(newTitle); }} />
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx index ea410160408a9..0630f9c36d9dd 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx @@ -16,14 +16,14 @@ import { import { i18n } from '@kbn/i18n'; import { css } from '@emotion/css'; import { AssistantAvatar } from '../assistant_avatar'; -import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base'; -import { EMPTY_CONVERSATION_TITLE } from '../../i18n'; -import { KnowledgeBaseCallout } from './knowledge_base_callout'; -import { TechnicalPreviewBadge } from '../technical_preview_badge'; +import { ChatActionsMenu } from './chat_actions_menu'; +import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; import { useUnmountAndRemountWhenPropChanges } from '../../hooks/use_unmount_and_remount_when_prop_changes'; import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; +// needed to prevent InlineTextEdit component from expanding container const minWidthClassName = css` min-width: 0; `; @@ -31,19 +31,35 @@ const minWidthClassName = css` export function ChatHeader({ title, loading, - knowledgeBase, + licenseInvalid, connectors, + connectorsManagementHref, + modelsManagementHref, + conversationId, + knowledgeBase, + startedFrom, onSaveTitle, + onCopyConversation, }: { title: string; loading: boolean; - knowledgeBase: UseKnowledgeBaseResult; + licenseInvalid: boolean; connectors: UseGenAIConnectorsResult; + connectorsManagementHref: string; + modelsManagementHref: string; + conversationId?: string; + knowledgeBase: UseKnowledgeBaseResult; + startedFrom?: StartedFrom; + onCopyConversation: () => void; onSaveTitle?: (title: string) => void; }) { const hasTitle = !!title; - const displayedTitle = title || EMPTY_CONVERSATION_TITLE; + const displayedTitle = !connectors.selectedConnector + ? ASSISTANT_SETUP_TITLE + : licenseInvalid + ? UPGRADE_LICENSE_TITLE + : title || EMPTY_CONVERSATION_TITLE; const theme = useEuiTheme(); @@ -55,52 +71,41 @@ export function ChatHeader({ return ( - + - {loading ? : } + {loading ? : } + - - - - - {shouldRender ? ( - - ) : null} - - - - - - - - - - + {shouldRender ? ( + + ) : null} - + diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_actions.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_actions.tsx index 79240a03bb314..64585d2f52ecc 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_actions.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_actions.tsx @@ -53,6 +53,7 @@ export function ChatItemActions({ } )} color="text" + data-test-subj="observabilityAiAssistantChatItemActionsEditPromptButton" display={editing ? 'fill' : 'empty'} iconType="documentEdit" onClick={onToggleEdit} @@ -68,6 +69,7 @@ export function ChatItemActions({ } )} color="text" + data-test-subj="observabilityAiAssistantChatItemActionsInspectPromptButton" display={expanded ? 'fill' : 'empty'} iconType={expanded ? 'eyeClosed' : 'eye'} onClick={onToggleExpand} @@ -85,6 +87,7 @@ export function ChatItemActions({ } )} color="text" + data-test-subj="observabilityAiAssistantChatItemActionsCopyMessageButton" iconType="copyClipboard" display={isPopoverOpen === 'copy' ? 'fill' : 'empty'} onClick={() => { diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx index b3c5b6daca72a..7033e1b48c47c 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { EuiButtonEmpty, EuiButtonIcon, @@ -18,7 +19,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { CodeEditor } from '@kbn/kibana-react-plugin/public'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; import { MessageRole, type Message } from '../../../common'; import { useJsonEditorModel } from '../../hooks/use_json_editor_model'; import { FunctionListPopover } from './function_list_popover'; @@ -51,17 +51,21 @@ export function ChatPromptEditor({ const [functionPayload, setFunctionPayload] = useState( initialFunctionPayload ); + const [functionEditorLineCount, setFunctionEditorLineCount] = useState(0); const { model, initialJsonString } = useJsonEditorModel({ functionName: selectedFunctionName, - initialJson: initialFunctionPayload, + initialJson: functionPayload, }); const textAreaRef = useRef(null); - useEffect(() => { - setFunctionPayload(initialJsonString); - }, [initialJsonString, selectedFunctionName]); + const recalculateFunctionEditorLineCount = useCallback(() => { + const newLineCount = model?.getLineCount() || 0; + if (newLineCount !== functionEditorLineCount) { + setFunctionEditorLineCount(newLineCount); + } + }, [functionEditorLineCount, model]); const handleChange = (event: React.ChangeEvent) => { setPrompt(event.currentTarget.value); @@ -69,6 +73,7 @@ export function ChatPromptEditor({ const handleChangeFunctionPayload = (params: string) => { setFunctionPayload(params); + recalculateFunctionEditorLineCount(); }; const handleClearSelection = () => { @@ -91,7 +96,7 @@ export function ChatPromptEditor({ }; const handleSubmit = useCallback(async () => { - if (loading) { + if (loading || !prompt?.trim()) { return; } const currentPrompt = prompt; @@ -129,6 +134,14 @@ export function ChatPromptEditor({ } }, [functionPayload, loading, onSubmit, prompt, selectedFunctionName]); + useEffect(() => { + setFunctionPayload(initialJsonString); + }, [initialJsonString, selectedFunctionName]); + + useEffect(() => { + recalculateFunctionEditorLineCount(); + }, [model, recalculateFunctionEditorLineCount]); + useEffect(() => { const keyboardListener = (event: KeyboardEvent) => { if (!event.shiftKey && event.key === keys.ENTER && (prompt || selectedFunctionName)) { @@ -168,16 +181,17 @@ export function ChatPromptEditor({ {selectedFunctionName ? ( {i18n.translate('xpack.observabilityAiAssistant.prompt.emptySelection', { @@ -193,8 +207,9 @@ export function ChatPromptEditor({ 8 ? '200px' : '120px'} languageId="json" isCopyable languageConfiguration={{ @@ -238,6 +253,9 @@ export function ChatPromptEditor({ ) : ( = (props: ChatTimelineProps) => setCount(count >= 0 && count < props.items.length - 1 ? count + 1 : 0)} > Add message @@ -48,6 +49,18 @@ const Template: ComponentStory = (props: ChatTimelineProps) => }; const defaultProps: ComponentProps = { + knowledgeBase: { + status: { + loading: false, + value: { + ready: true, + }, + refresh: () => {}, + }, + isInstalling: false, + installError: undefined, + install: async () => {}, + }, items: [ buildChatInitItem(), buildUserChatItem(), diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx index 5cf6a098b45db..8e50f11842e5d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx @@ -11,8 +11,10 @@ import { compact } from 'lodash'; import { EuiCommentList } from '@elastic/eui'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { ChatItem } from './chat_item'; +import { ChatWelcomePanel } from './chat_welcome_panel'; import type { Feedback } from '../feedback_buttons'; import type { Message } from '../../../common'; +import { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; export interface ChatTimelineItem extends Pick { @@ -36,6 +38,7 @@ export interface ChatTimelineItem export interface ChatTimelineProps { items: ChatTimelineItem[]; + knowledgeBase: UseKnowledgeBaseResult; onEdit: (item: ChatTimelineItem, message: Message) => Promise; onFeedback: (item: ChatTimelineItem, feedback: Feedback) => void; onRegenerate: (item: ChatTimelineItem) => void; @@ -44,11 +47,14 @@ export interface ChatTimelineProps { export function ChatTimeline({ items = [], + knowledgeBase, onEdit, onFeedback, onRegenerate, onStopGenerating, }: ChatTimelineProps) { + const filteredItems = items.filter((item) => !item.display.hide); + return ( {compact( - items.map((item, index) => - !item.display.hide ? ( - { - onFeedback(item, feedback); - }} - onRegenerateClick={() => { - onRegenerate(item); - }} - onEditSubmit={(message) => { - return onEdit(item, message); - }} - onStopGeneratingClick={onStopGenerating} - /> - ) : null - ) + filteredItems.map((item, index) => ( + { + onFeedback(item, feedback); + }} + onRegenerateClick={() => { + onRegenerate(item); + }} + onEditSubmit={(message) => { + return onEdit(item, message); + }} + onStopGeneratingClick={onStopGenerating} + /> + )) )} + {filteredItems.length === 1 ? : null} ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_welcome_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_welcome_panel.tsx new file mode 100644 index 0000000000000..8240c6fef4054 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_welcome_panel.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiImage, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; +import { euiThemeVars } from '@kbn/ui-theme'; +import ctaImage from '../../assets/elastic_ai_assistant.png'; +import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; + +const incorrectLicenseContainer = css` + height: 100%; + padding: ${euiThemeVars.euiPanelPaddingModifiers.paddingMedium}; +`; + +export function ChatWelcomePanel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { + return ( + + + + +

+ {i18n.translate('xpack.observabilityAiAssistant.chatWelcomePanel.title', { + defaultMessage: "Let's work on this together", + })} +

+
+ +

+ {knowledgeBase.status.value?.ready + ? i18n.translate('xpack.observabilityAiAssistant.chatWelcomePanel.body.kbReady', { + defaultMessage: + 'Keep in mind that Elastic AI Assistant is a technical preview feature. Please provide feedback at any time.', + }) + : i18n.translate('xpack.observabilityAiAssistant.chatWelcomePanel.body.kbNotReady', { + defaultMessage: + 'We recommend you enable the knowledge base for a better experience. It will provide the assistant with the ability to learn from your interaction with it. Keep in mind that Elastic AI Assistant is a technical preview feature. Please provide feedback at any time.', + })} +

+
+ + {!knowledgeBase.status.value?.ready ? ( + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatWelcomePanel.knowledgeBase.buttonLabel.notInstalledYet', + { + defaultMessage: 'Set up knowledge base', + } + )} + + ) : null} +
+
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx index a230a49c5a9fb..08cb45387a9ef 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -18,7 +19,6 @@ import { } from '@elastic/eui'; import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; -import React from 'react'; import { NewChatButton } from '../buttons/new_chat_button'; const containerClassName = css` @@ -43,17 +43,16 @@ const newChatButtonWrapperClassName = css` export function ConversationList({ selected, - onClickNewChat, loading, error, conversations, + onClickNewChat, onClickDeleteConversation, }: { selected: string; loading: boolean; error?: any; conversations?: Array<{ id: string; label: string; href?: string }>; - onClickConversation: (conversationId: string) => void; onClickNewChat: () => void; onClickDeleteConversation: (id: string) => void; }) { diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/experimental_feature_banner.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/experimental_feature_banner.tsx new file mode 100644 index 0000000000000..8ec5346283365 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/experimental_feature_banner.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiImage, + EuiPanel, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import illustration from '../../assets/illustration.png'; + +export function ExperimentalFeatureBanner() { + return ( +
+ + + + + + + + Tech Preview }} + /> + + + + + {i18n.translate( + 'xpack.observabilityAiAssistant.experimentalFunctionBanner.feedbackButton', + { defaultMessage: 'Give feedback' } + )} + + + + + +
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx index f0684daece223..72dac17c71c2a 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx @@ -109,6 +109,7 @@ export function FunctionListPopover({ anchorPosition="downLeft" button={ + + + +

{UPGRADE_LICENSE_TITLE}

+
+ +

+ {i18n.translate('xpack.observabilityAiAssistant.incorrectLicense.body', { + defaultMessage: 'You need an Enterprise license to use the Elastic AI Assistant.', + })} +

+
+ + + + + {i18n.translate( + 'xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton', + { + defaultMessage: 'Subscription plans', + } + )} + + + + + {i18n.translate('xpack.observabilityAiAssistant.incorrectLicense.manageLicense', { + defaultMessage: 'Manage license', + })} + + + + +
+ + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/initial_setup_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/initial_setup_panel.tsx new file mode 100644 index 0000000000000..281cf46c972bb --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/initial_setup_panel.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + EuiBetaBadge, + EuiButton, + EuiCallOut, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base'; +import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import { ExperimentalFeatureBanner } from './experimental_feature_banner'; +import { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; + +export function InitialSetupPanel({ + connectors, + connectorsManagementHref, + knowledgeBase, + startedFrom, +}: { + connectors: UseGenAIConnectorsResult; + connectorsManagementHref: string; + knowledgeBase: UseKnowledgeBaseResult; + startedFrom?: StartedFrom; +}) { + return ( + <> + + + + + + +

+ {i18n.translate('xpack.observabilityAiAssistant.initialSetupPanel.title', { + defaultMessage: + 'Start your Al experience with Elastic by completing the steps below.', + })} +

+
+ + + + + + } + title={i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.title', + { + defaultMessage: 'Knowledge Base', + } + )} + description={ + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.description.paragraph1', + { + defaultMessage: + 'We recommend you enable the knowledge base for a better experience. It will provide the assistant with the ability to learn from your interaction with it.', + } + )} +

+

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.description.paragraph2', + { + defaultMessage: 'This step is optional, you can always do it later.', + } + )} +

+
+ } + footer={ + knowledgeBase.status.value?.ready ? ( + + ) : ( + + {knowledgeBase.isInstalling || knowledgeBase.status.loading + ? i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.buttonLabel.installingKb', + { + defaultMessage: 'Installing knowledge base', + } + ) + : i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.buttonLabel.kbNotInstalledYet', + { + defaultMessage: 'Set up knowledge base', + } + )} + + ) + } + /> +
+ + + } + title={i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.title', + { + defaultMessage: 'Connector setup', + } + )} + description={ + !connectors.connectors?.length ? ( + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description1', + { + defaultMessage: 'Set up a Generative AI connector with your AI provider.', + } + )} +

+ +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2', + { + defaultMessage: + 'The Generative AI model needs to support function calls. We strongly recommend using GPT4.', + } + )} + +

+
+ ) : connectors.connectors.length && !connectors.selectedConnector ? ( + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description', + { + defaultMessage: 'Please select a provider.', + } + )} +

+
+ ) : ( + '' + ) + } + footer={ + !connectors.connectors?.length ? ( + + {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel', + { + defaultMessage: 'Set up Generative AI connector', + } + )} + + ) : connectors.connectors.length && !connectors.selectedConnector ? ( + + ) : null + } + /> +
+
+ + + + +

+ {i18n.translate('xpack.observabilityAiAssistant.initialSetupPanel.disclaimer', { + defaultMessage: + 'The AI provider that is configured may collect telemetry when using the Elastic AI Assistant. Contact your AI provider for information on how data is collected.', + })} +

+
+
+ + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.tsx index e5eee16503235..36d6842286aa8 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.tsx @@ -89,6 +89,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow } else if (!knowledgeBase.status.value?.ready && !knowledgeBase.status.error) { content = ( { knowledgeBase.install(); }} @@ -111,7 +112,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow hasShadow={false} borderRadius="none" color={color} - paddingSize={knowledgeBase.status.value?.ready ? 'none' : 's'} + paddingSize="s" css={{ width: 'max-content' }} > {content} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx b/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx index 08b58a85bfadf..2b55e3d8cac35 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx @@ -32,6 +32,7 @@ export function FeedbackButtons({ onClickFeedback }: FeedbackButtonsProps) { (); + const [conversationId, setConversationId] = useState(); + + const { conversation, displayedMessages, setDisplayedMessages, save, saveTitle } = + useConversation({ + conversationId, + connectorId, + chatService, + }); + + const conversationTitle = conversationId + ? conversation.value?.conversation.title || '' + : defaultTitle; const reloadReply = useCallback(() => { setLoading(true); - const nextSubscription = chatService.chat({ messages, connectorId }).subscribe({ - next: (msg) => { - setPendingMessage(() => msg); - }, - complete: () => { - setLoading(false); - }, - }); + let lastPendingMessage: PendingMessage | undefined; + + const nextSubscription = chatService + .chat({ messages: initialMessages, connectorId, function: 'none' }) + .subscribe({ + next: (msg) => { + lastPendingMessage = msg; + setPendingMessage(() => msg); + }, + complete: () => { + setDisplayedMessages((prevMessages) => + prevMessages.concat({ + '@timestamp': new Date().toISOString(), + message: { + ...lastPendingMessage!.message, + }, + }) + ); + setLoading(false); + }, + }); setSubscription(nextSubscription); - }, [messages, connectorId, chatService]); + }, [initialMessages, setDisplayedMessages, connectorId, chatService]); useEffect(() => { reloadReply(); }, [reloadReply]); + useEffect(() => { + setDisplayedMessages(initialMessages); + }, [initialMessages, setDisplayedMessages]); + const [isOpen, setIsOpen] = useState(false); - const displayedMessages = useMemo(() => { + const messagesWithPending = useMemo(() => { return pendingMessage - ? messages.concat({ + ? displayedMessages.concat({ '@timestamp': new Date().toISOString(), message: { ...pendingMessage.message, }, }) - : messages; - }, [pendingMessage, messages]); + : displayedMessages; + }, [pendingMessage, displayedMessages]); + + const lastMessage = last(messagesWithPending); return ( <> } + body={} error={pendingMessage?.error} controls={ loading ? ( @@ -85,6 +118,14 @@ function ChatContent({ onClick={() => { subscription?.unsubscribe(); setLoading(false); + setDisplayedMessages((prevMessages) => + prevMessages.concat({ + '@timestamp': new Date().toISOString(), + message: { + ...pendingMessage!.message, + }, + }) + ); setPendingMessage((prev) => ({ message: { role: MessageRole.Assistant, @@ -116,12 +157,27 @@ function ChatContent({ } /> { setIsOpen(() => false); }} messages={displayedMessages} + conversationId={conversationId} + startedFrom="contextualInsight" + onChatComplete={(nextMessages) => { + save(nextMessages) + .then((nextConversation) => { + setConversationId(nextConversation.conversation.id); + }) + .catch(() => {}); + }} + onChatUpdate={(nextMessages) => { + setDisplayedMessages(nextMessages); + }} + onChatTitleSave={(newTitle) => { + saveTitle(newTitle); + }} /> ); @@ -149,7 +205,11 @@ export function Insight({ messages, title }: { messages: Message[]; title: strin if (hasOpened && connectors.selectedConnector) { children = ( - + ); } else if (!connectors.loading && !connectors.connectors?.length) { children = ( diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.tsx index ff22f15b7c948..b6c0b4db906b7 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.tsx @@ -21,7 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { AssistantAvatar } from '../assistant_avatar'; -import { TechnicalPreviewBadge } from '../technical_preview_badge'; +import { ExperimentalFeatureBanner } from '../chat/experimental_feature_banner'; export interface InsightBaseProps { title: string; @@ -95,6 +95,7 @@ export function InsightBase({ )} color="text" css={{ alignSelf: 'flex-start' }} + data-test-subj="observabilityAiAssistantInsightBaseButtonIcon" disabled={actions?.length === 0} display="empty" iconType="boxesHorizontal" @@ -117,15 +118,13 @@ export function InsightBase({ ) : null} - - - ) : null } onToggle={onToggle} > + {children} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx index c743185a5eb33..7542a2f843ef1 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx @@ -24,7 +24,11 @@ export function InsightError() { - + {i18n.translate('xpack.observabilityAiAssistant.insight.error.buttonLabel', { defaultMessage: 'Regenerate', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx index 6fd9a1c70923d..d82e76ef5001f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx @@ -16,7 +16,6 @@ import classNames from 'classnames'; import type { Code, InlineCode, Parent, Text } from 'mdast'; import React, { useMemo } from 'react'; import type { Node } from 'unist'; -import { v4 } from 'uuid'; interface Props { content: string; @@ -48,7 +47,10 @@ const cursorCss = css` const Cursor = () => ; -const CURSOR = `{{${v4()}}}`; +// a weird combination of different whitespace chars to make sure it stays +// invisible even when we cannot properly parse the text while still being +// unique +const CURSOR = ` ᠎  `; const loadingCursorPlugin = () => { const visitor = (node: Node, parent?: Parent) => { diff --git a/x-pack/plugins/observability_ai_assistant/public/components/missing_credentials_callout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/missing_credentials_callout.tsx index 20c8257b8721f..3cf5511a042e2 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/missing_credentials_callout.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/missing_credentials_callout.tsx @@ -29,7 +29,12 @@ export function MissingCredentialsCallout(props: Props) { - + {i18n.translate('xpack.observabilityAiAssistant.missingCredentialsCallout.buttonLabel', { defaultMessage: 'Connect Assistant', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/alerts.ts b/x-pack/plugins/observability_ai_assistant/public/functions/alerts.ts index 03ea9a055fcc7..b4c0d0bd1bdfd 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/alerts.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/alerts.ts @@ -58,11 +58,16 @@ export function registerAlertsFunction({ description: 'a KQL query to filter the data by. If no filter should be applied, leave it empty.', }, + includeRecovered: { + type: 'boolean', + description: + 'Whether to include recovered/closed alerts. Defaults to false, which means only active alerts will be returned', + }, }, required: ['start', 'end'], } as const, }, - ({ arguments: { start, end, featureIds, filter } }, signal) => { + ({ arguments: { start, end, featureIds, filter, includeRecovered } }, signal) => { return service.callApi('POST /internal/observability_ai_assistant/functions/alerts', { params: { body: { @@ -71,6 +76,7 @@ export function registerAlertsFunction({ featureIds: featureIds && featureIds.length > 0 ? featureIds : DEFAULT_FEATURE_IDS.concat(), filter, + includeRecovered, }, }, signal, diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/index.ts b/x-pack/plugins/observability_ai_assistant/public/functions/index.ts index f0a676a5c221d..a0c1a264bfb51 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/index.ts @@ -14,7 +14,7 @@ import { registerElasticsearchFunction } from './elasticsearch'; import { registerKibanaFunction } from './kibana'; import { registerLensFunction } from './lens'; import { registerRecallFunction } from './recall'; -import { registerSummarisationFunction } from './summarise'; +import { registerSummarizationFunction } from './summarize'; import { registerAlertsFunction } from './alerts'; export async function registerFunctions({ @@ -49,11 +49,14 @@ export async function registerFunctions({ In KQL, escaping happens with double quotes, not single quotes. Some characters that need escaping are: ':()\\\ /\". Always put a field value in double quotes. Best: service.name:\"opbeans-go\". Wrong: service.name:opbeans-go. This is very important! - You can use Github-flavored Markdown in your responses. If a function returns an array, consider using a Markdown table to format the response.` + You can use Github-flavored Markdown in your responses. If a function returns an array, consider using a Markdown table to format the response. + + If multiple functions are suitable, use the most specific and easy one. E.g., when the user asks to visualise APM data, use the APM functions (if available) rather than Lens. + ` ); if (isReady) { - description += `You can use the "summarise" functions to store new information you have learned in a knowledge database. Once you have established that you did not know the answer to a question, and the user gave you this information, it's important that you create a summarisation of what you have learned and store it in the knowledge database. + description += `You can use the "summarize" functions to store new information you have learned in a knowledge database. Once you have established that you did not know the answer to a question, and the user gave you this information, it's important that you create a summarisation of what you have learned and store it in the knowledge database. Don't create a new summarization if you see a similar summarization in the conversation, instead, update the existing one by re-using its ID. Additionally, you can use the "recall" function to retrieve relevant information from the knowledge database. `; @@ -64,9 +67,9 @@ export async function registerFunctions({ - ALWAYS query the knowledge base, using the recall function, when a user starts a chat, no matter how confident you are in your ability to answer the question. - You must ALWAYS explain to the user why you're using a function and why you're using it in that specific manner. - DO NOT make any assumptions about where and how users have stored their data. - - ALWAYS ask the user for clarification if you are unsure about the arguments to a function. When given this clarification, you MUST use the summarise function to store what you have learned. + - ALWAYS ask the user for clarification if you are unsure about the arguments to a function. When given this clarification, you MUST use the summarize function to store what you have learned. `; - registerSummarisationFunction({ service, registerFunction }); + registerSummarizationFunction({ service, registerFunction }); registerRecallFunction({ service, registerFunction }); registerLensFunction({ service, pluginsStart, registerFunction }); } else { diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/kibana.ts b/x-pack/plugins/observability_ai_assistant/public/functions/kibana.ts index 5ad877b2c2bff..a47acdb02d433 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/kibana.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/kibana.ts @@ -50,7 +50,7 @@ export function registerKibanaFunction({ description: 'The body of the request', }, }, - required: ['method', 'pathname', 'body'] as const, + required: ['method', 'pathname'] as const, }, }, ({ arguments: { method, pathname, body, query } }, signal) => { diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/lens.tsx b/x-pack/plugins/observability_ai_assistant/public/functions/lens.tsx index d5d2c715d0c52..efacbe07816e1 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/lens.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/functions/lens.tsx @@ -4,13 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types'; +import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import { LensAttributesBuilder, XYChart, XYDataLayer } from '@kbn/lens-embeddable-utils'; -import React from 'react'; +import type { LensEmbeddableInput, LensPublicStart } from '@kbn/lens-plugin/public'; +import React, { useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { Assign } from 'utility-types'; import type { RegisterFunctionDefinition } from '../../common/types'; -import { useKibana } from '../hooks/use_kibana'; import type { ObservabilityAIAssistantPluginStartDependencies, ObservabilityAIAssistantService, @@ -33,20 +36,16 @@ function Lens({ xyDataLayer, start, end, + lens, + dataViews, }: { indexPattern: string; xyDataLayer: XYDataLayer; start: string; end: string; + lens: LensPublicStart; + dataViews: DataViewsServicePublic; }) { - const { - services: { - plugins: { - start: { lens, dataViews }, - }, - }, - } = useKibana(); - const formulaAsync = useAsync(() => { return lens.stateHelperApi(); }, [lens]); @@ -59,6 +58,8 @@ function Lens({ }); }, [indexPattern]); + const [isSaveModalOpen, setIsSaveModalOpen] = useState(false); + if (!formulaAsync.value || !dataViewAsync.value) { return ; } @@ -71,19 +72,65 @@ function Lens({ }), }).build(); + const lensEmbeddableInput: Assign = { + id: indexPattern, + attributes, + timeRange: { + from: start, + to: end, + mode: 'relative' as const, + }, + }; + return ( - + <> + + + + + { + lens.navigateToPrefilledEditor(lensEmbeddableInput); + }} + > + {i18n.translate('xpack.observabilityAiAssistant.lensFunction.openInLens', { + defaultMessage: 'Open in Lens', + })} + + + + { + setIsSaveModalOpen(() => true); + }} + > + {i18n.translate('xpack.observabilityAiAssistant.lensFunction.save', { + defaultMessage: 'Save', + })} + + + + + + + + + {isSaveModalOpen ? ( + { + setIsSaveModalOpen(() => false); + }} + /> + ) : null} + ); } @@ -101,9 +148,9 @@ export function registerLensFunction({ name: 'lens', contexts: ['core'], description: - "Use this function to create custom visualisations, using Lens, that can be saved to dashboards. When using this function, make sure to use the recall function to get more information about how to use it, with how you want to use it. Make sure the query also contains information about the user's request. The visualisation is displayed to the user above your reply, DO NOT try to generate or display an image yourself.", + "Use this function to create custom visualizations, using Lens, that can be saved to dashboards. This function does not return data to the assistant, it only shows it to the user. When using this function, make sure to use the recall function to get more information about how to use it, with how you want to use it. Make sure the query also contains information about the user's request. The visualisation is displayed to the user above your reply, DO NOT try to generate or display an image yourself.", descriptionForUser: - 'Use this function to create custom visualisations, using Lens, that can be saved to dashboards.', + 'Use this function to create custom visualizations, using Lens, that can be saved to dashboards.', parameters: { type: 'object', additionalProperties: false, @@ -214,7 +261,16 @@ export function registerLensFunction({ }, }); - return ; + return ( + + ); } ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts b/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts index 5f1e14af30d15..f07cf43e1c282 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts @@ -6,7 +6,7 @@ */ import type { Serializable } from '@kbn/utility-types'; -import type { RegisterFunctionDefinition } from '../../common/types'; +import { MessageRole, RegisterFunctionDefinition } from '../../common/types'; import type { ObservabilityAIAssistantService } from '../types'; export function registerRecallFunction({ @@ -20,14 +20,17 @@ export function registerRecallFunction({ { name: 'recall', contexts: ['core'], - description: `Use this function to recall earlier learnings. Anything you will summarise can be retrieved again later via this function. The queries you use are very important, as they will decide the context that is included in the conversation. Make sure the query covers the following aspects: - - The user's intent - - Any data (like field names) mentioned in the user's request - - Anything you've inferred from the user's request - - The functions you think might be suitable for answering the user's request. If there are multiple functions that seem suitable, create multiple queries. Use the function name in the query. + description: `Use this function to recall earlier learnings. Anything you will summarize can be retrieved again later via this function. - For instance, when the user asks: "can you visualise the average request duration for opbeans-go over the last 7 days?", the queries could be: - - "visualise average request duration for APM service opbeans-go" + Make sure the query covers the following aspects: + - Anything you've inferred from the user's request, but is not mentioned in the user's request + - The functions you think might be suitable for answering the user's request. If there are multiple functions that seem suitable, create multiple queries. Use the function name in the query. + + DO NOT include the user's request. It will be added internally. + + The user asks: "can you visualise the average request duration for opbeans-go over the last 7 days?" + You recall: + - "APM service" - "lens function usage" - "get_apm_timeseries function usage"`, descriptionForUser: 'This function allows the assistant to recall previous learnings.', @@ -45,15 +48,21 @@ export function registerRecallFunction({ }, }, }, - required: ['queries' as const], - }, + required: ['queries'], + } as const, }, - ({ arguments: { queries } }, signal) => { + ({ arguments: { queries }, messages }, signal) => { + const userMessages = messages.filter((message) => message.message.role === MessageRole.User); + + const userPrompt = userMessages[userMessages.length - 1]?.message.content; + + const queriesWithUserPrompt = userPrompt ? [userPrompt, ...queries] : queries; + return service .callApi('POST /internal/observability_ai_assistant/functions/recall', { params: { body: { - queries, + queries: queriesWithUserPrompt, }, }, signal, diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts b/x-pack/plugins/observability_ai_assistant/public/functions/summarize.ts similarity index 93% rename from x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts rename to x-pack/plugins/observability_ai_assistant/public/functions/summarize.ts index 7bef0e6399c3a..14f637591e613 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/summarize.ts @@ -8,7 +8,7 @@ import type { RegisterFunctionDefinition } from '../../common/types'; import type { ObservabilityAIAssistantService } from '../types'; -export function registerSummarisationFunction({ +export function registerSummarizationFunction({ service, registerFunction, }: { @@ -17,12 +17,12 @@ export function registerSummarisationFunction({ }) { registerFunction( { - name: 'summarise', + name: 'summarize', contexts: ['core'], description: - "Use this function to summarise things learned from the conversation. You can score the learnings with a confidence metric, whether it is a correction on a previous learning. An embedding will be created that you can recall later with a semantic search. There is no need to ask the user for permission to store something you have learned, unless you do not feel confident. When you create this summarisation, make sure you craft it in a way that can be recalled with a semantic search later, and that it would have answered the user's original request.", + "Use this function to summarize things learned from the conversation. You can score the learnings with a confidence metric, whether it is a correction on a previous learning. An embedding will be created that you can recall later with a semantic search. There is no need to ask the user for permission to store something you have learned, unless you do not feel confident. When you create this summarisation, make sure you craft it in a way that can be recalled with a semantic search later, and that it would have answered the user's original request.", descriptionForUser: - 'This function allows the Elastic Assistant to summarise things from the conversation.', + 'This function allows the Elastic Assistant to summarize things from the conversation.', parameters: { type: 'object', additionalProperties: false, @@ -66,7 +66,7 @@ export function registerSummarisationFunction({ signal ) => { return service - .callApi('POST /internal/observability_ai_assistant/functions/summarise', { + .callApi('POST /internal/observability_ai_assistant/functions/summarize', { params: { body: { id, diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts index 466708baad8af..e6d1f81dabd2f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts @@ -55,6 +55,8 @@ export const useJsonEditorModel = ({ if (model === null) { model = editor.createModel(initialJsonString, 'json', modelUri); + } else { + model.setValue(initialJsonString); } return { model, initialJsonString }; diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_license.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_license.ts new file mode 100644 index 0000000000000..c28e7da132569 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_license.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { Observable } from 'rxjs'; +import useObservable from 'react-use/lib/useObservable'; +import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public'; +import { useObservabilityAIAssistant } from './use_observability_ai_assistant'; + +interface UseLicenseReturnValue { + getLicense: () => ILicense | null; + hasAtLeast: (level: LicenseType) => boolean | undefined; +} + +export const useLicense = (): UseLicenseReturnValue => { + const service = useObservabilityAIAssistant(); + + const license = useObservable(service.getLicense() ?? new Observable(), null); + + return { + getLicense: () => license, + hasAtLeast: useCallback( + (level: LicenseType) => { + if (!license) return; + + return !!license && license.isAvailable && license.isActive && license.hasAtLeast(level); + }, + [license] + ), + }; +}; diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_license_management_locator.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_license_management_locator.ts new file mode 100644 index 0000000000000..fe6c636e5286e --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_license_management_locator.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useObservabilityAIAssistant } from './use_observability_ai_assistant'; + +const LICENSE_MANAGEMENT_LOCATOR = 'LICENSE_MANAGEMENT_LOCATOR'; + +export const useLicenseManagementLocator = () => { + const service = useObservabilityAIAssistant(); + + const locators = service.getLicenseManagementLocator(); + + const locator = locators.url.locators.get(LICENSE_MANAGEMENT_LOCATOR); + + // license management does not exist on serverless + if (!locator) return; + + return () => + locator.navigate({ + page: 'dashboard', + }); +}; diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.test.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.test.ts index 3284cdf66e99f..829594b8261d8 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.test.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.test.ts @@ -434,11 +434,20 @@ describe('useTimeline', () => { expect(props.onChatComplete).not.toHaveBeenCalled(); - expect(props.chatService.executeFunction).toHaveBeenCalledWith( - 'my_function', - '{}', - expect.any(Object) - ); + expect(props.chatService.executeFunction).toHaveBeenCalledWith({ + name: 'my_function', + args: '{}', + messages: [ + { + '@timestamp': expect.any(String), + message: { + content: 'Hello', + role: 'user', + }, + }, + ], + signal: expect.any(Object), + }); act(() => { subject.next({ diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts index c24f87a60912b..3182fe5c19535 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts @@ -10,6 +10,7 @@ import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { last } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; import type { Subscription } from 'rxjs'; +import usePrevious from 'react-use/lib/usePrevious'; import { i18n } from '@kbn/i18n'; import { ContextDefinition, @@ -22,7 +23,10 @@ import type { ChatTimelineProps } from '../components/chat/chat_timeline'; import { EMPTY_CONVERSATION_TITLE } from '../i18n'; import { getAssistantSetupMessage } from '../service/get_assistant_setup_message'; import type { ObservabilityAIAssistantChatService, PendingMessage } from '../types'; -import { getTimelineItemsfromConversation } from '../utils/get_timeline_items_from_conversation'; +import { + getTimelineItemsfromConversation, + StartedFrom, +} from '../utils/get_timeline_items_from_conversation'; import type { UseGenAIConnectorsResult } from './use_genai_connectors'; import { useKibana } from './use_kibana'; @@ -52,15 +56,19 @@ export type UseTimelineResult = Pick< export function useTimeline({ messages, connectors, + conversationId, currentUser, chatService, + startedFrom, onChatUpdate, onChatComplete, }: { messages: Message[]; + conversationId?: string; connectors: UseGenAIConnectorsResult; currentUser?: Pick; chatService: ObservabilityAIAssistantChatService; + startedFrom?: StartedFrom; onChatUpdate: (messages: Message[]) => void; onChatComplete: (messages: Message[]) => void; }): UseTimelineResult { @@ -74,14 +82,15 @@ export function useTimeline({ const conversationItems = useMemo(() => { const items = getTimelineItemsfromConversation({ - messages, currentUser, - hasConnector, chatService, + hasConnector, + messages, + startedFrom, }); return items; - }, [messages, currentUser, hasConnector, chatService]); + }, [currentUser, chatService, hasConnector, messages, startedFrom]); const [subscription, setSubscription] = useState(); @@ -89,6 +98,13 @@ export function useTimeline({ const [pendingMessage, setPendingMessage] = useState(); + const prevConversationId = usePrevious(conversationId); + useEffect(() => { + if (prevConversationId !== conversationId && pendingMessage?.error) { + setPendingMessage(undefined); + } + }, [conversationId, pendingMessage?.error, prevConversationId]); + function chat(nextMessages: Message[]): Promise { const controller = new AbortController(); @@ -122,8 +138,10 @@ export function useTimeline({ }, error: reject, complete: () => { - if (pendingMessageLocal?.error) { - notifications.toasts.addError(pendingMessageLocal?.error, { + const error = pendingMessageLocal?.error; + + if (error) { + notifications.toasts.addError(error, { title: i18n.translate('xpack.observabilityAiAssistant.failedToLoadResponse', { defaultMessage: 'Failed to load response from the AI Assistant', }), @@ -164,11 +182,12 @@ export function useTimeline({ const name = lastMessage.message.function_call.name; try { - const message = await chatService!.executeFunction( + const message = await chatService!.executeFunction({ name, - lastMessage.message.function_call.arguments, - controller.signal - ); + args: lastMessage.message.function_call.arguments, + messages: messagesAfterChat.slice(0, -1), + signal: controller.signal, + }); return await chat( messagesAfterChat.concat({ @@ -190,7 +209,7 @@ export function useTimeline({ name, content: JSON.stringify({ message: error.toString(), - error: error.body, + error, }), }, }) diff --git a/x-pack/plugins/observability_ai_assistant/public/i18n.ts b/x-pack/plugins/observability_ai_assistant/public/i18n.ts index 393d4ae7e8f10..dcc28d7ff531a 100644 --- a/x-pack/plugins/observability_ai_assistant/public/i18n.ts +++ b/x-pack/plugins/observability_ai_assistant/public/i18n.ts @@ -7,7 +7,21 @@ 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_ai_assistant/public/plugin.tsx b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx index 965a97e4a0a87..6e9eb6dc73196 100644 --- a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx @@ -97,6 +97,8 @@ export class ObservabilityAIAssistantPlugin const service = (this.service = createService({ coreStart, securityStart: pluginsStart.security, + licenseStart: pluginsStart.licensing, + shareStart: pluginsStart.share, enabled: coreStart.application.capabilities.observabilityAIAssistant.show === true, })); diff --git a/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx b/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx index 557b04bb1fa88..5af2e740deb9b 100644 --- a/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx @@ -23,6 +23,7 @@ import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_as import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href'; +import { getModelsManagementHref } from '../../utils/get_models_management_href'; import { EMPTY_CONVERSATION_TITLE } from '../../i18n'; const containerClassName = css` @@ -134,14 +135,6 @@ export function ConversationView() { loading={conversations.loading || isUpdatingList} error={conversations.error} conversations={displayedConversations} - onClickConversation={(nextConversationId) => { - observabilityAIAssistantRouter.push('/conversations/{conversationId}', { - path: { - conversationId: nextConversationId, - }, - query: {}, - }); - }} onClickNewChat={() => { observabilityAIAssistantRouter.push('/conversations/new', { path: {}, @@ -238,10 +231,12 @@ export function ConversationView() { currentUser={currentUser} connectors={connectors} connectorsManagementHref={getConnectorsManagementHref(http)} + modelsManagementHref={getModelsManagementHref(http)} conversationId={conversationId} knowledgeBase={knowledgeBase} messages={displayedMessages} title={conversation.value.conversation.title} + startedFrom="conversationView" onChatUpdate={(messages) => { setDisplayedMessages(messages); }} diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts index b857c4daeea03..ba7f5e2216bc8 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts @@ -156,6 +156,26 @@ describe('createChatService', () => { }); }); + it('propagates content errors', async () => { + respondWithChunks({ + chunks: [ + `data: {"error":{"message":"The server had an error while processing your request. Sorry about that!","type":"server_error","param":null,"code":null}}`, + ], + }); + + const response$ = chat(); + + const value = await lastValueFrom(response$); + + expect(value).toEqual({ + aborted: false, + error: expect.any(Error), + message: { + role: 'assistant', + }, + }); + }); + it('cancels a running http request when aborted', async () => { clientSpy.mockImplementationOnce((endpoint: string, options: HttpFetchOptions) => { options.signal?.addEventListener('abort', () => { diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts index 3a0a4b0bbe9b1..1c109be6baaf2 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts @@ -4,31 +4,33 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +/* eslint-disable max-classes-per-file*/ +import { Validator, type Schema, type OutputUnit } from '@cfworker/json-schema'; +import { HttpResponse } from '@kbn/core/public'; +import { AbortError } from '@kbn/kibana-utils-plugin/common'; import { IncomingMessage } from 'http'; import { cloneDeep, pick } from 'lodash'; import { BehaviorSubject, - map, - filter as rxJsFilter, - scan, catchError, - of, concatMap, - shareReplay, - finalize, delay, + filter as rxJsFilter, + finalize, + map, + of, + scan, + shareReplay, tap, } from 'rxjs'; -import { HttpResponse } from '@kbn/core/public'; -import { AbortError } from '@kbn/kibana-utils-plugin/common'; import { - type RegisterContextDefinition, - type RegisterFunctionDefinition, - Message, - MessageRole, ContextRegistry, FunctionRegistry, + Message, + MessageRole, + type RegisterContextDefinition, + type RegisterFunctionDefinition, } from '../../common/types'; import { ObservabilityAIAssistantAPIClient } from '../api'; import type { @@ -45,6 +47,14 @@ class TokenLimitReachedError extends Error { } } +class ServerError extends Error {} + +export class FunctionArgsValidationError extends Error { + constructor(public readonly errors: OutputUnit[]) { + super('Function arguments are invalid'); + } +} + export async function createChatService({ signal: setupAbortSignal, registrations, @@ -57,11 +67,14 @@ export async function createChatService({ const contextRegistry: ContextRegistry = new Map(); const functionRegistry: FunctionRegistry = new Map(); + const validators = new Map(); + const registerContext: RegisterContextDefinition = (context) => { contextRegistry.set(context.name, context); }; const registerFunction: RegisterFunctionDefinition = (def, respond, render) => { + validators.set(def.name, new Validator(def.parameters as Schema, '2020-12', true)); functionRegistry.set(def.name, { options: def, respond, render }); }; @@ -90,8 +103,16 @@ export async function createChatService({ registrations.map((fn) => fn({ signal: setupAbortSignal, registerContext, registerFunction })) ); + function validate(name: string, parameters: unknown) { + const validator = validators.get(name)!; + const result = validator.validate(parameters); + if (!result.valid) { + throw new FunctionArgsValidationError(result.errors); + } + } + return { - executeFunction: async (name, args, signal) => { + executeFunction: async ({ name, args, signal, messages }) => { const fn = functionRegistry.get(name); if (!fn) { @@ -100,9 +121,9 @@ export async function createChatService({ const parsedArguments = args ? JSON.parse(args) : {}; - // validate + validate(name, parsedArguments); - return await fn.respond({ arguments: parsedArguments }, signal); + return await fn.respond({ arguments: parsedArguments, messages }, signal); }, renderFunction: (name, args, response) => { const fn = functionRegistry.get(name); @@ -118,8 +139,6 @@ export async function createChatService({ data: JSON.parse(response.data ?? '{}'), }; - // validate - return fn.render?.({ response: parsedResponse, arguments: parsedArguments }); }, getContexts, @@ -127,7 +146,15 @@ export async function createChatService({ hasRenderFunction: (name: string) => { return !!getFunctions().find((fn) => fn.options.name === name)?.render; }, - chat({ connectorId, messages }: { connectorId: string; messages: Message[] }) { + chat({ + connectorId, + messages, + function: callFunctions = 'auto', + }: { + connectorId: string; + messages: Message[]; + function?: 'none' | 'auto'; + }) { const subject = new BehaviorSubject({ message: { role: MessageRole.Assistant, @@ -145,7 +172,10 @@ export async function createChatService({ body: { messages, connectorId, - functions: functions.map((fn) => pick(fn.options, 'name', 'description', 'parameters')), + functions: + callFunctions === 'none' + ? [] + : functions.map((fn) => pick(fn.options, 'name', 'description', 'parameters')), }, }, signal: controller.signal, @@ -171,10 +201,23 @@ export async function createChatService({ .pipe( map((line) => line.substring(6)), rxJsFilter((line) => !!line && line !== '[DONE]'), - map((line) => JSON.parse(line) as CreateChatCompletionResponseChunk), - rxJsFilter((line) => line.object === 'chat.completion.chunk'), - tap((choice) => { - if (choice.choices[0].finish_reason === 'length') { + map( + (line) => + JSON.parse(line) as + | CreateChatCompletionResponseChunk + | { error: { message: string } } + ), + tap((line) => { + if ('error' in line) { + throw new ServerError(line.error.message); + } + }), + rxJsFilter( + (line): line is CreateChatCompletionResponseChunk => + 'object' in line && line.object === 'chat.completion.chunk' + ), + tap((line) => { + if (line.choices[0].finish_reason === 'length') { throw new TokenLimitReachedError(); } }), @@ -217,6 +260,16 @@ export async function createChatService({ subject.complete(); }); }) + .catch(async (err) => { + if ('response' in err) { + const body = await (err.response as HttpResponse['response'])?.json(); + err.body = body; + if (body.message) { + err.message = body.message; + } + } + throw err; + }) .catch((err) => { subject.next({ ...subject.value, diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts index 0978457ffe28c..5e7356250d515 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -6,19 +6,24 @@ */ import type { CoreStart } from '@kbn/core/public'; -import { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import { createCallObservabilityAIAssistantAPI } from '../api'; import type { ChatRegistrationFunction, ObservabilityAIAssistantService } from '../types'; -import { createChatService } from './create_chat_service'; export function createService({ coreStart, - securityStart, enabled, + licenseStart, + securityStart, + shareStart, }: { coreStart: CoreStart; - securityStart: SecurityPluginStart; enabled: boolean; + licenseStart: LicensingPluginStart; + securityStart: SecurityPluginStart; + shareStart: SharePluginStart; }): ObservabilityAIAssistantService & { register: (fn: ChatRegistrationFunction) => void } { const client = createCallObservabilityAIAssistantAPI(coreStart); @@ -32,10 +37,13 @@ export function createService({ registrations.push(fn); }, start: async ({ signal }) => { - return await createChatService({ client, signal, registrations }); + const mod = await import('./create_chat_service'); + return await mod.createChatService({ client, signal, registrations }); }, callApi: client, getCurrentUser: () => securityStart.authc.getCurrentUser(), + getLicense: () => licenseStart.license$, + getLicenseManagementLocator: () => shareStart, }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 3d16d6fbcd7a8..d1a71f6933126 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -29,6 +29,8 @@ import type { DataViewsPublicPluginSetup, DataViewsPublicPluginStart, } from '@kbn/data-views-plugin/public'; +import type { LicensingPluginStart, ILicense } from '@kbn/licensing-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { ContextDefinition, FunctionDefinition, @@ -55,15 +57,20 @@ export interface PendingMessage { } export interface ObservabilityAIAssistantChatService { - chat: (options: { messages: Message[]; connectorId: string }) => Observable; + chat: (options: { + messages: Message[]; + connectorId: string; + function?: 'none' | 'auto'; + }) => Observable; getContexts: () => ContextDefinition[]; getFunctions: (options?: { contexts?: string[]; filter?: string }) => FunctionDefinition[]; hasRenderFunction: (name: string) => boolean; - executeFunction: ( - name: string, - args: string | undefined, - signal: AbortSignal - ) => Promise<{ content?: Serializable; data?: Serializable }>; + executeFunction: ({}: { + name: string; + args: string | undefined; + messages: Message[]; + signal: AbortSignal; + }) => Promise<{ content?: Serializable; data?: Serializable }>; renderFunction: ( name: string, args: string | undefined, @@ -81,6 +88,8 @@ export interface ObservabilityAIAssistantService { isEnabled: () => boolean; callApi: ObservabilityAIAssistantAPIClient; getCurrentUser: () => Promise; + getLicense: () => Observable; + getLicenseManagementLocator: () => SharePluginStart; start: ({}: { signal: AbortSignal }) => Promise; } @@ -90,20 +99,22 @@ export interface ObservabilityAIAssistantPluginStart extends ObservabilityAIAssi export interface ObservabilityAIAssistantPluginSetup {} export interface ObservabilityAIAssistantPluginSetupDependencies { - triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; - security: SecurityPluginSetup; + dataViews: DataViewsPublicPluginSetup; features: FeaturesPluginSetup; - observabilityShared: ObservabilitySharedPluginSetup; lens: LensPublicSetup; - dataViews: DataViewsPublicPluginSetup; + observabilityShared: ObservabilitySharedPluginSetup; + security: SecurityPluginSetup; + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; } export interface ObservabilityAIAssistantPluginStartDependencies { - security: SecurityPluginStart; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; - observabilityShared: ObservabilitySharedPluginStart; + dataViews: DataViewsPublicPluginStart; features: FeaturesPluginStart; lens: LensPublicStart; - dataViews: DataViewsPublicPluginStart; + licensing: LicensingPluginStart; + observabilityShared: ObservabilitySharedPluginStart; + security: SecurityPluginStart; + share: SharePluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } export interface ConfigSchema {} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_models_management_href.ts b/x-pack/plugins/observability_ai_assistant/public/utils/get_models_management_href.ts new file mode 100644 index 0000000000000..8ff585e005f95 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_models_management_href.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpStart } from '@kbn/core/public'; + +export function getModelsManagementHref(http: HttpStart) { + return http!.basePath.prepend(`/app/ml/trained_models`); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx index e8aad76b7abda..6d8d8ed8fee13 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx @@ -53,16 +53,20 @@ function FunctionName({ name: functionName }: { name: string }) { return {functionName}; } +export type StartedFrom = 'contextualInsight' | 'appTopNavbar' | 'conversationView'; + export function getTimelineItemsfromConversation({ + chatService, currentUser, - messages, hasConnector, - chatService, + messages, + startedFrom, }: { + chatService: ObservabilityAIAssistantChatService; currentUser?: Pick; - messages: Message[]; hasConnector: boolean; - chatService: ObservabilityAIAssistantChatService; + messages: Message[]; + startedFrom?: StartedFrom; }): ChatTimelineItem[] { return [ { @@ -123,7 +127,8 @@ export function getTimelineItemsfromConversation({ parsedContent = message.message.content; } - const isError = typeof parsedContent === 'object' && 'error' in parsedContent; + const isError = + parsedContent && typeof parsedContent === 'object' && 'error' in parsedContent; title = !isError ? ( el.message.role === MessageRole.User + ); + + display.collapsed = index === firstUserMessageIndex; + } } break; diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx index 860bec95f69f5..3c4dc569ab5e5 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx @@ -9,6 +9,7 @@ import { Observable } from 'rxjs'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { Serializable } from '@kbn/utility-types'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import { ObservabilityAIAssistantProvider } from '../context/observability_ai_assistant_provider'; import { ObservabilityAIAssistantAPIClient } from '../api'; import type { Message } from '../../common'; @@ -24,11 +25,12 @@ const chatService: ObservabilityAIAssistantChatService = { chat: (options: { messages: Message[]; connectorId: string }) => new Observable(), getContexts: () => [], getFunctions: () => [buildFunctionElasticsearch(), buildFunctionServiceSummary()], - executeFunction: async ( - name: string, - args: string | undefined, - signal: AbortSignal - ): Promise<{ content?: Serializable; data?: Serializable }> => ({}), + executeFunction: async ({}: { + name: string; + args: string | undefined; + messages: Message[]; + signal: AbortSignal; + }): Promise<{ content?: Serializable; data?: Serializable }> => ({}), renderFunction: (name: string, args: string | undefined, response: {}) => (
Hello! {name}
), @@ -51,6 +53,12 @@ const service: ObservabilityAIAssistantService = { authentication_type: '', elastic_cloud_user: false, }), + getLicense: () => new Observable(), + getLicenseManagementLocator: () => + ({ + url: {}, + navigate: () => {}, + } as unknown as SharePluginStart), }; export function KibanaReactStorybookDecorator(Story: ComponentType) { diff --git a/x-pack/plugins/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_ai_assistant/server/plugin.ts index 01bd4fafb71c7..7aa6ddeeef7da 100644 --- a/x-pack/plugins/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_ai_assistant/server/plugin.ts @@ -62,9 +62,6 @@ export class ObservabilityAIAssistantPlugin category: DEFAULT_APP_CATEGORIES.observability, app: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID, 'kibana'], catalogue: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID], - management: { - insightsAndAlerting: ['triggersActionsConnectors'], - }, minimumLicense: 'enterprise', // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { @@ -80,9 +77,6 @@ export class ObservabilityAIAssistantPlugin ], read: [], }, - management: { - insightsAndAlerting: ['triggersActionsConnectors'], - }, ui: ['show'], }, read: { diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts index 0ed8c93a4fba8..6716da98d78a5 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -4,12 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as t from 'io-ts'; -import { IncomingMessage } from 'http'; import { notImplemented } from '@hapi/boom'; +import { IncomingMessage } from 'http'; +import * as t from 'io-ts'; +import { MessageRole } from '../../../common'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; import { messageRt } from '../runtime_types'; -import { MessageRole } from '../../../common'; const chatRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/chat', @@ -47,11 +47,17 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ const isRecallFunctionAvailable = functions.some((fn) => fn.name === 'recall') === true; + const willUseRecall = isStartOfConversation && isRecallFunctionAvailable; + return client.chat({ messages, connectorId, - functions, - functionCall: isStartOfConversation && isRecallFunctionAvailable ? 'recall' : undefined, + ...(functions.length + ? { + functions, + functionCall: willUseRecall ? 'recall' : undefined, + } + : {}), }); }, }); diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts index f88d6c56efe5b..d58c84a603fc1 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts @@ -11,6 +11,10 @@ import { nonEmptyStringRt, toBooleanRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { omit } from 'lodash'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import { + ALERT_STATUS, + ALERT_STATUS_ACTIVE, +} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; import type { KnowledgeBaseEntry } from '../../../common/types'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; @@ -79,6 +83,7 @@ const functionAlertsRoute = createObservabilityAIAssistantServerRoute({ }), t.partial({ filter: t.string, + includeRecovered: toBooleanRt, }), ]), }), @@ -95,6 +100,7 @@ const functionAlertsRoute = createObservabilityAIAssistantServerRoute({ start: startAsDatemath, end: endAsDatemath, filter, + includeRecovered, } = resources.params.body; const racContext = await resources.context.rac; @@ -120,6 +126,15 @@ const functionAlertsRoute = createObservabilityAIAssistantServerRoute({ }, }, ...kqlQuery, + ...(!includeRecovered + ? [ + { + term: { + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + }, + }, + ] + : []), ], }, }, @@ -163,7 +178,7 @@ const functionRecallRoute = createObservabilityAIAssistantServerRoute({ }); const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ - endpoint: 'POST /internal/observability_ai_assistant/functions/summarise', + endpoint: 'POST /internal/observability_ai_assistant/functions/summarize', params: t.type({ body: t.type({ id: t.string, @@ -193,7 +208,7 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ labels, } = resources.params.body; - return client.summarise({ + return client.summarize({ entry: { confidence, id, diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts index dd256c4784831..ce3f3c39ad1a9 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -246,9 +246,17 @@ export class ObservabilityAIAssistantClient { }); if ('object' in response && response.object === 'chat.completion') { - const title = - response.choices[0].message?.content?.slice(1, -1) || - `Conversation on ${conversation['@timestamp']}`; + const input = + response.choices[0].message?.content || `Conversation on ${conversation['@timestamp']}`; + + // This regular expression captures a string enclosed in single or double quotes. + // It extracts the string content without the quotes. + // Example matches: + // - "Hello, World!" => Captures: Hello, World! + // - 'Another Example' => Captures: Another Example + // - JustTextWithoutQuotes => Captures: JustTextWithoutQuotes + const match = input.match(/^["']?([^"']+)["']?$/); + const title = match ? match[1] : input; const updatedConversation: Conversation = merge( {}, @@ -325,12 +333,12 @@ export class ObservabilityAIAssistantClient { }); }; - summarise = async ({ + summarize = async ({ entry, }: { entry: Omit; }): Promise => { - return this.dependencies.knowledgeBaseService.summarise({ + return this.dependencies.knowledgeBaseService.summarize({ namespace: this.dependencies.namespace, user: this.dependencies.user, entry, diff --git a/x-pack/plugins/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/index.ts index e3a0eb9f15469..28cc2b7293029 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/index.ts @@ -7,7 +7,7 @@ import * as Boom from '@hapi/boom'; import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server/plugin'; -import { createConcreteWriteIndex } from '@kbn/alerting-plugin/server'; +import { createConcreteWriteIndex, getDataStreamAdapter } from '@kbn/alerting-plugin/server'; import type { CoreSetup, CoreStart, KibanaRequest, Logger } from '@kbn/core/server'; import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import { getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; @@ -147,6 +147,7 @@ export class ObservabilityAIAssistantService { name: `${conversationAliasName}-000001`, template: this.resourceNames.indexTemplate.conversations, }, + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts: false }), }); await esClient.cluster.putComponentTemplate({ @@ -203,6 +204,7 @@ export class ObservabilityAIAssistantService { name: `${kbAliasName}-000001`, template: this.resourceNames.indexTemplate.kb, }, + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts: false }), }); this.kbService = new KnowledgeBaseService({ diff --git a/x-pack/plugins/observability_ai_assistant/server/service/kb_service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/kb_service/index.ts index eab3a0a2fbe57..f2e0b41f092c9 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/kb_service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/kb_service/index.ts @@ -112,7 +112,7 @@ export class KnowledgeBaseService { return; } - await this.summarise({ + await this.summarize({ entry: operation.document, }); } @@ -223,7 +223,7 @@ export class KnowledgeBaseService { } }; - summarise = async ({ + summarize = async ({ entry: { id, ...document }, user, namespace, diff --git a/x-pack/plugins/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_ai_assistant/tsconfig.json index ddbd38c21bc5d..afdc9a4a89243 100644 --- a/x-pack/plugins/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_ai_assistant/tsconfig.json @@ -44,7 +44,9 @@ "@kbn/data-views-plugin", "@kbn/task-manager-plugin", "@kbn/es-query", - "@kbn/rule-registry-plugin" + "@kbn/rule-registry-plugin", + "@kbn/licensing-plugin", + "@kbn/share-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_log_explorer/.storybook/__mocks__/package_icon.tsx b/x-pack/plugins/observability_log_explorer/.storybook/__mocks__/package_icon.tsx new file mode 100644 index 0000000000000..2bb5c6c7614aa --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/.storybook/__mocks__/package_icon.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiIcon } from '@elastic/eui'; + +// Export mock package icon that doesn't trigger http requests +export const PackageIcon = () => ; diff --git a/x-pack/plugins/observability_log_explorer/.storybook/main.js b/x-pack/plugins/observability_log_explorer/.storybook/main.js new file mode 100644 index 0000000000000..79b438e7eb7ce --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/.storybook/main.js @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const defaultConfig = require('@kbn/storybook').defaultConfig; + +module.exports = { + ...defaultConfig, + stories: ['../**/*.stories.mdx', ...defaultConfig.stories], + webpackFinal: async (config) => { + const originalConfig = await defaultConfig.webpackFinal(config); + + // Mock fleet plugin for PackageIcon component + originalConfig.resolve.alias['@kbn/fleet-plugin/public'] = require.resolve( + './__mocks__/package_icon' + ); + return originalConfig; + }, +}; diff --git a/x-pack/plugins/observability_log_explorer/.storybook/preview.js b/x-pack/plugins/observability_log_explorer/.storybook/preview.js new file mode 100644 index 0000000000000..33a07bd3fac1f --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/.storybook/preview.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const parameters = { + docs: { + source: { + type: 'code', // without this, stories in mdx documents freeze the browser + }, + }, +}; diff --git a/x-pack/plugins/observability_log_explorer/README.md b/x-pack/plugins/observability_log_explorer/README.md new file mode 100644 index 0000000000000..604b33dd2b288 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/README.md @@ -0,0 +1,3 @@ +# Observability Log Explorer + +This plugin provides an app based on the `LogExplorer` component from the `log_explorer` plugin, but adds observability-specific affordances. diff --git a/x-pack/plugins/discover_log_explorer/common/plugin_config.ts b/x-pack/plugins/observability_log_explorer/common/plugin_config.ts similarity index 73% rename from x-pack/plugins/discover_log_explorer/common/plugin_config.ts rename to x-pack/plugins/observability_log_explorer/common/plugin_config.ts index ca8a3dab2a182..6d467f4236c3e 100644 --- a/x-pack/plugins/discover_log_explorer/common/plugin_config.ts +++ b/x-pack/plugins/observability_log_explorer/common/plugin_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -export interface DiscoverLogExplorerConfig { - featureFlags: { - deepLinkVisible: boolean; +export interface ObservabilityLogExplorerConfig { + navigation: { + showAppLink: boolean; }; } diff --git a/x-pack/plugins/observability_log_explorer/common/translations.ts b/x-pack/plugins/observability_log_explorer/common/translations.ts new file mode 100644 index 0000000000000..5ec1940fa8dff --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/translations.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const logExplorerAppTitle = i18n.translate('xpack.observabilityLogExplorer.appTitle', { + defaultMessage: 'Log Explorer', +}); + +export const betaBadgeTitle = i18n.translate('xpack.observabilityLogExplorer.betaBadgeTitle', { + defaultMessage: 'Beta', +}); + +export const betaBadgeDescription = i18n.translate( + 'xpack.observabilityLogExplorer.betaBadgeDescription', + { + defaultMessage: 'This application is in beta and therefore subject to change.', + } +); diff --git a/x-pack/plugins/observability_log_explorer/jest.config.js b/x-pack/plugins/observability_log_explorer/jest.config.js new file mode 100644 index 0000000000000..f258a72a9d0c6 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/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 = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/observability_log_explorer'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/observability_log_explorer', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/observability_log_explorer/{common,public}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/observability_log_explorer/kibana.jsonc b/x-pack/plugins/observability_log_explorer/kibana.jsonc new file mode 100644 index 0000000000000..35121b578c39c --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/kibana.jsonc @@ -0,0 +1,24 @@ +{ + "type": "plugin", + "id": "@kbn/observability-log-explorer-plugin", + "owner": "@elastic/infra-monitoring-ui", + "description": "This plugin exposes and registers observability log consumption features.", + "plugin": { + "id": "observabilityLogExplorer", + "server": true, + "browser": true, + "configPath": [ + "xpack", + "observabilityLogExplorer" + ], + "requiredPlugins": [ + "data", + "logExplorer", + "observabilityShared" + ], + "optionalPlugins": [ + "serverless" + ], + "requiredBundles": [] + } +} diff --git a/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx b/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx new file mode 100644 index 0000000000000..7d6863e4eb45a --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.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 { AppMountParameters, CoreStart, ScopedHistory } from '@kbn/core/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { Route, Router, Routes } from '@kbn/shared-ux-router'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { ObservablityLogExplorerMainRoute } from '../routes/main'; +import { ObservabilityLogExplorerPluginStart, ObservabilityLogExplorerStartDeps } from '../types'; + +export const renderObservabilityLogExplorer = ( + core: CoreStart, + pluginsStart: ObservabilityLogExplorerStartDeps, + ownPluginStart: ObservabilityLogExplorerPluginStart, + { element, history }: AppMountParameters +) => { + ReactDOM.render( + , + element + ); + + return () => { + // work around race condition between unmount effect and current app id + // observable in the search session service + pluginsStart.data.search.session.clear(); + + ReactDOM.unmountComponentAtNode(element); + }; +}; + +export interface ObservabilityLogExplorerAppProps { + core: CoreStart; + plugins: ObservabilityLogExplorerStartDeps; + pluginStart: ObservabilityLogExplorerPluginStart; + history: ScopedHistory; +} + +export const ObservabilityLogExplorerApp = ({ + core, + plugins: { logExplorer, observabilityShared, serverless }, + pluginStart, + history, +}: ObservabilityLogExplorerAppProps) => ( + + + + ( + + )} + /> + + + +); diff --git a/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx b/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx new file mode 100644 index 0000000000000..e79b8b1bc6271 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiPageSectionProps } from '@elastic/eui'; +import { css } from '@emotion/react'; +import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; +import React from 'react'; + +export const ObservabilityLogExplorerPageTemplate = ({ + children, + observabilityShared, +}: React.PropsWithChildren<{ + observabilityShared: ObservabilitySharedPluginStart; +}>) => ( + + {children} + +); + +const fullHeightContentStyles = css` + display: flex; + flex-direction: column; + flex: 1 0 auto; + width: 100%; + height: 100%; +`; + +const pageSectionProps: EuiPageSectionProps = { + grow: true, + paddingSize: 'none', + contentProps: { css: fullHeightContentStyles }, +}; diff --git a/x-pack/plugins/discover_log_explorer/public/index.ts b/x-pack/plugins/observability_log_explorer/public/index.ts similarity index 53% rename from x-pack/plugins/discover_log_explorer/public/index.ts rename to x-pack/plugins/observability_log_explorer/public/index.ts index 0c4298100c558..18e03f7889385 100644 --- a/x-pack/plugins/discover_log_explorer/public/index.ts +++ b/x-pack/plugins/observability_log_explorer/public/index.ts @@ -6,9 +6,9 @@ */ import { PluginInitializerContext } from '@kbn/core/public'; -import { DiscoverLogExplorerConfig } from '../common/plugin_config'; -import { DiscoverLogExplorerPlugin } from './plugin'; +import { ObservabilityLogExplorerConfig } from '../common/plugin_config'; +import { ObservabilityLogExplorerPlugin } from './plugin'; -export function plugin(context: PluginInitializerContext) { - return new DiscoverLogExplorerPlugin(context); +export function plugin(context: PluginInitializerContext) { + return new ObservabilityLogExplorerPlugin(context); } diff --git a/x-pack/plugins/observability_log_explorer/public/plugin.ts b/x-pack/plugins/observability_log_explorer/public/plugin.ts new file mode 100644 index 0000000000000..6afb62235ba15 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/plugin.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + AppNavLinkStatus, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin, + PluginInitializerContext, +} from '@kbn/core/public'; +import { type ObservabilityLogExplorerConfig } from '../common/plugin_config'; +import { logExplorerAppTitle } from '../common/translations'; +import { renderObservabilityLogExplorer } from './applications/observability_log_explorer'; +import type { + ObservabilityLogExplorerPluginSetup, + ObservabilityLogExplorerPluginStart, + ObservabilityLogExplorerSetupDeps, + ObservabilityLogExplorerStartDeps, +} from './types'; + +export class ObservabilityLogExplorerPlugin + implements Plugin +{ + private config: ObservabilityLogExplorerConfig; + + constructor(context: PluginInitializerContext) { + this.config = context.config.get(); + } + + public setup( + core: CoreSetup, + _pluginsSetup: ObservabilityLogExplorerSetupDeps + ) { + core.application.register({ + id: 'observability-log-explorer', + title: logExplorerAppTitle, + category: DEFAULT_APP_CATEGORIES.observability, + euiIconType: 'logoLogging', + navLinkStatus: this.config.navigation.showAppLink + ? AppNavLinkStatus.visible + : AppNavLinkStatus.hidden, + searchable: true, + mount: async (appMountParams) => { + const [coreStart, pluginsStart, ownPluginStart] = await core.getStartServices(); + + return renderObservabilityLogExplorer( + coreStart, + pluginsStart, + ownPluginStart, + appMountParams + ); + }, + }); + + return {}; + } + + public start(_core: CoreStart, _pluginsStart: ObservabilityLogExplorerStartDeps) { + return {}; + } +} diff --git a/x-pack/plugins/observability_log_explorer/public/routes/main/index.tsx b/x-pack/plugins/observability_log_explorer/public/routes/main/index.tsx new file mode 100644 index 0000000000000..889e340497cf9 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/routes/main/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 './main_route'; diff --git a/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx b/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx new file mode 100644 index 0000000000000..5e9b22fb1ad5d --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart, ScopedHistory } from '@kbn/core/public'; +import { LogExplorerPluginStart } from '@kbn/log-explorer-plugin/public'; +import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; +import { ServerlessPluginStart } from '@kbn/serverless/public'; +import React from 'react'; +import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template'; +import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs'; + +export interface ObservablityLogExplorerMainRouteProps { + core: CoreStart; + history: ScopedHistory; + logExplorer: LogExplorerPluginStart; + observabilityShared: ObservabilitySharedPluginStart; + serverless?: ServerlessPluginStart; +} + +export const ObservablityLogExplorerMainRoute = ({ + core, + history, + logExplorer, + observabilityShared, + serverless, +}: ObservablityLogExplorerMainRouteProps) => { + useBreadcrumbs(noBreadcrumbs, core.chrome, serverless); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/observability_log_explorer/public/types.ts b/x-pack/plugins/observability_log_explorer/public/types.ts new file mode 100644 index 0000000000000..f5e6526c502d9 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { LogExplorerPluginStart } from '@kbn/log-explorer-plugin/public'; +import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; +import { ServerlessPluginStart } from '@kbn/serverless/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ObservabilityLogExplorerPluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ObservabilityLogExplorerPluginStart {} + +export interface ObservabilityLogExplorerSetupDeps { + serverless?: ServerlessPluginStart; +} + +export interface ObservabilityLogExplorerStartDeps { + data: DataPublicPluginStart; + logExplorer: LogExplorerPluginStart; + observabilityShared: ObservabilitySharedPluginStart; + serverless?: ServerlessPluginStart; +} diff --git a/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx b/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx new file mode 100644 index 0000000000000..a8b575d5341dc --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBreadcrumb } from '@elastic/eui'; +import type { ChromeStart } from '@kbn/core-chrome-browser'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; +import { useEffect } from 'react'; +import { + betaBadgeDescription, + betaBadgeTitle, + logExplorerAppTitle, +} from '../../common/translations'; + +export const useBreadcrumbs = ( + breadcrumbs: EuiBreadcrumb[], + chromeService: ChromeStart, + serverlessService?: ServerlessPluginStart +) => { + useEffect(() => { + setBreadcrumbs(breadcrumbs, chromeService, serverlessService); + }, [breadcrumbs, chromeService, serverlessService]); +}; + +export function setBreadcrumbs( + breadcrumbs: EuiBreadcrumb[], + chromeService: ChromeStart, + serverlessService?: ServerlessPluginStart +) { + if (serverlessService) { + serverlessService.setBreadcrumbs(breadcrumbs); + } else if (chromeService) { + chromeService.setBreadcrumbs([ + { + text: logExplorerAppTitle, + }, + ...breadcrumbs, + ]); + } + chromeService.setBadge({ + text: betaBadgeTitle, + tooltip: betaBadgeDescription, + }); +} + +export const noBreadcrumbs: EuiBreadcrumb[] = []; diff --git a/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx b/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx new file mode 100644 index 0000000000000..d8b2235586e55 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from '@kbn/core/public'; +import { + createKibanaReactContext, + KibanaReactContextValue, + useKibana, +} from '@kbn/kibana-react-plugin/public'; +import { useMemo } from 'react'; +import { ObservabilityLogExplorerPluginStart, ObservabilityLogExplorerStartDeps } from '../types'; + +export type PluginKibanaContextValue = CoreStart & + ObservabilityLogExplorerStartDeps & + ObservabilityLogExplorerPluginStart; + +export const createKibanaContextForPlugin = ( + core: CoreStart, + plugins: ObservabilityLogExplorerStartDeps, + pluginStart: ObservabilityLogExplorerPluginStart +) => + createKibanaReactContext({ + ...core, + ...plugins, + ...pluginStart, + }); + +export const useKibanaContextForPlugin = + useKibana as () => KibanaReactContextValue; + +export const useKibanaContextForPluginProvider = ( + core: CoreStart, + plugins: ObservabilityLogExplorerStartDeps, + pluginStart: ObservabilityLogExplorerPluginStart +) => { + const { Provider } = useMemo( + () => createKibanaContextForPlugin(core, plugins, pluginStart), + [core, pluginStart, plugins] + ); + + return Provider; +}; diff --git a/x-pack/plugins/observability_log_explorer/server/config.ts b/x-pack/plugins/observability_log_explorer/server/config.ts new file mode 100644 index 0000000000000..1977c5e625c15 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/server/config.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, offeringBasedSchema } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core/server'; +import { ObservabilityLogExplorerConfig } from '../common/plugin_config'; + +export const configSchema = schema.object({ + navigation: schema.object({ + showAppLink: offeringBasedSchema({ + serverless: schema.boolean({ + defaultValue: true, + }), + options: { + defaultValue: false, + }, + }), + }), +}); + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot( + 'xpack.discoverLogExplorer.featureFlags.deepLinkVisible', + 'xpack.observabilityLogExplorer.navigation.showAppLink', + { level: 'warning' } + ), + ], + exposeToBrowser: { + navigation: { + showAppLink: true, + }, + }, +}; diff --git a/x-pack/plugins/discover_log_explorer/server/index.ts b/x-pack/plugins/observability_log_explorer/server/index.ts similarity index 67% rename from x-pack/plugins/discover_log_explorer/server/index.ts rename to x-pack/plugins/observability_log_explorer/server/index.ts index 091fe7bb47e47..a487aefc8fd14 100644 --- a/x-pack/plugins/discover_log_explorer/server/index.ts +++ b/x-pack/plugins/observability_log_explorer/server/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { DiscoverLogExplorerServerPlugin } from './plugin'; +import { ObservabilityLogExplorerServerPlugin } from './plugin'; export { config } from './config'; -export const plugin = () => new DiscoverLogExplorerServerPlugin(); +export const plugin = () => new ObservabilityLogExplorerServerPlugin(); diff --git a/x-pack/plugins/observability_log_explorer/server/plugin.ts b/x-pack/plugins/observability_log_explorer/server/plugin.ts new file mode 100644 index 0000000000000..da2d5edee0e96 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/server/plugin.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Plugin } from '@kbn/core/server'; + +export class ObservabilityLogExplorerServerPlugin implements Plugin { + setup() {} + + start() {} +} diff --git a/x-pack/plugins/observability_log_explorer/tsconfig.json b/x-pack/plugins/observability_log_explorer/tsconfig.json new file mode 100644 index 0000000000000..5f94d15d30fea --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + ".storybook/**/*.tsx" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/log-explorer-plugin", + "@kbn/i18n", + "@kbn/react-kibana-context-render", + "@kbn/shared-ux-router", + "@kbn/observability-shared-plugin", + "@kbn/data-plugin", + "@kbn/kibana-react-plugin", + "@kbn/serverless", + "@kbn/core-chrome-browser", + "@kbn/config-schema", + ], + "exclude": [ + "target/**/*" + ] +} diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts new file mode 100644 index 0000000000000..495fd5672bfb9 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.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. + */ + +describe('[Logs onboarding] Give Feedback', () => { + beforeEach(() => { + cy.loginAsElastic(); + cy.visitKibana('/app/observabilityOnboarding'); + }); + + it('feedback button is present in system logs onboarding', () => { + cy.getByTestSubj('obltOnboardingHomeStartSystemLogStream').click(); + cy.getByTestSubj('observabilityOnboardingPageGiveFeedback').should('exist'); + }); + + it('feedback button is present in custom logs onboarding', () => { + cy.getByTestSubj('obltOnboardingHomeStartLogFileStream').click(); + cy.getByTestSubj('observabilityOnboardingPageGiveFeedback').should('exist'); + }); + + it('feedback button is not present in the landing page', () => { + cy.getByTestSubj('observabilityOnboardingPageGiveFeedback').should( + 'not.exist' + ); + }); +}); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/system_logs.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/system_logs.cy.ts new file mode 100644 index 0000000000000..3497ac145eaf3 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/system_logs.cy.ts @@ -0,0 +1,657 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +describe('[Logs onboarding] System logs', () => { + describe('System integration', () => { + beforeEach(() => { + cy.deleteIntegration('system'); + }); + + describe('when user is missing privileges', () => { + beforeEach(() => { + cy.loginAsViewerUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + it('installation fails', () => { + cy.getByTestSubj( + 'obltOnboardingSystemLogsIntegrationInstallationFailed' + ).should('exist'); + }); + }); + + describe('when user has proper privileges', () => { + beforeEach(() => { + cy.loginAsEditorUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + after(() => { + cy.deleteIntegration('system'); + }); + + it('installation succeed', () => { + cy.getByTestSubj('obltOnboardingSystemLogsIntegrationInstalled').should( + 'exist' + ); + }); + + it('show link to navigate to system integration when clicking info icon', () => { + cy.getByTestSubj('obltOnboardingSystemLogsIntegrationInstalled').should( + 'exist' + ); + cy.getByTestSubj('obltOnboardingSystemLogsIntegrationInfo') + .should('exist') + .click(); + cy.getByTestSubj( + 'observabilityOnboardingSystemIntegrationLearnMore' + ).should('exist'); + }); + }); + }); + + describe('ApiKey generation', () => { + describe('when user is missing privileges', () => { + it('apiKey is not generated', () => { + cy.loginAsEditorUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + + cy.getByTestSubj('obltOnboardingLogsApiKeyCreationNoPrivileges').should( + 'exist' + ); + }); + }); + + describe('when user has proper privileges', () => { + beforeEach(() => { + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + it('apiKey is generated', () => { + cy.getByTestSubj('obltOnboardingLogsApiKeyCreated').should('exist'); + }); + }); + + describe('when an error occurred on creation', () => { + before(() => { + cy.intercept('/internal/observability_onboarding/logs/flow', { + statusCode: 500, + body: { + message: 'Internal error', + }, + }); + + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + it('apiKey is not generated', () => { + cy.getByTestSubj('obltOnboardingLogsApiKeyCreationFailed').should( + 'exist' + ); + }); + }); + }); + + describe('Install the Elastic Agent step', () => { + beforeEach(() => { + cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( + 'createOnboardingFlow' + ); + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + describe('When user select Linux OS', () => { + it('Auto download config to host is disabled by default', () => { + cy.get('.euiButtonGroup').contains('Linux').click(); + cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') + .should('be.enabled') + .should('not.be.checked'); + }); + + it('Installation script is shown', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('exist'); + }); + }); + + describe('When user select Mac OS', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('MacOS').click(); + }); + + it('Auto download config to host is disabled by default', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') + .should('be.enabled') + .should('not.be.checked'); + }); + + it('Installation script is shown', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('exist'); + }); + }); + + describe('When user select Windows OS', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('Windows').click(); + }); + + it('Auto download config to host is disabled by default', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') + .should('be.disabled') + .should('not.be.checked'); + }); + + it('A link to the documentation is shown instead of installation script', () => { + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentWindowsDocsLink' + ).should('exist'); + + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('not.exist'); + }); + }); + + describe('When Auto download config', () => { + describe('is selected', () => { + it('autoDownloadConfig flag is added to installation script', () => { + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ).click(); + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfigCallout' + ).should('exist'); + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('contain', 'autoDownloadConfig=1'); + }); + + it('Download config button is disabled', () => { + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ).click(); + cy.getByTestSubj( + 'obltOnboardingConfigureElasticAgentStepDownloadConfig' + ).should('be.disabled'); + }); + }); + + it('is not selected autoDownloadConfig flag is not added to installation script', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('not.contain', 'autoDownloadConfig=1'); + }); + }); + + describe('When user executes the installation script in the host', () => { + let onboardingId: string; + + describe('updates on steps are shown in the flow', () => { + beforeEach(() => { + cy.wait('@createOnboardingFlow') + .its('response.body') + .then((body) => { + onboardingId = body.onboardingId; + }); + }); + + describe('Download elastic Agent step', () => { + it('shows a loading callout when elastic agent is downloading', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'loading' + ); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Downloading Elastic Agent') + .should('exist'); + }); + + it('shows a success callout when elastic agent is downloaded', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains('Elastic Agent downloaded') + .should('exist'); + }); + + it('shows a danger callout when elastic agent was not downloaded', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'danger' + ); + cy.getByTestSubj('obltOnboardingStepStatus-danger') + .contains('Download Elastic Agent') + .should('exist'); + }); + }); + + describe('Extract elastic Agent step', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + }); + + it('shows a loading callout when elastic agent is extracting', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'loading' + ); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Extracting Elastic Agent') + .should('exist'); + }); + + it('shows a success callout when elastic agent is extracted', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains('Elastic Agent extracted') + .should('exist'); + }); + + it('shows a danger callout when elastic agent was not extracted', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'danger' + ); + cy.getByTestSubj('obltOnboardingStepStatus-danger') + .contains('Extract Elastic Agent') + .should('exist'); + }); + }); + + describe('Install elastic Agent step', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + }); + + it('shows a loading callout when elastic agent is installing', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'loading' + ); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Installing Elastic Agent') + .should('exist'); + }); + + it('shows a success callout when elastic agent is installed', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains('Elastic Agent installed') + .should('exist'); + }); + + it('shows a danger callout when elastic agent was not installed', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'danger' + ); + cy.getByTestSubj('obltOnboardingStepStatus-danger') + .contains('Install Elastic Agent') + .should('exist'); + }); + }); + + describe('Check elastic Agent status step', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + }); + + it('shows a loading callout when getting elastic agent status', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'loading' + ); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Connecting to the Elastic Agent') + .should('exist'); + }); + + it('shows a success callout when elastic agent status is healthy', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'complete' + ); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains('Connected to the Elastic Agent') + .should('exist'); + }); + + it('shows a warning callout when elastic agent status is not healthy', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'warning' + ); + cy.getByTestSubj('obltOnboardingStepStatus-warning') + .contains('Connect to the Elastic Agent') + .should('exist'); + }); + }); + }); + }); + }); + + describe('Configure Elastic Agent step', () => { + let onboardingId: string; + + beforeEach(() => { + cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( + 'createOnboardingFlow' + ); + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + cy.wait('@createOnboardingFlow') + .its('response.body') + .then((body) => { + onboardingId = body.onboardingId; + }); + }); + + describe('When user select Linux OS', () => { + beforeEach(() => { + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ).click(); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); + cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); + cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete'); + }); + + it('shows loading callout when config is being downloaded to the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'loading'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Downloading Elastic Agent config') + .should('exist'); + }); + + it('shows success callout when the configuration has been written to the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains( + 'Elastic Agent config written to /opt/Elastic/Agent/elastic-agent.yml' + ) + .should('exist'); + }); + + it('shows warning callout when the configuration was not written in the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'warning'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-warning"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-warning') + .contains('Configure the agent') + .should('exist'); + }); + }); + + describe('When user select Mac OS', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('MacOS').click(); + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ).click(); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); + cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); + cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete'); + }); + + it('shows loading callout when config is being downloaded to the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'loading'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Downloading Elastic Agent config') + .should('exist'); + }); + + it('shows success callout when the configuration has been written to the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains( + 'Elastic Agent config written to /Library/Elastic/Agent/elastic-agent.yml' + ) + .should('exist'); + }); + + it('shows warning callout when the configuration was not written in the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'warning'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-warning"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-warning') + .contains('Configure the agent') + .should('exist'); + }); + }); + + describe('When user select Windows', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('Windows').click(); + }); + + it('step is disabled', () => { + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-disabled"]' + ).should('exist'); + }); + }); + }); + + describe('Check logs step', () => { + let onboardingId: string; + + beforeEach(() => { + cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( + 'createOnboardingFlow' + ); + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + cy.wait('@createOnboardingFlow') + .its('response.body') + .then((body) => { + onboardingId = body.onboardingId; + }); + }); + + describe('When user select Linux OS or MacOS', () => { + describe('When configure Elastic Agent step is not finished', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'loading'); + }); + + it('check logs is not triggered', () => { + cy.get( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-incomplete"]' + ).should('exist'); + cy.get('.euiStep__title') + .contains('Ship logs to Elastic Observability') + .should('exist'); + }); + }); + + describe('When configure Elastic Agent step has finished', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-config', + 'complete' + ); + }); + + it('shows loading callout when logs are being checked', () => { + cy.get( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' + ).should('exist'); + cy.get('.euiStep__title') + .contains('Waiting for logs to be shipped...') + .should('exist'); + }); + }); + }); + + describe('When user select Windows', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('Windows').click(); + }); + + it('step is disabled', () => { + cy.get( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-disabled"]' + ).should('exist'); + }); + }); + }); + + describe('When logs are being shipped', () => { + beforeEach(() => { + cy.intercept('GET', '**/progress', { + status: 200, + body: { + progress: { + 'ea-download': { status: 'complete' }, + 'ea-extract': { status: 'complete' }, + 'ea-install': { status: 'complete' }, + 'ea-status': { status: 'complete' }, + 'ea-config': { status: 'complete' }, + 'logs-ingest': { status: 'complete' }, + }, + }, + }).as('checkOnboardingProgress'); + cy.intercept('GET', '/api/fleet/epm/packages/system').as( + 'systemIntegrationInstall' + ); + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + it('shows success callout when logs has arrived to elastic', () => { + cy.wait('@checkOnboardingProgress'); + cy.get( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' + ).should('exist'); + cy.get('.euiStep__title') + .contains('Logs are being shipped!') + .should('exist'); + }); + + it('when user clicks on Explore Logs it navigates to discover', () => { + cy.wait('@systemIntegrationInstall'); + cy.wait('@checkOnboardingProgress'); + cy.getByTestSubj('obltOnboardingExploreLogs').should('exist').click(); + cy.url().should('include', '/app/discover'); + + cy.get('button[title="logs-*"]').should('exist'); + }); + }); +}); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts new file mode 100644 index 0000000000000..cc5d0e2500ad1 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.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. + */ + +describe('[Observability onboarding] Navigation', () => { + beforeEach(() => { + cy.loginAsElastic(); + cy.visitKibana('/app/observabilityOnboarding/'); + }); + + describe('When user clicks on the card', () => { + it('navigates to system logs onboarding', () => { + cy.getByTestSubj('obltOnboardingHomeStartSystemLogStream').click(); + + cy.url().should('include', '/app/observabilityOnboarding/systemLogs'); + }); + + it('navigates to custom logs onboarding', () => { + cy.getByTestSubj('obltOnboardingHomeStartLogFileStream').click(); + + cy.url().should('include', '/app/observabilityOnboarding/customLogs'); + }); + + it('navigates to apm tutorial', () => { + cy.getByTestSubj('obltOnboardingHomeStartApmTutorial').click(); + + cy.url().should('include', '/app/home#/tutorial/apm'); + }); + + it('navigates to kubernetes integration', () => { + cy.getByTestSubj('obltOnboardingHomeGoToKubernetesIntegration').click(); + + cy.url().should( + 'include', + '/app/integrations/detail/kubernetes/overview' + ); + }); + + it('navigates to integrations', () => { + cy.getByTestSubj('obltOnboardingHomeExploreIntegrations').click(); + + cy.url().should('include', '/app/integrations/browse'); + }); + }); + + describe('When user clicks on Quick links', () => { + it('navigates to use sample data', () => { + cy.getByTestSubj('obltOnboardingHomeUseSampleData').click(); + + cy.url().should('include', '/app/home#/tutorial_directory/sampleData'); + }); + + it('navigates to upload a file', () => { + cy.getByTestSubj('obltOnboardingHomeUploadAFile').click(); + + cy.url().should('include', '/app/home#/tutorial_directory/fileDataViz'); + }); + }); +}); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts index 299729f1d8fa7..ea321f48a0bae 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts @@ -6,6 +6,51 @@ */ import URL from 'url'; +import { ObservabilityOnboardingUsername } from '../../../server/test_helpers/create_observability_onboarding_users/authentication'; + +export type InstallationStep = + | 'ea-download' + | 'ea-extract' + | 'ea-install' + | 'ea-status' + | 'ea-config'; + +export type InstallationStepStatus = + | 'incomplete' + | 'complete' + | 'disabled' + | 'loading' + | 'warning' + | 'danger' + | 'current'; + +Cypress.Commands.add('loginAsViewerUser', () => { + return cy.loginAs({ + username: ObservabilityOnboardingUsername.viewerUser, + password: 'changeme', + }); +}); + +Cypress.Commands.add('loginAsEditorUser', () => { + return cy.loginAs({ + username: ObservabilityOnboardingUsername.editorUser, + password: 'changeme', + }); +}); + +Cypress.Commands.add('loginAsLogMonitoringUser', () => { + return cy.loginAs({ + username: ObservabilityOnboardingUsername.logMonitoringUser, + password: 'changeme', + }); +}); + +Cypress.Commands.add('loginAsElastic', () => { + return cy.loginAs({ + username: 'elastic', + password: 'changeme', + }); +}); Cypress.Commands.add( 'loginAs', @@ -31,13 +76,6 @@ Cypress.Commands.add( } ); -Cypress.Commands.add('loginAsElastic', () => { - return cy.loginAs({ - username: 'elastic', - password: 'changeme', - }); -}); - Cypress.Commands.add('getByTestSubj', (selector: string) => { return cy.get(`[data-test-subj="${selector}"]`); }); @@ -57,3 +95,59 @@ Cypress.Commands.add( }); } ); + +Cypress.Commands.add('deleteIntegration', (integrationName: string) => { + const kibanaUrl = Cypress.env('KIBANA_URL'); + + cy.request({ + log: false, + method: 'GET', + url: `${kibanaUrl}/api/fleet/epm/packages/${integrationName}`, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'editor', pass: 'changeme' }, + }).then((response) => { + const status = response.body.item.status; + if (status === 'installed') { + cy.request({ + log: false, + method: 'DELETE', + url: `${kibanaUrl}/api/fleet/epm/packages/${integrationName}`, + body: { + force: false, + }, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'editor', pass: 'changeme' }, + }); + } + }); +}); + +Cypress.Commands.add( + 'updateInstallationStepStatus', + ( + onboardingId: string, + step: InstallationStep, + status: InstallationStepStatus + ) => { + const kibanaUrl = Cypress.env('KIBANA_URL'); + + cy.log(onboardingId, step, status); + + cy.request({ + log: false, + method: 'POST', + url: `${kibanaUrl}/internal/observability_onboarding/flow/${onboardingId}/step/${step}`, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'editor', pass: 'changeme' }, + body: { + status, + }, + }); + } +); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/support/types.d.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/support/types.d.ts index 8db14ad70561a..6b5695d159d76 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/support/types.d.ts @@ -11,8 +11,17 @@ declare namespace Cypress { username: string; password: string; }): Cypress.Chainable>; + loginAsViewerUser(): Cypress.Chainable>; + loginAsEditorUser(): Cypress.Chainable>; + loginAsLogMonitoringUser(): Cypress.Chainable>; loginAsElastic(): Cypress.Chainable>; getByTestSubj(selector: string): Chainable>; visitKibana(url: string, rangeFrom?: string, rangeTo?: string): void; + deleteIntegration(integrationName: string): void; + updateInstallationStepStatus( + onboardingId: string, + step: InstallationStep, + status: InstallationStepStatus + ): void; } } diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress_test_runner.ts b/x-pack/plugins/observability_onboarding/e2e/cypress_test_runner.ts index 1d056897a6d02..34765a892cc1c 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress_test_runner.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress_test_runner.ts @@ -9,6 +9,7 @@ import cypress from 'cypress'; import path from 'path'; import Url from 'url'; import { FtrProviderContext } from './ftr_provider_context'; +import { createObservabilityOnboardingUsers } from '../server/test_helpers/create_observability_onboarding_users'; export async function cypressTestRunner({ ftrProviderContext: { getService }, @@ -22,6 +23,13 @@ export async function cypressTestRunner({ const username = config.get('servers.elasticsearch.username'); const password = config.get('servers.elasticsearch.password'); + const kibanaUrl = Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + auth: `${username}:${password}`, + }); + const esNode = Url.format({ protocol: config.get('servers.elasticsearch.protocol'), port: config.get('servers.elasticsearch.port'), @@ -29,6 +37,12 @@ export async function cypressTestRunner({ auth: `${username}:${password}`, }); + // Creates ObservabilityOnboarding users + await createObservabilityOnboardingUsers({ + elasticsearch: { node: esNode, username, password }, + kibana: { hostname: kibanaUrl }, + }); + const esRequestTimeout = config.get('timeouts.esRequestTimeout'); const kibanaUrlWithoutAuth = Url.format({ diff --git a/x-pack/plugins/observability_onboarding/public/assets/standalone_agent_setup.sh b/x-pack/plugins/observability_onboarding/public/assets/standalone_agent_setup.sh index 733e304eeba92..f4c7a31e76b6c 100755 --- a/x-pack/plugins/observability_onboarding/public/assets/standalone_agent_setup.sh +++ b/x-pack/plugins/observability_onboarding/public/assets/standalone_agent_setup.sh @@ -61,6 +61,7 @@ updateStepProgress() { --header "Authorization: ApiKey ${API_KEY_ENCODED}" \ --header "Content-Type: application/json" \ --header "kbn-xsrf: true" \ + --header "x-elastic-internal-origin: Kibana" \ --data "{\"status\":\"${STATUS}\", \"message\":\"${MESSAGE}\"}" \ --output /dev/null \ --no-progress-meter @@ -124,14 +125,20 @@ waitForElasticAgentStatus if [ "$?" -ne 0 ]; then updateStepProgress "ea-status" "warning" "Unable to determine agent status" fi -ELASTIC_AGENT_STATE="$(elastic-agent status | grep -m1 State | sed 's/State: //')" -ELASTIC_AGENT_MESSAGE="$(elastic-agent status | grep -m1 Message | sed 's/Message: //')" -if [ "${ELASTIC_AGENT_STATE}" = "HEALTHY" ] && [ "${ELASTIC_AGENT_MESSAGE}" = "Running" ]; then + +# https://www.elastic.co/guide/en/fleet/current/elastic-agent-cmd-options.html#elastic-agent-status-command +ELASTIC_AGENT_STATES=(STARTING CONFIGURING HEALTHY DEGRADED FAILED STOPPING UPGRADING ROLLBACK) + +# Get elastic-agent status in json format | removing extra states in the json | finding "state":value | removing , | removing "state": | trimming the result +ELASTIC_AGENT_STATE="$(elastic-agent status --output json | sed -n '/components/q;p' | grep state | sed 's/\(.*\),/\1 /' | sed 's/"state": //' | sed 's/\s//g')" +# Get elastic-agent status in json format | removing extra states in the json | finding "message":value | removing , | removing "message": | trimming the result | removing "" +ELASTIC_AGENT_MESSAGE="$(elastic-agent status --output json | sed -n '/components/q;p' | grep message | sed 's/\(.*\),/\1 /' | sed 's/"message": //' | sed 's/\s//g' | sed 's/\"//g')" +if [ "${ELASTIC_AGENT_STATE}" = "2" ] && [ "${ELASTIC_AGENT_MESSAGE}" = "Running" ]; then echo "Elastic Agent running" echo "Download and save configuration to ${cfg}" updateStepProgress "ea-status" "complete" else - updateStepProgress "ea-status" "warning" "Expected agent status HEALTHY / Running but got ${ELASTIC_AGENT_STATE} / ${ELASTIC_AGENT_MESSAGE}" + updateStepProgress "ea-status" "warning" "Expected agent status HEALTHY / Running but got ${ELASTIC_AGENT_STATES[ELASTIC_AGENT_STATE]} / ${ELASTIC_AGENT_MESSAGE}" fi downloadElasticAgentConfig() { @@ -142,6 +149,7 @@ downloadElasticAgentConfig() { --header "Authorization: ApiKey ${API_KEY_ENCODED}" \ --header "Content-Type: application/json" \ --header "kbn-xsrf: true" \ + --header "x-elastic-internal-origin: Kibana" \ --no-progress-meter \ --output ${cfg} @@ -156,7 +164,7 @@ downloadElasticAgentConfig() { if [ "${AUTO_DOWNLOAD_CONFIG}" == "autoDownloadConfig=1" ]; then downloadElasticAgentConfig - echo "Done with standalone Elastic Agent setup for custom logs. Look for streaming logs to arrive in Kibana" + echo "Done with standalone Elastic Agent setup. Look for streaming logs to arrive in Kibana" else - echo "Done with standalone Elastic Agent setup for custom logs. Make sure to add your configuration to ${cfg}, then look for streaming logs to arrive in Kibana" + echo "Done with standalone Elastic Agent setup. Make sure to add your configuration to ${cfg}, then look for streaming logs to arrive in Kibana" fi diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx index 57a7ba7b4cab1..c55f554fc4845 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx @@ -54,6 +54,7 @@ export function ApiKeyBanner({ } color="primary" + data-test-subj="obltOnboardingLogsCreatingApiKey" /> ); @@ -67,6 +68,7 @@ export function ApiKeyBanner({ )} color="success" iconType="check" + data-test-subj="obltOnboardingLogsApiKeyCreated" >

{i18n.translate( @@ -123,6 +125,7 @@ export function ApiKeyBanner({ )} color="danger" iconType="error" + data-test-subj="obltOnboardingLogsApiKeyCreationFailed" >

{i18n.translate( @@ -148,6 +151,7 @@ export function ApiKeyBanner({ )} color="warning" iconType="warning" + data-test-subj="obltOnboardingLogsApiKeyCreationNoPrivileges" >

{i18n.translate( diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx index 27a44e94f07b6..6dcb343b222cb 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx @@ -123,7 +123,12 @@ export function Home() { { defaultMessage: 'Stream host system logs' } )} footer={ - + {getStartedLabel} } @@ -216,7 +221,11 @@ export function Home() { } )} footer={ - + {getStartedLabel} } @@ -245,6 +254,7 @@ export function Home() { {getStartedLabel} @@ -282,7 +292,11 @@ export function Home() { )} footer={ <> - + {i18n.translate( 'xpack.observability_onboarding.card.integrations.start', { defaultMessage: 'Start exploring' } @@ -298,14 +312,20 @@ export function Home() { - + {i18n.translate( 'xpack.observability_onboarding.card.integrations.sampleData', { defaultMessage: 'Use sample data' } )} - + {i18n.translate( 'xpack.observability_onboarding.card.integrations.uploadFile', { defaultMessage: 'Upload a file' } diff --git a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx index 5a71c588c0711..4c7e7fd63625e 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx @@ -171,7 +171,11 @@ export function InstallElasticAgent() { : stepStatus === 'complete' ? CHECK_LOGS_LABELS.completed : CHECK_LOGS_LABELS.incomplete; - return { title, status: stepStatus }; + return { + title, + status: stepStatus, + 'data-test-subj': 'obltOnboardingCheckLogsStep', + }; } return { title: CHECK_LOGS_LABELS.incomplete, @@ -203,6 +207,7 @@ export function InstallElasticAgent() { fill iconType="magnifyWithPlus" onClick={onContinue} + data-test-subj="obltOnboardingExploreLogs" > {i18n.translate( 'xpack.observability_onboarding.steps.exploreLogs', diff --git a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx index d567a13f3d3a4..61ebaca8056ea 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx @@ -74,6 +74,7 @@ export function SystemIntegrationBanner() { } color="primary" + data-test-subj="obltOnboardingSystemLogsInstallingIntegration" /> ); } @@ -89,6 +90,7 @@ export function SystemIntegrationBanner() { )} color="warning" iconType="warning" + data-test-subj="obltOnboardingSystemLogsIntegrationInstallationFailed" > {error?.message} @@ -106,6 +108,7 @@ export function SystemIntegrationBanner() { values={{ systemIntegrationTooltip: ( ); diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/install_elastic_agent_steps.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/install_elastic_agent_steps.tsx index 16996a18d05b0..282591e0a1487 100644 --- a/x-pack/plugins/observability_onboarding/public/components/shared/install_elastic_agent_steps.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/shared/install_elastic_agent_steps.tsx @@ -111,12 +111,7 @@ export function InstallElasticAgentSteps({ selectedPlatform ); return ( - + ); })} @@ -184,6 +179,7 @@ export function InstallElasticAgentSteps({ download="elastic-agent.yml" target="_blank" isDisabled={autoDownloadConfig} + data-test-subj="obltOnboardingConfigureElasticAgentStepDownloadConfig" > {i18n.translate( 'xpack.observability_onboarding.installElasticAgent.configStep.downloadConfigButton', @@ -209,6 +205,7 @@ export function InstallElasticAgentSteps({ ({ checked={autoDownloadConfig} onChange={onToggleAutoDownloadConfig} disabled={disableSteps || isInstallStarted} + data-test-subj="obltOnboardingInstallElasticAgentAutoDownloadConfig" /> {autoDownloadConfig && ( @@ -288,6 +286,7 @@ export function InstallElasticAgentSteps({ )} color="warning" iconType="warning" + data-test-subj="obltOnboardingInstallElasticAgentAutoDownloadConfigCallout" /> @@ -318,6 +317,7 @@ export function InstallElasticAgentSteps({ ), }, { + 'data-test-subj': 'obltOnboardingConfigureElasticAgentStep', title: i18n.translate( 'xpack.observability_onboarding.installElasticAgent.configureStep.title', { defaultMessage: 'Configure the Elastic agent' } @@ -329,6 +329,7 @@ export function InstallElasticAgentSteps({ children: null, ...euiStep, status: disableSteps ? 'disabled' : euiStep.status, + 'data-test-subj': euiStep['data-test-subj'], })), ]} /> diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx index 66165edc8e133..8a0e8b53e7350 100644 --- a/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx @@ -13,6 +13,7 @@ interface PopoverTooltipProps { iconType?: string; title?: string; children: React.ReactNode; + dataTestSubj: string; } export function PopoverTooltip({ @@ -20,6 +21,7 @@ export function PopoverTooltip({ iconType = 'iInCircle', title, children, + dataTestSubj, }: PopoverTooltipProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -32,6 +34,7 @@ export function PopoverTooltip({ style={{ margin: '-5px 0 0 -5px' }} button={ ) => { setIsPopoverOpen(!isPopoverOpen); diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/step_status.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/step_status.tsx index 59ad5d202cf89..d2a5341fda731 100644 --- a/x-pack/plugins/observability_onboarding/public/components/shared/step_status.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/shared/step_status.tsx @@ -27,7 +27,7 @@ export function StepStatus({ }) { if (status === 'loading') { return ( - + @@ -43,7 +43,7 @@ export function StepStatus({ } if (status === 'complete') { return ( - + {message} @@ -52,7 +52,7 @@ export function StepStatus({ } if (status === 'danger') { return ( - + {message} @@ -61,7 +61,7 @@ export function StepStatus({ } if (status === 'warning') { return ( - + {message} diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/windows_install_step.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/windows_install_step.tsx index 42677924a4706..6784095d4e516 100644 --- a/x-pack/plugins/observability_onboarding/public/components/shared/windows_install_step.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/shared/windows_install_step.tsx @@ -40,6 +40,7 @@ export function WindowsInstallStep({ href={docsLink} target="_blank" style={{ width: 'fit-content' }} + data-test-subj="obltOnboardingInstallElasticAgentWindowsDocsLink" > {i18n.translate( 'xpack.observability_onboarding.windows.installStep.link.label', diff --git a/x-pack/plugins/observability_onboarding/server/routes/logs/route.ts b/x-pack/plugins/observability_onboarding/server/routes/logs/route.ts index c28c393382930..085f7cece0d8a 100644 --- a/x-pack/plugins/observability_onboarding/server/routes/logs/route.ts +++ b/x-pack/plugins/observability_onboarding/server/routes/logs/route.ts @@ -40,7 +40,7 @@ const installShipperSetupRoute = createObservabilityOnboardingServerRoute({ scriptDownloadUrl: string; elasticAgentVersion: string; }> { - const { core, plugins, kibanaVersion } = resources; + const { core, plugins } = resources; const coreStart = await core.start(); const kibanaUrl = @@ -53,7 +53,7 @@ const installShipperSetupRoute = createObservabilityOnboardingServerRoute({ return { apiEndpoint, scriptDownloadUrl, - elasticAgentVersion: kibanaVersion, + elasticAgentVersion: '8.9.1', }; }, }); diff --git a/x-pack/plugins/observability_onboarding/server/saved_objects/observability_onboarding_status.ts b/x-pack/plugins/observability_onboarding/server/saved_objects/observability_onboarding_status.ts index b7426ac048cd8..9bc6b31bf22f5 100644 --- a/x-pack/plugins/observability_onboarding/server/saved_objects/observability_onboarding_status.ts +++ b/x-pack/plugins/observability_onboarding/server/saved_objects/observability_onboarding_status.ts @@ -6,6 +6,7 @@ */ import { SavedObjectsType } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; export const OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE = 'observability-onboarding-state'; @@ -46,6 +47,18 @@ export interface SavedObservabilityOnboardingFlow updatedAt: number; } +const LogFilesStateSchema = schema.object({ + datasetName: schema.string(), + serviceName: schema.maybe(schema.string()), + customConfigurations: schema.maybe(schema.string()), + logFilePaths: schema.arrayOf(schema.string()), + namespace: schema.string(), +}); + +const SystemLogsStateSchema = schema.object({ + namespace: schema.string(), +}); + export const observabilityOnboardingFlow: SavedObjectsType = { name: OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE, hidden: false, @@ -57,4 +70,24 @@ export const observabilityOnboardingFlow: SavedObjectsType = { progress: { type: 'object', dynamic: false }, }, }, + modelVersions: { + '1': { + changes: [], + schemas: { + create: schema.object({ + type: schema.string(), + state: schema.maybe( + schema.oneOf([LogFilesStateSchema, SystemLogsStateSchema]) + ), + progress: schema.mapOf( + schema.string(), + schema.object({ + status: schema.string(), + message: schema.maybe(schema.string()), + }) + ), + }), + }, + }, + }, }; diff --git a/x-pack/plugins/osquery/common/api/asset/assets.schema.yaml b/x-pack/plugins/osquery/common/api/asset/assets.schema.yaml new file mode 100644 index 0000000000000..31688b7ce66cb --- /dev/null +++ b/x-pack/plugins/osquery/common/api/asset/assets.schema.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.0 +info: + title: Assets Schema + version: '1' +paths: + /internal/osquery/assets: + get: + summary: Get assets + parameters: + - $ref: './assets_status.schema.yaml#/components/parameters/AssetsStatusRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './assets_status.schema.yaml#/components/schemas/SuccessResponse' + /internal/osquery/assets/update: + post: + summary: Update assets + parameters: + - $ref: './assets_status.schema.yaml#/components/parameters/AssetsStatusRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './assets_status.schema.yaml#/components/schemas/SuccessResponse' diff --git a/x-pack/plugins/osquery/common/api/asset/assets_status.schema.yaml b/x-pack/plugins/osquery/common/api/asset/assets_status.schema.yaml new file mode 100644 index 0000000000000..48322c1266b07 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/asset/assets_status.schema.yaml @@ -0,0 +1,20 @@ +openapi: 3.0.0 +info: + title: Assets Status Schema + version: '1' +paths: { } +components: + parameters: + AssetsStatusRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/AssetsRequestQuery' + schemas: + AssetsRequestQuery: + type: object + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.schema.yaml new file mode 100644 index 0000000000000..7e46e15abb825 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.schema.yaml @@ -0,0 +1,70 @@ +openapi: 3.0.0 +info: + title: Fleet wrapper schema + version: '1' +paths: + /internal/osquery/fleet_wrapper/agents: + get: + summary: Get agents + parameters: + - $ref: './get_agents.schema.yaml#/components/parameters/GetAgentsRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './get_agents.schema.yaml#/components/schemas/SuccessResponse' + + /internal/osquery/fleet_wrapper/agents/{id}: + get: + summary: Get Agent details + parameters: + - $ref: './get_agent_details.schema.yaml#/components/parameters/GetAgentDetailsRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './get_agent_details.schema.yaml#/components/schemas/SuccessResponse' + + /internal/osquery/fleet_wrapper/agent_policies: + get: + summary: Get Agent policies + parameters: + - $ref: './get_agent_policies.schema.yaml#/components/parameters/GetAgentPoliciesRequestParameter' + - $ref: './get_agent_policies.schema.yaml#/components/parameters/GetAgentPoliciesRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './get_agent_policies.schema.yaml#/components/schemas/SuccessResponse' + + /internal/osquery/fleet_wrapper/agent_policies/{id}: + get: + summary: Get Agent policy + parameters: + - $ref: './get_agent_policy.schema.yaml#/components/parameters/GetAgentPolicyRequestParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './get_agent_policy.schema.yaml#/components/schemas/SuccessResponse' + + /internal/osquery/fleet_wrapper/package_policies: + get: + summary: Get Agent policy + parameters: + - $ref: './get_package_policies.schema.yaml#/components/parameters/GetPackagePoliciesRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './get_package_policies.schema.yaml#/components/schemas/SuccessResponse' diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.schema.yaml new file mode 100644 index 0000000000000..bdf4cb3329cdf --- /dev/null +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.schema.yaml @@ -0,0 +1,20 @@ +openapi: 3.0.0 +info: + title: Get agent details schema + version: '1' +paths: { } +components: + parameters: + GetAgentDetailsRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/GetAgentDetailsRequestParams' + schemas: + GetAgentDetailsRequestParams: + type: object + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.schema.yaml new file mode 100644 index 0000000000000..cdfb521712674 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.schema.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.0 +info: + title: Get agent policies schema + version: '1' +paths: { } +components: + parameters: + GetAgentPoliciesRequestQueryParameter: + name: query + in: query + required: true + schema: + $ref: '#/components/schemas/GetAgentPoliciesRequestParams' + GetAgentPoliciesRequestParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/GetAgentPoliciesRequestParams' + schemas: + GetAgentPoliciesRequestParams: + type: object + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.schema.yaml new file mode 100644 index 0000000000000..dc4a2607bfc6b --- /dev/null +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.schema.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: Get agent policy schema + version: '1' +paths: { } +components: + parameters: + GetAgentPolicyRequestParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/GetAgentPolicyRequestParams' + schemas: + GetAgentPolicyRequestParams: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.schema.yaml new file mode 100644 index 0000000000000..e10174bee2634 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.schema.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + title: Get agent status schema + version: '1' +paths: { } +components: + parameters: + GetAgentStatusRequestQueryParameter: + name: query + in: query + required: true + schema: + $ref: '#/components/schemas/GetAgentStatusRequestQueryParams' + GetAgentStatusRequestParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/GetAgentStatusRequestParams' + schemas: + GetAgentStatusRequestParams: + type: object + GetAgentStatusRequestQueryParams: + type: object + properties: + kuery: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/KueryOrUndefined' + policyId: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.schema.yaml new file mode 100644 index 0000000000000..c1a387512c3d3 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.schema.yaml @@ -0,0 +1,20 @@ +openapi: 3.0.0 +info: + title: Get agents schema + version: '1' +paths: { } +components: + parameters: + GetAgentsRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/GetAgentsRequestParams' + schemas: + GetAgentsRequestParams: + type: object + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.schema.yaml new file mode 100644 index 0000000000000..708867e8f7fa1 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.schema.yaml @@ -0,0 +1,20 @@ +openapi: 3.0.0 +info: + title: Get package policies schema + version: '1' +paths: { } +components: + parameters: + GetPackagePoliciesRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/GetPackagePoliciesRequestParams' + schemas: + GetPackagePoliciesRequestParams: + type: object + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/create_live_query.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/create_live_query.schema.yaml new file mode 100644 index 0000000000000..4cae65cab8b82 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/live_query/create_live_query.schema.yaml @@ -0,0 +1,53 @@ +openapi: 3.0.0 +info: + title: Create Live Query Schema + version: '2023-10-31' +paths: { } +components: + schemas: + CreateLiveQueryRequestBody: + type: object + properties: + agent_ids: + type: array + items: + type: string + agent_all: + type: boolean + agent_platforms: + type: array + items: + type: string + agent_policy_ids: + type: array + items: + type: string + query: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/QueryOrUndefined' + queries: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/ArrayQueries' + saved_query_id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SavedQueryIdOrUndefined' + ecs_mapping: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/ECSMappingOrUndefined' + pack_id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PackIdOrUndefined' + alert_ids: + type: array + items: + type: string + case_ids: + type: array + items: + type: string + event_ids: + type: array + items: + type: string + metadata: + type: object + nullable: true + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/find_live_query.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/find_live_query.schema.yaml new file mode 100644 index 0000000000000..730051c5a329d --- /dev/null +++ b/x-pack/plugins/osquery/common/api/live_query/find_live_query.schema.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.0 +info: + title: Find Live Queries Schema + version: '2023-10-31' +paths: { } +components: + parameters: + FindLiveQueryRequestQueryParameter: + name: query + in: query + required: true + schema: + $ref: '#/components/schemas/FindLiveQueryRequestQuery' + schemas: + FindLiveQueryRequestQuery: + type: object + properties: + kuery: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/KueryOrUndefined' + page: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PageOrUndefined' + pageSize: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PageSizeOrUndefined' + sort: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrUndefined' + sortOrder: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrderOrUndefined' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.schema.yaml new file mode 100644 index 0000000000000..8bc32f55162f4 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/live_query/get_live_query_details.schema.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + title: Get Live Query Details Schema + version: '2023-10-31' +paths: { } +components: + parameters: + GetLiveQueryDetailsRequestParameter: + name: id + in: path + required: true + schema: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' + GetLiveQueryDetailsRequestQueryParameter: + name: query + in: query + schema: + type: object + additionalProperties: true + schemas: + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.schema.yaml new file mode 100644 index 0000000000000..6036820ef022b --- /dev/null +++ b/x-pack/plugins/osquery/common/api/live_query/get_live_query_results.schema.yaml @@ -0,0 +1,45 @@ +openapi: 3.0.0 +info: + title: Get Live Query Results Schema + version: '2023-10-31' +paths: { } +components: + parameters: + GetLiveQueryRequestResultsQueryParameter: + name: query + in: query + required: true + schema: + $ref: '#/components/schemas/GetLiveQueryResultsRequestQuery' + GetLiveQueryRequestResultsParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/GetLiveQueryResultsRequestParams' + schemas: + GetLiveQueryResultsRequestQuery: + type: object + properties: + kuery: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/KueryOrUndefined' + page: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PageOrUndefined' + pageSize: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PageSizeOrUndefined' + sort: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrUndefined' + sortOrder: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrderOrUndefined' + GetLiveQueryResultsRequestParams: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' + actionId: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' + + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/live_query/live_queries.schema.yaml b/x-pack/plugins/osquery/common/api/live_query/live_queries.schema.yaml new file mode 100644 index 0000000000000..feff5dd665f33 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/live_query/live_queries.schema.yaml @@ -0,0 +1,58 @@ +openapi: 3.0.0 +info: + title: Live Queries Schema + version: '2023-10-31' +paths: + /api/osquery/live_queries: + get: + summary: Find live queries + parameters: + - $ref: './find_live_query.schema.yaml#/components/parameters/FindLiveQueryRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './find_live_query.schema.yaml#/components/schemas/SuccessResponse' + post: + summary: Create a live query + requestBody: + required: true + content: + application/json: + schema: + $ref: './create_live_query.schema.yaml#/components/schemas/CreateLiveQueryRequestBody' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './create_live_query.schema.yaml#/components/schemas/SuccessResponse' + /api/osquery/live_queries/{id}: + get: + summary: Get live query details + parameters: + - $ref: './get_live_query_details.schema.yaml#/components/parameters/GetLiveQueryDetailsRequestQueryParameter' + - $ref: './get_live_query_details.schema.yaml#/components/parameters/GetLiveQueryDetailsRequestParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './get_live_query_details.schema.yaml#/components/schemas/SuccessResponse' + /api/osquery/live_queries/{id}/results/{actionId}: + get: + summary: Get live query results + parameters: + - $ref: './get_live_query_results.schema.yaml#/components/parameters/GetLiveQueryRequestResultsQueryParameter' + - $ref: './get_live_query_results.schema.yaml#/components/parameters/GetLiveQueryRequestResultsParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './get_live_query_results.schema.yaml#/components/schemas/SuccessResponse' diff --git a/x-pack/plugins/osquery/common/api/model/schema/common_attributes.schema.yaml b/x-pack/plugins/osquery/common/api/model/schema/common_attributes.schema.yaml new file mode 100644 index 0000000000000..ba236b8601e7d --- /dev/null +++ b/x-pack/plugins/osquery/common/api/model/schema/common_attributes.schema.yaml @@ -0,0 +1,251 @@ +openapi: 3.0.0 +info: + title: Common Osquery Attributes + version: '2023-10-31' +paths: { } +components: + schemas: + Id: + type: string + IdOrUndefined: + $ref: '#/components/schemas/Id' + nullable: true + + AgentSelection: + type: object + properties: + agents: + type: array + items: + type: string + allAgentsSelected: + type: boolean + platformsSelected: + type: array + items: + type: string + policiesSelected: + type: array + items: + type: string + + AgentSelectionOrUndefined: + $ref: '#/components/schemas/AgentSelection' + nullable: true + + + Description: + type: string + + DescriptionOrUndefined: + $ref: '#/components/schemas/Description' + nullable: true + + + Platform: + type: string + + PlatformOrUndefined: + $ref: '#/components/schemas/Platform' + nullable: true + + + Query: + type: string + + QueryOrUndefined: + $ref: '#/components/schemas/Query' + nullable: true + + Version: + type: string + + VersionOrUndefined: + $ref: '#/components/schemas/Version' + nullable: true + + Interval: + type: string + + IntervalOrUndefined: + $ref: '#/components/schemas/Interval' + nullable: true + + Snapshot: + type: boolean + + SnapshotOrUndefined: + $ref: '#/components/schemas/Snapshot' + nullable: true + + Removed: + type: boolean + + RemovedOrUndefined: + $ref: '#/components/schemas/Removed' + nullable: true + + PackName: + type: string + + SavedQueryId: + type: string + + SavedQueryIdOrUndefined: + $ref: '#/components/schemas/SavedQueryId' + nullable: true + + + PackId: + type: string + + PackIdOrUndefined: + $ref: '#/components/schemas/PackId' + nullable: true + + Enabled: + type: boolean + + EnabledOrUndefined: + $ref: '#/components/schemas/Enabled' + nullable: true + + PolicyIds: + type: array + items: + type: string + + PolicyIdsOrUndefined: + $ref: '#/components/schemas/PolicyIds' + nullable: true + + ExecutionContext: + type: object + properties: + name: + type: string + nullable: true + url: + type: string + nullable: true + + + ExecutionContextOrUndefined: + $ref: '#/components/schemas/ExecutionContext' + nullable: true + + + ECSMappingItem: + type: object + properties: + field: + type: string + value: + oneOf: + - type: string + - type: array + items: + type: string + + ECSMapping: + type: object + additionalProperties: + $ref: '#/components/schemas/ECSMappingItem' + + ECSMappingOrUndefined: + $ref: '#/components/schemas/ECSMapping' + nullable: true + + + StringArrayOrUndefined: + type: array + items: + type: string + nullable: true + + + ArrayQueriesItem: + type: object + properties: + id: + $ref: '#/components/schemas/Id' + query: + $ref: '#/components/schemas/Query' + ecs_mapping: + $ref: '#/components/schemas/ECSMappingOrUndefined' + version: + $ref: '#/components/schemas/VersionOrUndefined' + platform: + $ref: '#/components/schemas/PlatformOrUndefined' + removed: + $ref: '#/components/schemas/RemovedOrUndefined' + snapshot: + $ref: '#/components/schemas/SnapshotOrUndefined' + + ArrayQueries: + type: array + items: + $ref: '#/components/schemas/ArrayQueriesItem' + + ObjectQueriesItem: + type: object + properties: + query: + $ref: '#/components/schemas/Query' + id: + $ref: '#/components/schemas/Id' + ecs_mapping: + $ref: '#/components/schemas/ECSMappingOrUndefined' + version: + $ref: '#/components/schemas/VersionOrUndefined' + platform: + $ref: '#/components/schemas/PlatformOrUndefined' + saved_query_id: + $ref: '#/components/schemas/SavedQueryIdOrUndefined' + removed: + $ref: '#/components/schemas/RemovedOrUndefined' + snapshot: + $ref: '#/components/schemas/SnapshotOrUndefined' + + ObjectQueries: + type: object + additionalProperties: + $ref: '#/components/schemas/ObjectQueriesItem' + + Queries: + oneOf: + - $ref: '#/components/schemas/ArrayQueries' + - $ref: '#/components/schemas/ObjectQueries' + + + QueriesOrUndefined: + $ref: '#/components/schemas/Queries' + nullable: true + + KueryOrUndefined: + type: string + nullable: true + + PageOrUndefined: + type: integer + nullable: true + + PageSizeOrUndefined: + type: integer + nullable: true + + SortOrUndefined: + type: string + nullable: true + + SortOrderOrUndefined: + oneOf: + + - type: string + nullable: true + - enum: [ asc, desc ] + + Shards: + type: object + additionalProperties: + type: number diff --git a/x-pack/plugins/osquery/common/api/packs/create_pack.schema.yaml b/x-pack/plugins/osquery/common/api/packs/create_pack.schema.yaml new file mode 100644 index 0000000000000..da04d037b1d56 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/packs/create_pack.schema.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.0 +info: + title: Create Pack Schema + version: '2023-10-31' +paths: { } +components: + schemas: + CreatePacksRequestBody: + type: object + properties: + name: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PackName' + description: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DescriptionOrUndefined' + enabled: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/EnabledOrUndefined' + policy_ids: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PolicyIdsOrUndefined' + shards: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Shards' + queries: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/ObjectQueries' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/packs/delete_packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/delete_packs.schema.yaml new file mode 100644 index 0000000000000..3286aa0b1bb7a --- /dev/null +++ b/x-pack/plugins/osquery/common/api/packs/delete_packs.schema.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: Delete Saved Queries Schema + version: '2023-10-31' +paths: { } +components: + parameters: + DeletePacksRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/DeletePacksRequestQuery' + schemas: + DeletePacksRequestQuery: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PackId' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/packs/find_packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/find_packs.schema.yaml new file mode 100644 index 0000000000000..4cd1c222bc6d2 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/packs/find_packs.schema.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.0 +info: + title: Find Saved Queries Schema + version: '2023-10-31' +paths: { } +components: + parameters: + FindPacksRequestQueryParameter: + name: query + in: query + required: true + schema: + $ref: '#/components/schemas/FindPacksRequestQuery' + schemas: + FindPacksRequestQuery: + type: object + properties: + page: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PageOrUndefined' + pageSize: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PageSizeOrUndefined' + sort: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrUndefined' + sortOrder: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrderOrUndefined' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/packs/packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/packs.schema.yaml new file mode 100644 index 0000000000000..64e0ed0e4ffe3 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/packs/packs.schema.yaml @@ -0,0 +1,67 @@ +openapi: 3.0.0 +info: + title: Packs Schema + version: '2023-10-31' +paths: + /api/osquery/packs: + get: + summary: Find packs + parameters: + - $ref: './find_packs.schema.yaml#/components/parameters/FindPacksRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './find_packs.schema.yaml#/components/schemas/SuccessResponse' + post: + summary: Create a packs + requestBody: + required: true + content: + application/json: + schema: + $ref: './create_pack.schema.yaml#/components/schemas/CreatePacksRequestBody' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './create_pack.schema.yaml#/components/schemas/SuccessResponse' + /api/osquery/packs/{id}: + get: + summary: Get packs details + parameters: + - $ref: './read_packs.schema.yaml#/components/parameters/ReadPacksRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './read_packs.schema.yaml#/components/schemas/SuccessResponse' + delete: + summary: Delete packs + parameters: + - $ref: './delete_packs.schema.yaml#/components/parameters/DeletePacksRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './find_packs.schema.yaml#/components/schemas/SuccessResponse' + put: + summary: Update packs + parameters: + - $ref: './update_packs.schema.yaml#/components/parameters/UpdatePacksRequestQueryBody' + - $ref: './update_packs.schema.yaml#/components/parameters/UpdatePacksRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './update_packs.schema.yaml#/components/schemas/SuccessResponse' diff --git a/x-pack/plugins/osquery/common/api/packs/read_packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/read_packs.schema.yaml new file mode 100644 index 0000000000000..8cfe415848c92 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/packs/read_packs.schema.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: Read Saved Queries Schema + version: '2023-10-31' +paths: { } +components: + parameters: + ReadPacksRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/ReadPacksRequestQuery' + schemas: + ReadPacksRequestQuery: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PackId' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/packs/update_packs.schema.yaml b/x-pack/plugins/osquery/common/api/packs/update_packs.schema.yaml new file mode 100644 index 0000000000000..344cb88fad7d4 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/packs/update_packs.schema.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.0 +info: + title: Update Saved Query Schema + version: '2023-10-31' +paths: { } +components: + parameters: + UpdatePacksRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/UpdatePacksRequestParams' + UpdatePacksRequestQueryBody: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/UpdatePacksRequestBody' + schemas: + UpdatePacksRequestParams: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PackId' + UpdatePacksRequestBody: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PackId' + description: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DescriptionOrUndefined' + enabled: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/EnabledOrUndefined' + policy_ids: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PolicyIdsOrUndefined' + shards: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Shards' + queries: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/ObjectQueries' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/create_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/create_saved_query.schema.yaml new file mode 100644 index 0000000000000..9735b0139754e --- /dev/null +++ b/x-pack/plugins/osquery/common/api/saved_query/create_saved_query.schema.yaml @@ -0,0 +1,32 @@ +openapi: 3.0.0 +info: + title: Create Saved Query Schema + version: '2023-10-31' +paths: { } +components: + schemas: + CreateSavedQueryRequestBody: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SavedQueryId' + description: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DescriptionOrUndefined' + query: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/QueryOrUndefined' + ecs_mapping: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/ECSMappingOrUndefined' + version: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/VersionOrUndefined' + platform: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DescriptionOrUndefined' + interval: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Interval' + snapshot: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SnapshotOrUndefined' + removed: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/RemovedOrUndefined' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.schema.yaml new file mode 100644 index 0000000000000..7a180c542aa23 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/saved_query/delete_saved_query.schema.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: Delete Saved Queries Schema + version: '2023-10-31' +paths: { } +components: + parameters: + DeleteSavedQueryRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/DeleteSavedQueryRequestQuery' + schemas: + DeleteSavedQueryRequestQuery: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SavedQueryId' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.schema.yaml new file mode 100644 index 0000000000000..dbebf003a4696 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/saved_query/find_saved_query.schema.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.0 +info: + title: Find Saved Queries Schema + version: '2023-10-31' +paths: { } +components: + parameters: + FindSavedQueryRequestQueryParameter: + name: query + in: query + required: true + schema: + $ref: '#/components/schemas/FindSavedQueryRequestQuery' + schemas: + FindSavedQueryRequestQuery: + type: object + properties: + page: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PageOrUndefined' + pageSize: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/PageSizeOrUndefined' + sort: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrUndefined' + sortOrder: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SortOrderOrUndefined' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.schema.yaml new file mode 100644 index 0000000000000..a5fed00a37e0c --- /dev/null +++ b/x-pack/plugins/osquery/common/api/saved_query/read_saved_query.schema.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: Read Saved Queries Schema + version: '2023-10-31' +paths: { } +components: + parameters: + ReadSavedQueryRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/ReadSavedQueryRequestQuery' + schemas: + ReadSavedQueryRequestQuery: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SavedQueryId' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/saved_query/saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/saved_query.schema.yaml new file mode 100644 index 0000000000000..1cd832370c0cd --- /dev/null +++ b/x-pack/plugins/osquery/common/api/saved_query/saved_query.schema.yaml @@ -0,0 +1,67 @@ +openapi: 3.0.0 +info: + title: Saved Queries Schema + version: '2023-10-31' +paths: + /api/osquery/saved_queries: + get: + summary: Find saved queries + parameters: + - $ref: './find_saved_query.schema.yaml#/components/parameters/FindSavedQueryRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './find_saved_query.schema.yaml#/components/schemas/SuccessResponse' + post: + summary: Create a saved query + requestBody: + required: true + content: + application/json: + schema: + $ref: './create_saved_query.schema.yaml#/components/schemas/CreateSavedQueryRequestBody' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './create_saved_query.schema.yaml#/components/schemas/SuccessResponse' + /api/osquery/saved_queries/{id}: + get: + summary: Get saved query details + parameters: + - $ref: './read_saved_query.schema.yaml#/components/parameters/ReadSavedQueryRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './read_saved_query.schema.yaml#/components/schemas/SuccessResponse' + delete: + summary: Delete saved query + parameters: + - $ref: './delete_saved_query.schema.yaml#/components/parameters/DeleteSavedQueryRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './find_saved_query.schema.yaml#/components/schemas/SuccessResponse' + put: + summary: Update saved query + parameters: + - $ref: './update_saved_query.schema.yaml#/components/parameters/UpdateSavedQueryRequestQueryBody' + - $ref: './update_saved_query.schema.yaml#/components/parameters/UpdateSavedQueryRequestQueryParameter' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './update_saved_query.schema.yaml#/components/schemas/SuccessResponse' diff --git a/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.schema.yaml b/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.schema.yaml new file mode 100644 index 0000000000000..c7004a72adee2 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/saved_query/update_saved_query.schema.yaml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Update Saved Query Schema + version: '2023-10-31' +paths: { } +components: + parameters: + UpdateSavedQueryRequestQueryParameter: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/UpdateSavedQueryRequestParams' + UpdateSavedQueryRequestQueryBody: + name: query + in: path + required: true + schema: + $ref: '#/components/schemas/UpdateSavedQueryRequestBody' + schemas: + UpdateSavedQueryRequestParams: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SavedQueryId' + UpdateSavedQueryRequestBody: + type: object + properties: + id: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SavedQueryId' + description: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DescriptionOrUndefined' + query: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/QueryOrUndefined' + ecs_mapping: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/ECSMappingOrUndefined' + version: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/VersionOrUndefined' + platform: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/DescriptionOrUndefined' + interval: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/IntervalOrUndefined' + snapshot: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/SnapshotOrUndefined' + removed: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/RemovedOrUndefined' + SuccessResponse: + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/status/privileges_check.schema.yaml b/x-pack/plugins/osquery/common/api/status/privileges_check.schema.yaml new file mode 100644 index 0000000000000..2702d1bafa040 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/status/privileges_check.schema.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + title: Osquery privileges Schema + version: '1' +paths: + /internal/osquery/privileges_check: + get: + summary: Get Osquery privileges check + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: { } diff --git a/x-pack/plugins/osquery/common/api/status/status.schema.yaml b/x-pack/plugins/osquery/common/api/status/status.schema.yaml new file mode 100644 index 0000000000000..9ab4d3bd0e607 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/status/status.schema.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + title: Osquery Status Schema + version: '1' +paths: + /internal/osquery/status: + get: + summary: Get Osquery installation status + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: { } diff --git a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts index 4011d2db427a2..a84cdb5013047 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts @@ -111,6 +111,7 @@ describe('ALL - Add Integration', { tags: ['@ess', '@brokenInServerless'] }, () cy.getBySel('createAgentPolicyFlyoutBtn').click(); cy.getBySel('agentPolicyNameLink').contains(policyName).click(); cy.getBySel('addPackagePolicyButton').click(); + cy.getBySel('epmList.searchBar').type('osquery'); cy.getBySel('integration-card:epr:osquery_manager').click(); cy.getBySel('addIntegrationPolicyButton').click(); cy.getBySel('agentPolicySelect').within(() => { diff --git a/x-pack/plugins/osquery/cypress/e2e/all/live_query_packs.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/live_query_packs.cy.ts index e4edd56ff3701..fb5566ba3c393 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/live_query_packs.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/live_query_packs.cy.ts @@ -76,7 +76,6 @@ describe('ALL - Live Query Packs', { tags: ['@serverless', '@ess'] }, () => { selectAllAgents(); submitQuery(); cy.getBySel('live-query-loading').should('exist'); - cy.getBySel('live-query-loading', { timeout: 10000 }).should('not.exist'); cy.getBySel('toggleIcon-system_memory_linux_elastic').click(); checkResults(); checkActionItemsInResults({ diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts index 96cf4a285ccc7..15fe843139174 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts @@ -55,6 +55,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.visit(FLEET_AGENT_POLICIES); cy.contains(AGENT_POLICY_NAME).click(); cy.contains('Add integration').click(); + cy.getBySel('epmList.searchBar').type('osquery'); cy.contains(integration).click(); addIntegration(AGENT_POLICY_NAME); cy.contains('Add Elastic Agent later').click(); @@ -221,6 +222,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.contains(`Agent policy '${agentPolicy}' created`).click(); cy.contains(agentPolicy).click(); cy.contains('Add integration').click(); + cy.getBySel('epmList.searchBar').type('osquery'); cy.contains(integration).click(); addIntegration(agentPolicy); cy.contains('Add Elastic Agent later').click(); diff --git a/x-pack/plugins/osquery/server/handlers/action/create_action_handler.ts b/x-pack/plugins/osquery/server/handlers/action/create_action_handler.ts index a6217f7c916fa..d60c7bae8d9b3 100644 --- a/x-pack/plugins/osquery/server/handlers/action/create_action_handler.ts +++ b/x-pack/plugins/osquery/server/handlers/action/create_action_handler.ts @@ -7,8 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import moment from 'moment'; -import { filter, flatten, isEmpty, map, omit, pick, pickBy, some } from 'lodash'; -import { AGENT_ACTIONS_INDEX } from '@kbn/fleet-plugin/common'; +import { filter, isEmpty, map, omit, pick, pickBy, some } from 'lodash'; import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import type { CreateLiveQueryRequestBodySchema } from '../../../common/api'; @@ -44,6 +43,7 @@ export const createActionHandler = async ( const internalSavedObjectsClient = await getInternalSavedObjectsClient( osqueryContext.getStartServices ); + const { soClient, metadata, alertData, error } = options; const savedObjectsClient = soClient ?? coreStartServices.savedObjects.createInternalRepository(); @@ -85,7 +85,7 @@ export const createActionHandler = async ( pack_id: params.pack_id, pack_name: packSO?.attributes?.name, pack_prebuilt: params.pack_id - ? !!some(packSO?.references, ['type', 'osquery-pack-asset']) + ? some(packSO?.references, ['type', 'osquery-pack-asset']) : undefined, queries: packSO ? map(convertSOQueriesToPack(packSO.attributes.queries), (packQuery, packQueryId) => { @@ -131,12 +131,7 @@ export const createActionHandler = async ( : []; if (fleetActions.length) { - await esClientInternal.bulk({ - refresh: 'wait_for', - body: flatten( - fleetActions.map((action) => [{ index: { _index: AGENT_ACTIONS_INDEX } }, action]) - ), - }); + await osqueryContext.service.getFleetActionsClient()?.bulkCreate(fleetActions); } const actionsComponentTemplateExists = await esClientInternal.indices.exists({ @@ -146,7 +141,7 @@ export const createActionHandler = async ( if (actionsComponentTemplateExists) { await esClientInternal.bulk({ refresh: 'wait_for', - body: [{ index: { _index: `${ACTIONS_INDEX}-default` } }, osqueryAction], + operations: [{ index: { _index: `${ACTIONS_INDEX}-default` } }, osqueryAction], }); } diff --git a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts index 25d668c16d93a..779715ea48aea 100644 --- a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts +++ b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts @@ -16,13 +16,18 @@ import type { } from '@kbn/fleet-plugin/server'; import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server'; import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; +import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions'; import type { ConfigType } from '../../common/config'; import type { TelemetryEventsSender } from './telemetry/sender'; export type OsqueryAppContextServiceStartContract = Partial< Pick< FleetStartContract, - 'agentService' | 'packageService' | 'packagePolicyService' | 'agentPolicyService' + | 'agentService' + | 'packageService' + | 'packagePolicyService' + | 'agentPolicyService' + | 'createFleetActionsClient' > > & { logger: Logger; @@ -41,6 +46,7 @@ export class OsqueryAppContextService { private packagePolicyService: PackagePolicyClient | undefined; private agentPolicyService: AgentPolicyServiceInterface | undefined; private ruleRegistryService: RuleRegistryPluginStartContract | undefined; + private fleetActionsClient: FleetActionsClientInterface | undefined; public start(dependencies: OsqueryAppContextServiceStartContract) { this.agentService = dependencies.agentService; @@ -48,6 +54,7 @@ export class OsqueryAppContextService { this.packagePolicyService = dependencies.packagePolicyService; this.agentPolicyService = dependencies.agentPolicyService; this.ruleRegistryService = dependencies.ruleRegistryService; + this.fleetActionsClient = dependencies.createFleetActionsClient?.('osquery'); } // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -72,6 +79,10 @@ export class OsqueryAppContextService { public getRuleRegistryService(): RuleRegistryPluginStartContract | undefined { return this.ruleRegistryService; } + + public getFleetActionsClient(): FleetActionsClientInterface | undefined { + return this.fleetActionsClient; + } } /** diff --git a/x-pack/plugins/osquery/server/routes/live_query/find_live_query_route.ts b/x-pack/plugins/osquery/server/routes/live_query/find_live_query_route.ts index 2258843b73227..008964f2468b5 100644 --- a/x-pack/plugins/osquery/server/routes/live_query/find_live_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/live_query/find_live_query_route.ts @@ -29,7 +29,7 @@ export const findLiveQueryRoute = (router: IRouter) = .get({ access: 'public', path: '/api/osquery/live_queries', - options: { tags: [`access:${PLUGIN_ID}-read`] }, + options: { tags: ['api', `access:${PLUGIN_ID}-read`] }, }) .addVersion( { diff --git a/x-pack/plugins/profiling/common/base64.ts b/x-pack/plugins/profiling/common/base64.ts deleted file mode 100644 index 0d724c142271a..0000000000000 --- a/x-pack/plugins/profiling/common/base64.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const safeBase64Decoder = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, - 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, - 45, 46, 47, 48, 49, 50, 51, 0, 0, 0, 0, 0, -]; - -export const safeBase64Encoder = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234456789-_'; - -/* eslint no-bitwise: ["error", { "allow": ["&"] }] */ -export function charCodeAt(input: string, i: number): number { - return safeBase64Decoder[input.charCodeAt(i) & 0x7f]; -} diff --git a/x-pack/plugins/profiling/common/columnar_view_model.test.ts b/x-pack/plugins/profiling/common/columnar_view_model.test.ts index c43a1aa86ba63..d967650c76c7f 100644 --- a/x-pack/plugins/profiling/common/columnar_view_model.test.ts +++ b/x-pack/plugins/profiling/common/columnar_view_model.test.ts @@ -7,12 +7,14 @@ import { sum } from 'lodash'; -import { createCalleeTree } from './callee'; +import { createCalleeTree } from '@kbn/profiling-data-access-plugin/common/callee'; import { createColumnarViewModel } from './columnar_view_model'; -import { createBaseFlameGraph, createFlameGraph } from './flamegraph'; -import { decodeStackTraceResponse } from './stack_traces'; - -import { stackTraceFixtures } from './__fixtures__/stacktraces'; +import { + createBaseFlameGraph, + createFlameGraph, +} from '@kbn/profiling-data-access-plugin/common/flamegraph'; +import { decodeStackTraceResponse } from '@kbn/profiling-data-access-plugin/common/stack_traces'; +import { stackTraceFixtures } from '@kbn/profiling-data-access-plugin/common/__fixtures__/stacktraces'; describe('Columnar view model operations', () => { stackTraceFixtures.forEach(({ response, seconds, upsampledBy }) => { diff --git a/x-pack/plugins/profiling/common/columnar_view_model.ts b/x-pack/plugins/profiling/common/columnar_view_model.ts index 20bf2b2761855..e4d2386a0bd74 100644 --- a/x-pack/plugins/profiling/common/columnar_view_model.ts +++ b/x-pack/plugins/profiling/common/columnar_view_model.ts @@ -6,8 +6,7 @@ */ import { ColumnarViewModel } from '@elastic/charts'; - -import { ElasticFlameGraph } from './flamegraph'; +import { ElasticFlameGraph } from '@kbn/profiling-data-access-plugin/common/flamegraph'; import { frameTypeToRGB, rgbToRGBA } from './frame_type_colors'; function normalize(n: number, lower: number, upper: number): number { diff --git a/x-pack/plugins/profiling/common/frame_type_colors.ts b/x-pack/plugins/profiling/common/frame_type_colors.ts index 11c8cbaca9f30..59e8a6004c4e6 100644 --- a/x-pack/plugins/profiling/common/frame_type_colors.ts +++ b/x-pack/plugins/profiling/common/frame_type_colors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FrameType } from './profiling'; +import { FrameType } from '@kbn/profiling-data-access-plugin/common/profiling'; /* * Helper to calculate the color of a given block to be drawn. The desirable outcomes of this are: diff --git a/x-pack/plugins/profiling/common/functions.test.ts b/x-pack/plugins/profiling/common/functions.test.ts index 1ba31d397a338..4c70cca80d7ba 100644 --- a/x-pack/plugins/profiling/common/functions.test.ts +++ b/x-pack/plugins/profiling/common/functions.test.ts @@ -6,11 +6,9 @@ */ import { sum } from 'lodash'; - import { createTopNFunctions } from './functions'; -import { decodeStackTraceResponse } from './stack_traces'; - -import { stackTraceFixtures } from './__fixtures__/stacktraces'; +import { decodeStackTraceResponse } from '@kbn/profiling-data-access-plugin/common/stack_traces'; +import { stackTraceFixtures } from '@kbn/profiling-data-access-plugin/common/__fixtures__/stacktraces'; describe('TopN function operations', () => { stackTraceFixtures.forEach(({ response, seconds, upsampledBy }) => { diff --git a/x-pack/plugins/profiling/common/functions.ts b/x-pack/plugins/profiling/common/functions.ts index 304c56b81e906..dac08c97abbf7 100644 --- a/x-pack/plugins/profiling/common/functions.ts +++ b/x-pack/plugins/profiling/common/functions.ts @@ -6,7 +6,10 @@ */ import * as t from 'io-ts'; import { sumBy } from 'lodash'; -import { createFrameGroupID, FrameGroupID } from './frame_group'; +import { + createFrameGroupID, + FrameGroupID, +} from '@kbn/profiling-data-access-plugin/common/frame_group'; import { createStackFrameMetadata, emptyExecutable, @@ -19,7 +22,7 @@ import { StackFrameMetadata, StackTrace, StackTraceID, -} from './profiling'; +} from '@kbn/profiling-data-access-plugin/common/profiling'; interface TopNFunctionAndFrameGroup { Frame: StackFrameMetadata; diff --git a/x-pack/plugins/profiling/common/index.ts b/x-pack/plugins/profiling/common/index.ts index 2713ed3f98a13..f6266b606ee5a 100644 --- a/x-pack/plugins/profiling/common/index.ts +++ b/x-pack/plugins/profiling/common/index.ts @@ -41,18 +41,6 @@ export function timeRangeFromRequest(request: any): [number, number] { return [timeFrom, timeTo]; } -// Converts from a Map object to a Record object since Map objects are not -// serializable to JSON by default -export function fromMapToRecord(m: Map): Record { - const output: Record = {}; - - for (const [key, value] of m) { - output[key] = value; - } - - return output; -} - export const NOT_AVAILABLE_LABEL = i18n.translate('xpack.profiling.notAvailableLabel', { defaultMessage: 'N/A', }); diff --git a/x-pack/plugins/profiling/common/run_length_encoding.test.ts b/x-pack/plugins/profiling/common/run_length_encoding.test.ts deleted file mode 100644 index 4831177075fc2..0000000000000 --- a/x-pack/plugins/profiling/common/run_length_encoding.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { runLengthDecode, runLengthDecodeBase64Url, runLengthEncode } from './run_length_encoding'; - -describe('Run-length encoding operations', () => { - test('run length is fully reversible', () => { - const tests: number[][] = [[], [0], [0, 1, 2, 3], [0, 1, 1, 2, 2, 2, 3, 3, 3, 3]]; - - for (const t of tests) { - expect(runLengthDecode(runLengthEncode(t))).toEqual(t); - } - }); - - test('runLengthDecode with optional parameter', () => { - const tests: Array<{ - bytes: Buffer; - expected: number[]; - }> = [ - { - bytes: Buffer.from([0x5, 0x0, 0x2, 0x2]), - expected: [0, 0, 0, 0, 0, 2, 2], - }, - { - bytes: Buffer.from([0x1, 0x8]), - expected: [8], - }, - ]; - - for (const t of tests) { - expect(runLengthDecode(t.bytes, t.expected.length)).toEqual(t.expected); - } - }); - - test('runLengthDecode with larger output than available input', () => { - const bytes = Buffer.from([0x5, 0x0, 0x2, 0x2]); - const decoded = [0, 0, 0, 0, 0, 2, 2]; - const expected = decoded.concat(Array(decoded.length).fill(0)); - - expect(runLengthDecode(bytes, expected.length)).toEqual(expected); - }); - - test('runLengthDecode without optional parameter', () => { - const tests: Array<{ - bytes: Buffer; - expected: number[]; - }> = [ - { - bytes: Buffer.from([0x5, 0x0, 0x2, 0x2]), - expected: [0, 0, 0, 0, 0, 2, 2], - }, - { - bytes: Buffer.from([0x1, 0x8]), - expected: [8], - }, - ]; - - for (const t of tests) { - expect(runLengthDecode(t.bytes)).toEqual(t.expected); - } - }); - - test('runLengthDecode works for very long runs', () => { - const tests: Array<{ - bytes: Buffer; - expected: number[]; - }> = [ - { - bytes: Buffer.from([0x5, 0x2, 0xff, 0x0]), - expected: [2, 2, 2, 2, 2].concat(Array(255).fill(0)), - }, - { - bytes: Buffer.from([0xff, 0x2, 0x1, 0x2]), - expected: Array(256).fill(2), - }, - ]; - - for (const t of tests) { - expect(runLengthDecode(t.bytes)).toEqual(t.expected); - } - }); - - test('runLengthEncode works for very long runs', () => { - const tests: Array<{ - numbers: number[]; - expected: Buffer; - }> = [ - { - numbers: [2, 2, 2, 2, 2].concat(Array(255).fill(0)), - expected: Buffer.from([0x5, 0x2, 0xff, 0x0]), - }, - { - numbers: Array(256).fill(2), - expected: Buffer.from([0xff, 0x2, 0x1, 0x2]), - }, - ]; - - for (const t of tests) { - expect(runLengthEncode(t.numbers)).toEqual(t.expected); - } - }); - - test('runLengthDecodeBase64Url', () => { - const tests: Array<{ - data: string; - expected: number[]; - }> = [ - { - data: 'CQM', - expected: [3, 3, 3, 3, 3, 3, 3, 3, 3], - }, - { - data: 'AQkBCAEHAQYBBQEEAQMBAgEBAQA', - expected: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0], - }, - { - data: 'EgMHBA', - expected: Array(18).fill(3).concat(Array(7).fill(4)), - }, - { - data: 'CAMfBQIDEAQ', - expected: Array(8) - .fill(3) - .concat(Array(31).fill(5)) - .concat([3, 3]) - .concat(Array(16).fill(4)), - }, - ]; - - for (const t of tests) { - expect(runLengthDecodeBase64Url(t.data, t.data.length, t.expected.length)).toEqual( - t.expected - ); - } - }); - - test('runLengthDecodeBase64Url with larger output than available input', () => { - const data = Buffer.from([0x5, 0x0, 0x3, 0x2]).toString('base64url'); - const decoded = [0, 0, 0, 0, 0, 2, 2, 2]; - const expected = decoded.concat(Array(decoded.length).fill(0)); - - expect(runLengthDecodeBase64Url(data, data.length, expected.length)).toEqual(expected); - }); - - test('runLengthDecodeBase64Url works for very long runs', () => { - const tests: Array<{ - data: string; - expected: number[]; - }> = [ - { - data: Buffer.from([0x5, 0x2, 0xff, 0x0]).toString('base64url'), - expected: [2, 2, 2, 2, 2].concat(Array(255).fill(0)), - }, - { - data: Buffer.from([0xff, 0x2, 0x1, 0x2]).toString('base64url'), - expected: Array(256).fill(2), - }, - ]; - - for (const t of tests) { - expect(runLengthDecodeBase64Url(t.data, t.data.length, t.expected.length)).toEqual( - t.expected - ); - } - }); -}); diff --git a/x-pack/plugins/profiling/common/run_length_encoding.ts b/x-pack/plugins/profiling/common/run_length_encoding.ts deleted file mode 100644 index 813c46eb09d91..0000000000000 --- a/x-pack/plugins/profiling/common/run_length_encoding.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { charCodeAt } from './base64'; - -// runLengthEncode run-length encodes the input array. -// -// The input is a list of uint8s. The output is a binary stream of -// 2-byte pairs (first byte is the length and the second byte is the -// binary representation of the object) in reverse order. -// -// E.g. uint8 array [0, 0, 0, 0, 0, 2, 2, 2] is converted into the byte -// array [5, 0, 3, 2]. -export function runLengthEncode(input: number[]): Buffer { - const output: number[] = []; - - if (input.length === 0) { - return Buffer.from(output); - } - - let count = 1; - let current = input[0]; - - for (let i = 1; i < input.length; i++) { - const next = input[i]; - - if (next === current && count < 255) { - count++; - continue; - } - - output.push(count, current); - - count = 1; - current = next; - } - - output.push(count, current); - - return Buffer.from(output); -} - -function copyNumber(target: number[], value: number, offset: number, end: number) { - for (let i = offset; i < end; i++) { - target[i] = value; - } -} - -// runLengthDecode decodes a run-length encoding for the input array. -// -// The input is a binary stream of 2-byte pairs (first byte is the length and the -// second byte is the binary representation of the object). The output is a list of -// uint8s. -// -// E.g. byte array [5, 0, 3, 2] is converted into an uint8 array like -// [0, 0, 0, 0, 0, 2, 2, 2]. -export function runLengthDecode(input: Buffer, outputSize?: number): number[] { - let size; - - if (typeof outputSize === 'undefined') { - size = 0; - for (let i = 0; i < input.length; i += 2) { - size += input[i]; - } - } else { - size = outputSize; - } - - const output: number[] = new Array(size); - - let idx = 0; - for (let i = 0; i < input.length; i += 2) { - for (let j = 0; j < input[i]; j++) { - output[idx] = input[i + 1]; - idx++; - } - } - - // Due to truncation of the frame types for stacktraces longer than 255, - // the expected output size and the actual decoded size can be different. - // Ordinarily, these two values should be the same. - // - // We have decided to fill in the remainder of the output array with zeroes - // as a reasonable default. Without this step, the output array would have - // undefined values. - copyNumber(output, 0, idx, size); - - return output; -} - -// runLengthDecodeBase64Url decodes a run-length encoding for the -// base64-encoded input string. -// -// The input is a base64-encoded string. The output is a list of uint8s. -// -// E.g. string 'BQADAg' is converted into an uint8 array like -// [0, 0, 0, 0, 0, 2, 2, 2]. -// -// The motivating intent for this method is to unpack a base64-encoded -// run-length encoding without using intermediate storage. -// -// This method relies on these assumptions and details: -// - array encoded using run-length and base64 always returns string of length -// 0, 3, or 6 (mod 8) -// - since original array is composed of uint8s, we ignore Unicode codepoints -// - JavaScript bitwise operators operate on 32-bits so decoding must be done -// in 32-bit chunks - -/* eslint no-bitwise: ["error", { "allow": ["<<", ">>", ">>=", "&", "|"] }] */ -export function runLengthDecodeBase64Url(input: string, size: number, capacity: number): number[] { - const output = new Array(capacity); - const multipleOf8 = Math.floor(size / 8); - const remainder = size % 8; - - let n = 0; - let count = 0; - let value = 0; - let i = 0; - let j = 0; - - for (i = 0; i < multipleOf8 * 8; i += 8) { - n = - (charCodeAt(input, i) << 26) | - (charCodeAt(input, i + 1) << 20) | - (charCodeAt(input, i + 2) << 14) | - (charCodeAt(input, i + 3) << 8) | - (charCodeAt(input, i + 4) << 2) | - (charCodeAt(input, i + 5) >> 4); - - count = (n >> 24) & 0xff; - value = (n >> 16) & 0xff; - - copyNumber(output, value, j, j + count); - j += count; - - count = (n >> 8) & 0xff; - value = n & 0xff; - - copyNumber(output, value, j, j + count); - j += count; - - n = - ((charCodeAt(input, i + 5) & 0xf) << 12) | - (charCodeAt(input, i + 6) << 6) | - charCodeAt(input, i + 7); - - count = (n >> 8) & 0xff; - value = n & 0xff; - - copyNumber(output, value, j, j + count); - j += count; - } - - if (remainder === 6) { - n = - (charCodeAt(input, i) << 26) | - (charCodeAt(input, i + 1) << 20) | - (charCodeAt(input, i + 2) << 14) | - (charCodeAt(input, i + 3) << 8) | - (charCodeAt(input, i + 4) << 2) | - (charCodeAt(input, i + 5) >> 4); - - count = (n >> 24) & 0xff; - value = (n >> 16) & 0xff; - - copyNumber(output, value, j, j + count); - j += count; - - count = (n >> 8) & 0xff; - value = n & 0xff; - - copyNumber(output, value, j, j + count); - j += count; - } else if (remainder === 3) { - n = (charCodeAt(input, i) << 12) | (charCodeAt(input, i + 1) << 6) | charCodeAt(input, i + 2); - n >>= 2; - - count = (n >> 8) & 0xff; - value = n & 0xff; - - copyNumber(output, value, j, j + count); - j += count; - } - - // Due to truncation of the frame types for stacktraces longer than 255, - // the expected output size and the actual decoded size can be different. - // Ordinarily, these two values should be the same. - // - // We have decided to fill in the remainder of the output array with zeroes - // as a reasonable default. Without this step, the output array would have - // undefined values. - copyNumber(output, 0, j, capacity); - - return output; -} diff --git a/x-pack/plugins/profiling/common/topn.ts b/x-pack/plugins/profiling/common/topn.ts index 41d820729b48f..cd58e35c6442f 100644 --- a/x-pack/plugins/profiling/common/topn.ts +++ b/x-pack/plugins/profiling/common/topn.ts @@ -9,9 +9,9 @@ import { euiPaletteColorBlind } from '@elastic/eui'; import { InferSearchResponseOf } from '@kbn/es-types'; import { i18n } from '@kbn/i18n'; import { orderBy } from 'lodash'; -import { ProfilingESField } from './elasticsearch'; +import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch'; +import { StackFrameMetadata } from '@kbn/profiling-data-access-plugin/common/profiling'; import { createUniformBucketsForTimeRange } from './histogram'; -import { StackFrameMetadata } from './profiling'; export const OTHER_BUCKET_LABEL = i18n.translate('xpack.profiling.topn.otherBucketLabel', { defaultMessage: 'Other', diff --git a/x-pack/plugins/profiling/kibana.jsonc b/x-pack/plugins/profiling/kibana.jsonc index 2985456d4ec8b..612e8d7936c25 100644 --- a/x-pack/plugins/profiling/kibana.jsonc +++ b/x-pack/plugins/profiling/kibana.jsonc @@ -23,12 +23,13 @@ "observabilityShared", "observabilityAIAssistant", "unifiedSearch", - "share" + "share", + "profilingDataAccess" ], "requiredBundles": [ "kibanaReact", "kibanaUtils", - "observabilityAIAssistant", + "observabilityAIAssistant" ] } } diff --git a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx index 5f4bc8cfab9cc..7d02f4b480350 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx @@ -19,7 +19,7 @@ import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { Maybe } from '@kbn/observability-plugin/common/typings'; import React, { useEffect, useMemo, useState } from 'react'; import { useUiTracker } from '@kbn/observability-shared-plugin/public'; -import { ElasticFlameGraph } from '../../../common/flamegraph'; +import { ElasticFlameGraph } from '@kbn/profiling-data-access-plugin/common/flamegraph'; import { getFlamegraphModel } from '../../utils/get_flamegraph_model'; import { FlameGraphLegend } from './flame_graph_legend'; import { FrameInformationWindow } from '../frame_information_window'; diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/get_information_rows.ts b/x-pack/plugins/profiling/public/components/frame_information_window/get_information_rows.ts index 057f56f9c2f06..8c2fa0ebd0763 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/get_information_rows.ts +++ b/x-pack/plugins/profiling/public/components/frame_information_window/get_information_rows.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; +import { describeFrameType } from '@kbn/profiling-data-access-plugin/common/profiling'; import { NOT_AVAILABLE_LABEL } from '../../../common'; -import { describeFrameType } from '../../../common/profiling'; export function getInformationRows({ fileID, diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx index d0925897877fc..96879a59fc737 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx @@ -13,7 +13,10 @@ import { useObservabilityAIAssistant, } from '@kbn/observability-ai-assistant-plugin/public'; import React, { useMemo } from 'react'; -import { FrameSymbolStatus, getFrameSymbolStatus } from '../../../common/profiling'; +import { + FrameSymbolStatus, + getFrameSymbolStatus, +} from '@kbn/profiling-data-access-plugin/common/profiling'; import { FrameInformationPanel } from './frame_information_panel'; import { getImpactRows } from './get_impact_rows'; import { getInformationRows } from './get_information_rows'; diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/missing_symbols_callout.stories.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/missing_symbols_callout.stories.tsx index ba353d48c6995..aba91dcc4127a 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/missing_symbols_callout.stories.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/missing_symbols_callout.stories.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Meta } from '@storybook/react'; import React from 'react'; -import { FrameType } from '../../../common/profiling'; +import { FrameType } from '@kbn/profiling-data-access-plugin/common/profiling'; import { MockProfilingDependenciesStorybook } from '../contexts/profiling_dependencies/mock_profiling_dependencies_storybook'; import { MissingSymbolsCallout } from './missing_symbols_callout'; diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/missing_symbols_callout.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/missing_symbols_callout.tsx index ff86ba5628c27..1f9d40d23a648 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/missing_symbols_callout.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/missing_symbols_callout.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { FrameType, getLanguageType } from '../../../common/profiling'; +import { FrameType, getLanguageType } from '@kbn/profiling-data-access-plugin/common/profiling'; import { PROFILING_FEEDBACK_LINK } from '../profiling_app_page_template'; import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies'; import { useProfilingRouter } from '../../hooks/use_profiling_router'; diff --git a/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx b/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx index 3a6945f7f2d7f..eb50e1b3ea941 100644 --- a/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx +++ b/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx @@ -4,7 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiHeaderLinks, EuiIcon } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHeaderLink, + EuiHeaderLinks, + EuiIcon, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import qs from 'query-string'; import React from 'react'; @@ -20,32 +27,38 @@ export function ProfilingHeaderActionMenu() { return ( - { - const query = qs.parse(window.location.search); - const storageExplorerURL = url.format({ - pathname: '/storage-explorer', - query: { - kuery: query.kuery, - rangeFrom: query.rangeFrom, - rangeTo: query.rangeTo, - }, - }); - history.push(storageExplorerURL); - }} + - - - - - - {i18n.translate('xpack.profiling.headerActionMenu.storageExplorer', { - defaultMessage: 'Storage Explorer', - })} - - - + { + const query = qs.parse(window.location.search); + const storageExplorerURL = url.format({ + pathname: '/storage-explorer', + query: { + kuery: query.kuery, + rangeFrom: query.rangeFrom || 'now-15m', + rangeTo: query.rangeTo || 'now', + }, + }); + history.push(storageExplorerURL); + }} + > + + + + + + {i18n.translate('xpack.profiling.headerActionMenu.storageExplorer', { + defaultMessage: 'Storage Explorer', + })} + + + + {i18n.translate('xpack.profiling.headerActionMenu.addData', { - defaultMessage: 'Add data', + defaultMessage: 'Add Data', })} diff --git a/x-pack/plugins/profiling/public/components/stack_frame_summary/index.tsx b/x-pack/plugins/profiling/public/components/stack_frame_summary/index.tsx index a92934df79009..5533df8479d4e 100644 --- a/x-pack/plugins/profiling/public/components/stack_frame_summary/index.tsx +++ b/x-pack/plugins/profiling/public/components/stack_frame_summary/index.tsx @@ -6,7 +6,11 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import React from 'react'; -import { getCalleeFunction, getCalleeSource, StackFrameMetadata } from '../../../common/profiling'; +import { + getCalleeFunction, + getCalleeSource, + StackFrameMetadata, +} from '@kbn/profiling-data-access-plugin/common/profiling'; interface Props { frame: StackFrameMetadata; diff --git a/x-pack/plugins/profiling/public/components/subchart.tsx b/x-pack/plugins/profiling/public/components/subchart.tsx index 72c869e85c3a3..ee388b851d0ed 100644 --- a/x-pack/plugins/profiling/public/components/subchart.tsx +++ b/x-pack/plugins/profiling/public/components/subchart.tsx @@ -32,7 +32,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { StackFrameMetadata } from '../../common/profiling'; +import { StackFrameMetadata } from '@kbn/profiling-data-access-plugin/common/profiling'; import { CountPerTime, OTHER_BUCKET_LABEL, TopNSample } from '../../common/topn'; import { useKibanaTimeZoneSetting } from '../hooks/use_kibana_timezone_setting'; import { useProfilingChartsTheme } from '../hooks/use_profiling_charts_theme'; diff --git a/x-pack/plugins/profiling/public/components/topn_functions/utils.ts b/x-pack/plugins/profiling/public/components/topn_functions/utils.ts index f44454d255601..f4524425883d1 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/utils.ts +++ b/x-pack/plugins/profiling/public/components/topn_functions/utils.ts @@ -5,8 +5,8 @@ * 2.0. */ import { keyBy } from 'lodash'; +import { StackFrameMetadata } from '@kbn/profiling-data-access-plugin/common/profiling'; import { TopNFunctions } from '../../../common/functions'; -import { StackFrameMetadata } from '../../../common/profiling'; import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates'; export function getColorLabel(percent: number) { diff --git a/x-pack/plugins/profiling/public/routing/index.tsx b/x-pack/plugins/profiling/public/routing/index.tsx index 9e08cfbc53fec..b6dcebc3ec18d 100644 --- a/x-pack/plugins/profiling/public/routing/index.tsx +++ b/x-pack/plugins/profiling/public/routing/index.tsx @@ -9,8 +9,11 @@ import { toNumberRt } from '@kbn/io-ts-utils'; import { createRouter, Outlet } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; +import { + StackTracesDisplayOption, + TopNType, +} from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { TopNFunctionSortField, topNFunctionSortFieldRt } from '../../common/functions'; -import { StackTracesDisplayOption, TopNType } from '../../common/stack_traces'; import { indexLifecyclePhaseRt, IndexLifecyclePhaseSelectOption, diff --git a/x-pack/plugins/profiling/public/services.ts b/x-pack/plugins/profiling/public/services.ts index 1a2a684f96e15..6fbe64f57d025 100644 --- a/x-pack/plugins/profiling/public/services.ts +++ b/x-pack/plugins/profiling/public/services.ts @@ -5,8 +5,12 @@ * 2.0. */ import { HttpFetchQuery } from '@kbn/core/public'; +import { + BaseFlameGraph, + createFlameGraph, + ElasticFlameGraph, +} from '@kbn/profiling-data-access-plugin/common/flamegraph'; import { getRoutePaths } from '../common'; -import { BaseFlameGraph, createFlameGraph, ElasticFlameGraph } from '../common/flamegraph'; import { TopNFunctions } from '../common/functions'; import type { IndexLifecyclePhaseSelectOption, diff --git a/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts b/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts index 9e0ec087523ad..6ba179ac44d9c 100644 --- a/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts +++ b/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts @@ -8,10 +8,10 @@ import { ColumnarViewModel } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import d3 from 'd3'; import { compact, range, sum, uniqueId } from 'lodash'; +import { describeFrameType, FrameType } from '@kbn/profiling-data-access-plugin/common/profiling'; +import { ElasticFlameGraph } from '@kbn/profiling-data-access-plugin/common/flamegraph'; import { createColumnarViewModel } from '../../../common/columnar_view_model'; -import { ElasticFlameGraph } from '../../../common/flamegraph'; import { FRAME_TYPE_COLOR_MAP, rgbToRGBA } from '../../../common/frame_type_colors'; -import { describeFrameType, FrameType } from '../../../common/profiling'; import { ComparisonMode } from '../../components/normalization_menu'; import { getInterpolationValue } from './get_interpolation_value'; diff --git a/x-pack/plugins/profiling/public/views/add_data_view/index.tsx b/x-pack/plugins/profiling/public/views/add_data_view/index.tsx index 066f5167d1721..6025e6685c19d 100644 --- a/x-pack/plugins/profiling/public/views/add_data_view/index.tsx +++ b/x-pack/plugins/profiling/public/views/add_data_view/index.tsx @@ -330,7 +330,7 @@ EOF`} {i18n.translate( 'xpack.profiling.tabs.elasticAgentIntegration.step1.collectionAgentUrl', - { defaultMessage: 'Universal Profiling Collector url:' } + { defaultMessage: 'Universal Profiling Collector URL:' } )} diff --git a/x-pack/plugins/profiling/public/views/stack_traces_view/get_stack_traces_tabs.ts b/x-pack/plugins/profiling/public/views/stack_traces_view/get_stack_traces_tabs.ts index 3b06074ba9db9..b1a70254348ab 100644 --- a/x-pack/plugins/profiling/public/views/stack_traces_view/get_stack_traces_tabs.ts +++ b/x-pack/plugins/profiling/public/views/stack_traces_view/get_stack_traces_tabs.ts @@ -8,7 +8,7 @@ import { EuiPageHeaderContentProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TypeOf } from '@kbn/typed-react-router-config'; -import { TopNType } from '../../../common/stack_traces'; +import { TopNType } from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { StatefulProfilingRouter } from '../../hooks/use_profiling_router'; import { ProfilingRoutes } from '../../routing'; diff --git a/x-pack/plugins/profiling/public/views/stack_traces_view/index.tsx b/x-pack/plugins/profiling/public/views/stack_traces_view/index.tsx index 638df8712e115..0df1f9ae855f9 100644 --- a/x-pack/plugins/profiling/public/views/stack_traces_view/index.tsx +++ b/x-pack/plugins/profiling/public/views/stack_traces_view/index.tsx @@ -7,7 +7,10 @@ import { EuiButton, EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { StackTracesDisplayOption, TopNType } from '../../../common/stack_traces'; +import { + StackTracesDisplayOption, + TopNType, +} from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { groupSamplesByCategory, TopNResponse } from '../../../common/topn'; import { useProfilingParams } from '../../hooks/use_profiling_params'; import { useProfilingRouter } from '../../hooks/use_profiling_router'; diff --git a/x-pack/plugins/profiling/public/views/stack_traces_view/utils.test.ts b/x-pack/plugins/profiling/public/views/stack_traces_view/utils.test.ts index 915b24c53429d..71272b7eda63c 100644 --- a/x-pack/plugins/profiling/public/views/stack_traces_view/utils.test.ts +++ b/x-pack/plugins/profiling/public/views/stack_traces_view/utils.test.ts @@ -4,7 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { StackTracesDisplayOption, TopNType } from '../../../common/stack_traces'; +import { + StackTracesDisplayOption, + TopNType, +} from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { getTracesViewRouteParams } from './utils'; describe('stack traces view utils', () => { diff --git a/x-pack/plugins/profiling/public/views/stack_traces_view/utils.ts b/x-pack/plugins/profiling/public/views/stack_traces_view/utils.ts index 74e13f249108c..7b6b2a0ead397 100644 --- a/x-pack/plugins/profiling/public/views/stack_traces_view/utils.ts +++ b/x-pack/plugins/profiling/public/views/stack_traces_view/utils.ts @@ -6,7 +6,10 @@ */ import { TypeOf } from '@kbn/typed-react-router-config'; -import { getFieldNameForTopNType, TopNType } from '../../../common/stack_traces'; +import { + getFieldNameForTopNType, + TopNType, +} from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { ProfilingRoutes } from '../../routing'; export function getTracesViewRouteParams({ diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/summary.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/summary.tsx index 051765a8880a6..19a3e95809155 100644 --- a/x-pack/plugins/profiling/public/views/storage_explorer/summary.tsx +++ b/x-pack/plugins/profiling/public/views/storage_explorer/summary.tsx @@ -9,7 +9,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel, EuiStat, EuiText } from ' import { i18n } from '@kbn/i18n'; import { asDynamicBytes } from '@kbn/observability-plugin/common'; import React from 'react'; -import { StackTracesDisplayOption, TopNType } from '../../../common/stack_traces'; +import { + StackTracesDisplayOption, + TopNType, +} from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { StorageExplorerSummaryAPIResponse } from '../../../common/storage_explorer'; import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies'; import { LabelWithHint } from '../../components/label_with_hint'; diff --git a/x-pack/plugins/profiling/server/routes/flamechart.ts b/x-pack/plugins/profiling/server/routes/flamechart.ts index 0f78ca0288d81..5dedcc15e0c78 100644 --- a/x-pack/plugins/profiling/server/routes/flamechart.ts +++ b/x-pack/plugins/profiling/server/routes/flamechart.ts @@ -6,21 +6,17 @@ */ import { schema } from '@kbn/config-schema'; - import { RouteRegisterParameters } from '.'; import { getRoutePaths } from '../../common'; -import { createCalleeTree } from '../../common/callee'; import { handleRouteHandlerError } from '../utils/handle_route_error_handler'; -import { createBaseFlameGraph } from '../../common/flamegraph'; -import { withProfilingSpan } from '../utils/with_profiling_span'; import { getClient } from './compat'; -import { createCommonFilter } from './query'; -import { searchStackTraces } from './search_stacktraces'; export function registerFlameChartSearchRoute({ router, logger, - services: { createProfilingEsClient }, + dependencies: { + start: { profilingDataAccess }, + }, }: RouteRegisterParameters) { const paths = getRoutePaths(); router.get( @@ -37,39 +33,15 @@ export function registerFlameChartSearchRoute({ }, async (context, request, response) => { const { timeFrom, timeTo, kuery } = request.query; - const targetSampleSize = 20000; // minimum number of samples to get statistically sound results try { const esClient = await getClient(context); - const profilingElasticsearchClient = createProfilingEsClient({ request, esClient }); - const filter = createCommonFilter({ - timeFrom, - timeTo, + const flamegraph = await profilingDataAccess.services.fetchFlamechartData({ + esClient, + rangeFrom: timeFrom, + rangeTo: timeTo, kuery, }); - const totalSeconds = timeTo - timeFrom; - - const { events, stackTraces, executables, stackFrames, totalFrames, samplingRate } = - await searchStackTraces({ - client: profilingElasticsearchClient, - filter, - sampleSize: targetSampleSize, - }); - - const flamegraph = await withProfilingSpan('create_flamegraph', async () => { - const tree = createCalleeTree( - events, - stackTraces, - stackFrames, - executables, - totalFrames, - samplingRate - ); - - const fg = createBaseFlameGraph(tree, samplingRate, totalSeconds); - - return fg; - }); return response.ok({ body: flamegraph }); } catch (error) { diff --git a/x-pack/plugins/profiling/server/routes/query.ts b/x-pack/plugins/profiling/server/routes/query.ts index f8a776ee68ce7..eed6b272c800e 100644 --- a/x-pack/plugins/profiling/server/routes/query.ts +++ b/x-pack/plugins/profiling/server/routes/query.ts @@ -7,7 +7,7 @@ import { QueryDslBoolQuery } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { kqlQuery } from '@kbn/observability-plugin/server'; -import { ProfilingESField } from '../../common/elasticsearch'; +import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch'; export interface ProjectTimeQuery { bool: QueryDslBoolQuery; diff --git a/x-pack/plugins/profiling/server/routes/search_stacktraces.ts b/x-pack/plugins/profiling/server/routes/search_stacktraces.ts index b90cbaf78bbe3..1e42cd11265ee 100644 --- a/x-pack/plugins/profiling/server/routes/search_stacktraces.ts +++ b/x-pack/plugins/profiling/server/routes/search_stacktraces.ts @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { decodeStackTraceResponse } from '../../common/stack_traces'; +import { decodeStackTraceResponse } from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { ProfilingESClient } from '../utils/create_profiling_es_client'; import { ProjectTimeQuery } from './query'; diff --git a/x-pack/plugins/profiling/server/routes/stacktrace.test.ts b/x-pack/plugins/profiling/server/routes/stacktrace.test.ts deleted file mode 100644 index 91b55312928c3..0000000000000 --- a/x-pack/plugins/profiling/server/routes/stacktrace.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createStackFrameID, StackTrace } from '../../common/profiling'; -import { runLengthEncode } from '../../common/run_length_encoding'; -import { decodeStackTrace, EncodedStackTrace } from './stacktrace'; - -enum fileID { - A = 'aQpJmTLWydNvOapSFZOwKg', - B = 'hz_u-HGyrN6qeIk6UIJeCA', - C = 'AJ8qrcXSoJbl_haPhlc4og', - D = 'lHZiv7a58px6Gumcpo-6yA', - E = 'fkbxUTZgljnk71ZMnqJnyA', - F = 'gnEsgxvvEODj6iFYMQWYlA', -} - -enum addressOrLine { - A = 515512, - B = 26278522, - C = 6712518, - D = 105806025, - E = 111, - F = 106182663, - G = 100965370, -} - -const frameID: Record = { - A: createStackFrameID(fileID.A, addressOrLine.A), - B: createStackFrameID(fileID.B, addressOrLine.B), - C: createStackFrameID(fileID.C, addressOrLine.C), - D: createStackFrameID(fileID.D, addressOrLine.D), - E: createStackFrameID(fileID.E, addressOrLine.E), - F: createStackFrameID(fileID.F, addressOrLine.F), - G: createStackFrameID(fileID.F, addressOrLine.G), -}; - -const frameTypeA = [0, 0, 0]; -const frameTypeB = [8, 8, 8, 8]; - -describe('Stack trace operations', () => { - test('decodeStackTrace', () => { - const tests: Array<{ - original: EncodedStackTrace; - expected: StackTrace; - }> = [ - { - original: { - Stacktrace: { - frame: { - ids: frameID.A + frameID.B + frameID.C, - types: runLengthEncode(frameTypeA).toString('base64url'), - }, - }, - } as EncodedStackTrace, - expected: { - FrameIDs: [frameID.A, frameID.B, frameID.C], - FileIDs: [fileID.A, fileID.B, fileID.C], - AddressOrLines: [addressOrLine.A, addressOrLine.B, addressOrLine.C], - Types: frameTypeA, - } as StackTrace, - }, - { - original: { - Stacktrace: { - frame: { - ids: frameID.D + frameID.E + frameID.F + frameID.G, - types: runLengthEncode(frameTypeB).toString('base64url'), - }, - }, - } as EncodedStackTrace, - expected: { - FrameIDs: [frameID.D, frameID.E, frameID.F, frameID.G], - FileIDs: [fileID.D, fileID.E, fileID.F, fileID.F], - AddressOrLines: [addressOrLine.D, addressOrLine.E, addressOrLine.F, addressOrLine.G], - Types: frameTypeB, - } as StackTrace, - }, - ]; - - for (const t of tests) { - expect(decodeStackTrace(t.original)).toEqual(t.expected); - } - }); -}); diff --git a/x-pack/plugins/profiling/server/routes/stacktrace.ts b/x-pack/plugins/profiling/server/routes/stacktrace.ts deleted file mode 100644 index 0777d8c741161..0000000000000 --- a/x-pack/plugins/profiling/server/routes/stacktrace.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DedotObject, ProfilingESField } from '../../common/elasticsearch'; -import { - getAddressFromStackFrameID, - getFileIDFromStackFrameID, - StackTrace, -} from '../../common/profiling'; -import { runLengthDecodeBase64Url } from '../../common/run_length_encoding'; - -const BASE64_FRAME_ID_LENGTH = 32; - -export type EncodedStackTrace = DedotObject<{ - // This field is a base64-encoded byte string. The string represents a - // serialized list of frame IDs in which the order of frames are - // reversed to allow for prefix compression (leaf frame last). Each - // frame ID is composed of two concatenated values: a 16-byte file ID - // and an 8-byte address or line number (depending on the context of - // the downstream reader). - // - // Frame ID #1 Frame ID #2 - // +----------------+--------+----------------+--------+---- - // | File ID | Addr | File ID | Addr | - // +----------------+--------+----------------+--------+---- - [ProfilingESField.StacktraceFrameIDs]: string; - - // This field is a run-length encoding of a list of uint8s. The order is - // reversed from the original input. - [ProfilingESField.StacktraceFrameTypes]: string; -}>; - -// decodeStackTrace unpacks an encoded stack trace from Elasticsearch -export function decodeStackTrace(input: EncodedStackTrace): StackTrace { - const inputFrameIDs = input.Stacktrace.frame.ids; - const inputFrameTypes = input.Stacktrace.frame.types; - const countsFrameIDs = inputFrameIDs.length / BASE64_FRAME_ID_LENGTH; - - const fileIDs: string[] = new Array(countsFrameIDs); - const frameIDs: string[] = new Array(countsFrameIDs); - const addressOrLines: number[] = new Array(countsFrameIDs); - - // Step 1: Convert the base64-encoded frameID list into two separate - // lists (frame IDs and file IDs), both of which are also base64-encoded. - // - // To get the frame ID, we grab the next 32 bytes. - // - // To get the file ID, we grab the first 22 bytes of the frame ID. - // However, since the file ID is base64-encoded using 21.33 bytes - // (16 * 4 / 3), then the 22 bytes have an extra 4 bits from the - // address (see diagram in definition of EncodedStackTrace). - for (let i = 0, pos = 0; i < countsFrameIDs; i++, pos += BASE64_FRAME_ID_LENGTH) { - const frameID = inputFrameIDs.slice(pos, pos + BASE64_FRAME_ID_LENGTH); - frameIDs[i] = frameID; - fileIDs[i] = getFileIDFromStackFrameID(frameID); - addressOrLines[i] = getAddressFromStackFrameID(frameID); - } - - // Step 2: Convert the run-length byte encoding into a list of uint8s. - const typeIDs = runLengthDecodeBase64Url(inputFrameTypes, inputFrameTypes.length, countsFrameIDs); - - return { - AddressOrLines: addressOrLines, - FileIDs: fileIDs, - FrameIDs: frameIDs, - Types: typeIDs, - } as StackTrace; -} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_breakdown_size_timeseries.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_breakdown_size_timeseries.ts index 57ec31e6bcfbd..529887f7f190d 100644 --- a/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_breakdown_size_timeseries.ts +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_breakdown_size_timeseries.ts @@ -6,7 +6,7 @@ */ import { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; -import { ProfilingESField } from '../../../common/elasticsearch'; +import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch'; import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../../common/histogram'; import { IndexLifecyclePhaseSelectOption, diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_details.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_details.ts index 65a66071431f1..20f3f080597f5 100644 --- a/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_details.ts +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_details.ts @@ -6,7 +6,7 @@ */ import { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; -import { ProfilingESField } from '../../../common/elasticsearch'; +import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch'; import { IndexLifecyclePhaseSelectOption, indexLifeCyclePhaseToDataTier, diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_profiling_hosts_details_by_id.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_profiling_hosts_details_by_id.ts index 6e3216dc0fd43..bc3a0dd581903 100644 --- a/x-pack/plugins/profiling/server/routes/storage_explorer/get_profiling_hosts_details_by_id.ts +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_profiling_hosts_details_by_id.ts @@ -6,7 +6,7 @@ */ import { kqlQuery } from '@kbn/observability-plugin/server'; import { keyBy } from 'lodash'; -import { ProfilingESField } from '../../../common/elasticsearch'; +import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch'; import { ProfilingESClient } from '../../utils/create_profiling_es_client'; interface HostDetails { diff --git a/x-pack/plugins/profiling/server/routes/topn.test.ts b/x-pack/plugins/profiling/server/routes/topn.test.ts index 1276c5dcc94dd..ab88c911ce97a 100644 --- a/x-pack/plugins/profiling/server/routes/topn.test.ts +++ b/x-pack/plugins/profiling/server/routes/topn.test.ts @@ -8,7 +8,7 @@ import { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/types'; import { coreMock } from '@kbn/core/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; -import { ProfilingESField } from '../../common/elasticsearch'; +import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch'; import { ProfilingESClient } from '../utils/create_profiling_es_client'; import { topNElasticSearchQuery } from './topn'; diff --git a/x-pack/plugins/profiling/server/routes/topn.ts b/x-pack/plugins/profiling/server/routes/topn.ts index ac4621cf0ed14..5e3f1cb16e8be 100644 --- a/x-pack/plugins/profiling/server/routes/topn.ts +++ b/x-pack/plugins/profiling/server/routes/topn.ts @@ -7,12 +7,15 @@ import { schema } from '@kbn/config-schema'; import type { Logger } from '@kbn/core/server'; -import { RouteRegisterParameters } from '.'; +import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch'; +import { groupStackFrameMetadataByStackTrace } from '@kbn/profiling-data-access-plugin/common/profiling'; +import { + getFieldNameForTopNType, + TopNType, +} from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { getRoutePaths, INDEX_EVENTS } from '../../common'; -import { ProfilingESField } from '../../common/elasticsearch'; +import { RouteRegisterParameters } from '.'; import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../common/histogram'; -import { groupStackFrameMetadataByStackTrace } from '../../common/profiling'; -import { getFieldNameForTopNType, TopNType } from '../../common/stack_traces'; import { createTopNSamples, getTopNAggregationRequest, TopNResponse } from '../../common/topn'; import { handleRouteHandlerError } from '../utils/handle_route_error_handler'; import { ProfilingESClient } from '../utils/create_profiling_es_client'; diff --git a/x-pack/plugins/profiling/server/types.ts b/x-pack/plugins/profiling/server/types.ts index e1542e6939896..6ee94e238effa 100644 --- a/x-pack/plugins/profiling/server/types.ts +++ b/x-pack/plugins/profiling/server/types.ts @@ -12,6 +12,10 @@ import { SpacesPluginStart, SpacesPluginSetup } from '@kbn/spaces-plugin/server' import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server'; import { FleetSetupContract, FleetStartContract } from '@kbn/fleet-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; +import { + ProfilingDataAccessPluginSetup, + ProfilingDataAccessPluginStart, +} from '@kbn/profiling-data-access-plugin/server'; export interface ProfilingPluginSetupDeps { observability: ObservabilityPluginSetup; @@ -20,6 +24,7 @@ export interface ProfilingPluginSetupDeps { fleet: FleetSetupContract; spaces?: SpacesPluginSetup; usageCollection?: UsageCollectionSetup; + profilingDataAccess: ProfilingDataAccessPluginSetup; } export interface ProfilingPluginStartDeps { @@ -28,6 +33,7 @@ export interface ProfilingPluginStartDeps { cloud: CloudStart; fleet: FleetStartContract; spaces?: SpacesPluginStart; + profilingDataAccess: ProfilingDataAccessPluginStart; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/x-pack/plugins/profiling/server/utils/create_profiling_es_client.ts b/x-pack/plugins/profiling/server/utils/create_profiling_es_client.ts index 8fa67eb23ecfe..1379fe2a56bf2 100644 --- a/x-pack/plugins/profiling/server/utils/create_profiling_es_client.ts +++ b/x-pack/plugins/profiling/server/utils/create_profiling_es_client.ts @@ -10,8 +10,11 @@ import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; import type { KibanaRequest } from '@kbn/core/server'; import { unwrapEsResponse } from '@kbn/observability-plugin/server'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + ProfilingStatusResponse, + StackTraceResponse, +} from '@kbn/profiling-data-access-plugin/common/stack_traces'; import { withProfilingSpan } from './with_profiling_span'; -import { ProfilingStatusResponse, StackTraceResponse } from '../../common/stack_traces'; export function cancelEsRequestOnAbort>( promise: T, diff --git a/x-pack/plugins/profiling/tsconfig.json b/x-pack/plugins/profiling/tsconfig.json index 980249959b215..ff93bada71703 100644 --- a/x-pack/plugins/profiling/tsconfig.json +++ b/x-pack/plugins/profiling/tsconfig.json @@ -47,7 +47,8 @@ "@kbn/licensing-plugin", "@kbn/utility-types", "@kbn/usage-collection-plugin", - "@kbn/observability-ai-assistant-plugin" + "@kbn/observability-ai-assistant-plugin", + "@kbn/profiling-data-access-plugin" // add references to other TypeScript projects the plugin depends on // requiredPlugins from ./kibana.json diff --git a/x-pack/plugins/profiling_data_access/.i18nrc.json b/x-pack/plugins/profiling_data_access/.i18nrc.json new file mode 100644 index 0000000000000..de8ac3249413e --- /dev/null +++ b/x-pack/plugins/profiling_data_access/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "profiling", + "paths": { + "profiling": "." + }, + "translations": [] +} diff --git a/x-pack/plugins/profiling/common/__fixtures__/README.md b/x-pack/plugins/profiling_data_access/common/__fixtures__/README.md similarity index 100% rename from x-pack/plugins/profiling/common/__fixtures__/README.md rename to x-pack/plugins/profiling_data_access/common/__fixtures__/README.md diff --git a/x-pack/plugins/profiling/common/__fixtures__/stacktraces.ts b/x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces.ts similarity index 100% rename from x-pack/plugins/profiling/common/__fixtures__/stacktraces.ts rename to x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces.ts diff --git a/x-pack/plugins/profiling/common/__fixtures__/stacktraces_3600s_5x.json b/x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces_3600s_5x.json similarity index 100% rename from x-pack/plugins/profiling/common/__fixtures__/stacktraces_3600s_5x.json rename to x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces_3600s_5x.json diff --git a/x-pack/plugins/profiling/common/__fixtures__/stacktraces_604800s_625x.json b/x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces_604800s_625x.json similarity index 100% rename from x-pack/plugins/profiling/common/__fixtures__/stacktraces_604800s_625x.json rename to x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces_604800s_625x.json diff --git a/x-pack/plugins/profiling/common/__fixtures__/stacktraces_60s_1x.json b/x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces_60s_1x.json similarity index 100% rename from x-pack/plugins/profiling/common/__fixtures__/stacktraces_60s_1x.json rename to x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces_60s_1x.json diff --git a/x-pack/plugins/profiling/common/__fixtures__/stacktraces_86400s_125x.json b/x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces_86400s_125x.json similarity index 100% rename from x-pack/plugins/profiling/common/__fixtures__/stacktraces_86400s_125x.json rename to x-pack/plugins/profiling_data_access/common/__fixtures__/stacktraces_86400s_125x.json diff --git a/x-pack/plugins/profiling/common/callee.test.ts b/x-pack/plugins/profiling_data_access/common/callee.test.ts similarity index 100% rename from x-pack/plugins/profiling/common/callee.test.ts rename to x-pack/plugins/profiling_data_access/common/callee.test.ts diff --git a/x-pack/plugins/profiling/common/callee.ts b/x-pack/plugins/profiling_data_access/common/callee.ts similarity index 100% rename from x-pack/plugins/profiling/common/callee.ts rename to x-pack/plugins/profiling_data_access/common/callee.ts diff --git a/x-pack/plugins/profiling/common/elasticsearch.ts b/x-pack/plugins/profiling_data_access/common/elasticsearch.ts similarity index 100% rename from x-pack/plugins/profiling/common/elasticsearch.ts rename to x-pack/plugins/profiling_data_access/common/elasticsearch.ts diff --git a/x-pack/plugins/profiling/common/flamegraph.test.ts b/x-pack/plugins/profiling_data_access/common/flamegraph.test.ts similarity index 100% rename from x-pack/plugins/profiling/common/flamegraph.test.ts rename to x-pack/plugins/profiling_data_access/common/flamegraph.test.ts diff --git a/x-pack/plugins/profiling/common/flamegraph.ts b/x-pack/plugins/profiling_data_access/common/flamegraph.ts similarity index 100% rename from x-pack/plugins/profiling/common/flamegraph.ts rename to x-pack/plugins/profiling_data_access/common/flamegraph.ts diff --git a/x-pack/plugins/profiling/common/frame_group.test.ts b/x-pack/plugins/profiling_data_access/common/frame_group.test.ts similarity index 100% rename from x-pack/plugins/profiling/common/frame_group.test.ts rename to x-pack/plugins/profiling_data_access/common/frame_group.test.ts diff --git a/x-pack/plugins/profiling/common/frame_group.ts b/x-pack/plugins/profiling_data_access/common/frame_group.ts similarity index 100% rename from x-pack/plugins/profiling/common/frame_group.ts rename to x-pack/plugins/profiling_data_access/common/frame_group.ts diff --git a/x-pack/plugins/profiling/common/hash.test.ts b/x-pack/plugins/profiling_data_access/common/hash.test.ts similarity index 100% rename from x-pack/plugins/profiling/common/hash.test.ts rename to x-pack/plugins/profiling_data_access/common/hash.test.ts diff --git a/x-pack/plugins/profiling/common/hash.ts b/x-pack/plugins/profiling_data_access/common/hash.ts similarity index 100% rename from x-pack/plugins/profiling/common/hash.ts rename to x-pack/plugins/profiling_data_access/common/hash.ts diff --git a/x-pack/plugins/profiling/common/profiling.test.ts b/x-pack/plugins/profiling_data_access/common/profiling.test.ts similarity index 90% rename from x-pack/plugins/profiling/common/profiling.test.ts rename to x-pack/plugins/profiling_data_access/common/profiling.test.ts index 5115055ad3c94..24c898bf1cfbe 100644 --- a/x-pack/plugins/profiling/common/profiling.test.ts +++ b/x-pack/plugins/profiling_data_access/common/profiling.test.ts @@ -6,26 +6,15 @@ */ import { - createStackFrameID, createStackFrameMetadata, FrameSymbolStatus, FrameType, - getAddressFromStackFrameID, getCalleeFunction, getCalleeSource, - getFileIDFromStackFrameID, getFrameSymbolStatus, getLanguageType, } from './profiling'; -describe('Stack frame operations', () => { - test('decode stack frame ID', () => { - const frameID = createStackFrameID('ABCDEFGHIJKLMNOPQRSTUw', 123456789); - expect(getAddressFromStackFrameID(frameID)).toEqual(123456789); - expect(getFileIDFromStackFrameID(frameID)).toEqual('ABCDEFGHIJKLMNOPQRSTUw'); - }); -}); - describe('Stack frame metadata operations', () => { test('metadata has executable and function names', () => { const metadata = createStackFrameMetadata({ diff --git a/x-pack/plugins/profiling/common/profiling.ts b/x-pack/plugins/profiling_data_access/common/profiling.ts similarity index 86% rename from x-pack/plugins/profiling/common/profiling.ts rename to x-pack/plugins/profiling_data_access/common/profiling.ts index 22480a26f8ab2..c6f72f20629d3 100644 --- a/x-pack/plugins/profiling/common/profiling.ts +++ b/x-pack/plugins/profiling_data_access/common/profiling.ts @@ -5,50 +5,10 @@ * 2.0. */ -import { charCodeAt, safeBase64Encoder } from './base64'; - export type StackTraceID = string; export type StackFrameID = string; export type FileID = string; -export function createStackFrameID(fileID: FileID, addressOrLine: number): StackFrameID { - const buf = Buffer.alloc(24); - Buffer.from(fileID, 'base64url').copy(buf); - buf.writeBigUInt64BE(BigInt(addressOrLine), 16); - return buf.toString('base64url'); -} - -/* eslint no-bitwise: ["error", { "allow": ["&"] }] */ -export function getFileIDFromStackFrameID(frameID: StackFrameID): FileID { - return frameID.slice(0, 21) + safeBase64Encoder[frameID.charCodeAt(21) & 0x30]; -} - -/* eslint no-bitwise: ["error", { "allow": ["<<=", "&"] }] */ -export function getAddressFromStackFrameID(frameID: StackFrameID): number { - let address = charCodeAt(frameID, 21) & 0xf; - address <<= 6; - address += charCodeAt(frameID, 22); - address <<= 6; - address += charCodeAt(frameID, 23); - address <<= 6; - address += charCodeAt(frameID, 24); - address <<= 6; - address += charCodeAt(frameID, 25); - address <<= 6; - address += charCodeAt(frameID, 26); - address <<= 6; - address += charCodeAt(frameID, 27); - address <<= 6; - address += charCodeAt(frameID, 28); - address <<= 6; - address += charCodeAt(frameID, 29); - address <<= 6; - address += charCodeAt(frameID, 30); - address <<= 6; - address += charCodeAt(frameID, 31); - return address; -} - export enum FrameType { Unsymbolized = 0, Python, diff --git a/x-pack/plugins/profiling/common/stack_traces.test.ts b/x-pack/plugins/profiling_data_access/common/stack_traces.test.ts similarity index 100% rename from x-pack/plugins/profiling/common/stack_traces.test.ts rename to x-pack/plugins/profiling_data_access/common/stack_traces.test.ts diff --git a/x-pack/plugins/profiling/common/stack_traces.ts b/x-pack/plugins/profiling_data_access/common/stack_traces.ts similarity index 100% rename from x-pack/plugins/profiling/common/stack_traces.ts rename to x-pack/plugins/profiling_data_access/common/stack_traces.ts diff --git a/x-pack/plugins/transform/common/types/privileges.ts b/x-pack/plugins/profiling_data_access/jest.config.js similarity index 58% rename from x-pack/plugins/transform/common/types/privileges.ts rename to x-pack/plugins/profiling_data_access/jest.config.js index 702b62210d062..c87c047a5ea73 100644 --- a/x-pack/plugins/transform/common/types/privileges.ts +++ b/x-pack/plugins/profiling_data_access/jest.config.js @@ -5,11 +5,10 @@ * 2.0. */ -export interface MissingPrivileges { - [key: string]: string[] | undefined; -} +const path = require('path'); -export interface Privileges { - hasAllPrivileges: boolean; - missingPrivileges: MissingPrivileges; -} +module.exports = { + preset: '@kbn/test', + rootDir: path.resolve(__dirname, '../../..'), + roots: ['/x-pack/plugins/profiling_data_access'], +}; diff --git a/x-pack/plugins/profiling_data_access/kibana.jsonc b/x-pack/plugins/profiling_data_access/kibana.jsonc new file mode 100644 index 0000000000000..3f0254ba92647 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/kibana.jsonc @@ -0,0 +1,14 @@ +{ + "type": "plugin", + "id": "@kbn/profiling-data-access-plugin", + "owner": "@elastic/profiling-ui", + "plugin": { + "id": "profilingDataAccess", + "server": true, + "browser": false, + "configPath": ["xpack", "profiling"], + "requiredPlugins": ["data"], + "optionalPlugins": [], + "requiredBundles": [] + } +} diff --git a/x-pack/plugins/profiling_data_access/server/index.ts b/x-pack/plugins/profiling_data_access/server/index.ts new file mode 100644 index 0000000000000..d979d22f4a011 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema, TypeOf } from '@kbn/config-schema'; + +import type { PluginInitializerContext } from '@kbn/core/server'; +import { ProfilingDataAccessPlugin } from './plugin'; +import type { ProfilingDataAccessPluginSetup, ProfilingDataAccessPluginStart } from './plugin'; + +const configSchema = schema.object({ + elasticsearch: schema.conditional( + schema.contextRef('dist'), + schema.literal(true), + schema.never(), + schema.maybe( + schema.object({ + hosts: schema.string(), + username: schema.string(), + password: schema.string(), + }) + ) + ), +}); + +export type ProfilingConfig = TypeOf; + +export type { ProfilingDataAccessPluginSetup, ProfilingDataAccessPluginStart }; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ProfilingDataAccessPlugin(initializerContext); +} diff --git a/x-pack/plugins/profiling_data_access/server/plugin.ts b/x-pack/plugins/profiling_data_access/server/plugin.ts new file mode 100644 index 0000000000000..23d72f39caee6 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/plugin.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import { ProfilingConfig } from '.'; +import { registerServices } from './services/register_services'; +import { createProfilingEsClient } from './utils/create_profiling_es_client'; + +export type ProfilingDataAccessPluginSetup = ReturnType; +export type ProfilingDataAccessPluginStart = ReturnType; + +export class ProfilingDataAccessPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + public setup(core: CoreSetup) {} + + public start(core: CoreStart) { + const config = this.initializerContext.config.get(); + + const profilingSpecificEsClient = config.elasticsearch + ? core.elasticsearch.createClient('profiling', { + hosts: [config.elasticsearch.hosts], + username: config.elasticsearch.username, + password: config.elasticsearch.password, + }) + : undefined; + + const services = registerServices({ + createProfilingEsClient: ({ esClient: defaultEsClient, useDefaultAuth = false }) => { + const esClient = + profilingSpecificEsClient && !useDefaultAuth + ? profilingSpecificEsClient.asInternalUser + : defaultEsClient; + + return createProfilingEsClient({ esClient }); + }, + }); + + // called after all plugins are set up + return { + services, + }; + } +} diff --git a/x-pack/plugins/profiling_data_access/server/services/fetch_flamechart/index.ts b/x-pack/plugins/profiling_data_access/server/services/fetch_flamechart/index.ts new file mode 100644 index 0000000000000..dd72a012c6343 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/services/fetch_flamechart/index.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core/server'; +import { RegisterServicesParams } from '../register_services'; +import { withProfilingSpan } from '../../utils/with_profiling_span'; +import { searchStackTraces } from '../search_stack_traces'; +import { createCalleeTree } from '../../../common/callee'; +import { createBaseFlameGraph } from '../../../common/flamegraph'; + +interface FetchFlamechartParams { + esClient: ElasticsearchClient; + rangeFrom: number; + rangeTo: number; + kuery: string; +} + +export function createFetchFlamechart({ createProfilingEsClient }: RegisterServicesParams) { + return async ({ esClient, rangeFrom, rangeTo, kuery }: FetchFlamechartParams) => { + const profilingEsClient = createProfilingEsClient({ esClient }); + const targetSampleSize = 20000; // minimum number of samples to get statistically sound results + + const totalSeconds = rangeTo - rangeFrom; + + const { events, stackTraces, executables, stackFrames, totalFrames, samplingRate } = + await searchStackTraces({ + client: profilingEsClient, + rangeFrom, + rangeTo, + kuery, + sampleSize: targetSampleSize, + }); + + const flamegraph = await withProfilingSpan('create_flamegraph', async () => { + const tree = createCalleeTree( + events, + stackTraces, + stackFrames, + executables, + totalFrames, + samplingRate + ); + + const fg = createBaseFlameGraph(tree, samplingRate, totalSeconds); + + return fg; + }); + + return flamegraph; + }; +} diff --git a/x-pack/plugins/profiling_data_access/server/services/register_services.ts b/x-pack/plugins/profiling_data_access/server/services/register_services.ts new file mode 100644 index 0000000000000..fdac4528f6b5d --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/services/register_services.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 { ElasticsearchClient } from '@kbn/core/server'; +import { createFetchFlamechart } from './fetch_flamechart'; +import { ProfilingESClient } from '../utils/create_profiling_es_client'; + +export interface RegisterServicesParams { + createProfilingEsClient: (params: { + esClient: ElasticsearchClient; + useDefaultAuth?: boolean; + }) => ProfilingESClient; +} + +export function registerServices(params: RegisterServicesParams) { + return { fetchFlamechartData: createFetchFlamechart(params) }; +} diff --git a/x-pack/plugins/profiling_data_access/server/services/search_stack_traces/index.ts b/x-pack/plugins/profiling_data_access/server/services/search_stack_traces/index.ts new file mode 100644 index 0000000000000..1c9e185fe4c05 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/services/search_stack_traces/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { decodeStackTraceResponse } from '../../../common/stack_traces'; +import { ProfilingESClient } from '../../utils/create_profiling_es_client'; + +export async function searchStackTraces({ + client, + sampleSize, + rangeFrom, + rangeTo, + kuery, +}: { + client: ProfilingESClient; + sampleSize: number; + rangeFrom: number; + rangeTo: number; + kuery: string; +}) { + const response = await client.profilingStacktraces({ + query: { + bool: { + filter: [ + ...kqlQuery(kuery), + { + range: { + ['@timestamp']: { + gte: String(rangeFrom), + lt: String(rangeTo), + format: 'epoch_second', + boost: 1.0, + }, + }, + }, + ], + }, + }, + sampleSize, + }); + + return decodeStackTraceResponse(response); +} + +function kqlQuery(kql?: string): estypes.QueryDslQueryContainer[] { + if (!kql) { + return []; + } + + const ast = fromKueryExpression(kql); + return [toElasticsearchQuery(ast)]; +} diff --git a/x-pack/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts b/x-pack/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts new file mode 100644 index 0000000000000..0c5b85f42c8fb --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ElasticsearchClient } from '@kbn/core/server'; +import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { unwrapEsResponse } from '@kbn/observability-plugin/server'; +import { ProfilingStatusResponse, StackTraceResponse } from '../../common/stack_traces'; +import { withProfilingSpan } from './with_profiling_span'; + +export interface ProfilingESClient { + search( + operationName: string, + searchRequest: TSearchRequest + ): Promise>; + profilingStacktraces({}: { + query: QueryDslQueryContainer; + sampleSize: number; + }): Promise; + profilingStatus(): Promise; + getEsClient(): ElasticsearchClient; +} + +export function createProfilingEsClient({ + esClient, +}: { + esClient: ElasticsearchClient; +}): ProfilingESClient { + return { + search( + operationName: string, + searchRequest: TSearchRequest + ): Promise> { + const controller = new AbortController(); + + const promise = withProfilingSpan(operationName, () => { + return esClient.search(searchRequest, { + signal: controller.signal, + meta: true, + }) as unknown as Promise<{ + body: InferSearchResponseOf; + }>; + }); + + return unwrapEsResponse(promise); + }, + profilingStacktraces({ query, sampleSize }) { + const controller = new AbortController(); + const promise = withProfilingSpan('_profiling/stacktraces', () => { + return esClient.transport.request( + { + method: 'POST', + path: encodeURI('/_profiling/stacktraces'), + body: { + query, + sample_size: sampleSize, + }, + }, + { + signal: controller.signal, + meta: true, + } + ); + }); + + return unwrapEsResponse(promise) as Promise; + }, + profilingStatus() { + const controller = new AbortController(); + + const promise = withProfilingSpan('_profiling/status', () => { + return esClient.transport.request( + { + method: 'GET', + path: encodeURI('/_profiling/status'), + }, + { + signal: controller.signal, + meta: true, + } + ); + }); + + return unwrapEsResponse(promise) as Promise; + }, + getEsClient() { + return esClient; + }, + }; +} diff --git a/x-pack/plugins/profiling_data_access/server/utils/with_profiling_span.ts b/x-pack/plugins/profiling_data_access/server/utils/with_profiling_span.ts new file mode 100644 index 0000000000000..6d366799780e7 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/utils/with_profiling_span.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { withSpan, SpanOptions, parseSpanOptions } from '@kbn/apm-utils'; + +export function withProfilingSpan( + optionsOrName: SpanOptions | string, + cb: () => Promise +): Promise { + const options = parseSpanOptions(optionsOrName); + + const optionsWithDefaults = { + ...(options.intercept ? {} : { type: 'plugin:profiling' }), + ...options, + labels: { + plugin: 'profiling', + ...options.labels, + }, + }; + + return withSpan(optionsWithDefaults, cb); +} diff --git a/x-pack/plugins/profiling_data_access/tsconfig.json b/x-pack/plugins/profiling_data_access/tsconfig.json new file mode 100644 index 0000000000000..f8693cb83658e --- /dev/null +++ b/x-pack/plugins/profiling_data_access/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "server/**/*", + "common/**/*.ts", + "common/**/*.json", + "jest.config.js" + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/config-schema", + "@kbn/core", + "@kbn/es-query", + "@kbn/es-types", + "@kbn/observability-plugin", + "@kbn/apm-utils" + ] +} diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index c561052669fdd..0b17e237057e6 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -100,6 +100,8 @@ export class RuleRegistryPlugin this.security = plugins.security; + const dataStreamAdapter = plugins.alerting.getDataStreamAdapter(); + this.ruleDataService = new RuleDataService({ logger, kibanaVersion, @@ -112,6 +114,7 @@ export class RuleRegistryPlugin }, frameworkAlerts: plugins.alerting.frameworkAlerts, pluginStop$: this.pluginStop$, + dataStreamAdapter, }); this.ruleDataService.initializeService(); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts index 751c21a08cf8d..dc6470c4739ce 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts @@ -44,5 +44,6 @@ export const createRuleDataClientMock = ( bulk, }) ), + isUsingDataStreams: jest.fn(() => false), }; }; diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts index 9c280cbcd51a8..968a7fbadac0b 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts @@ -29,12 +29,14 @@ interface GetRuleDataClientOptionsOpts { isWriterCacheEnabled?: boolean; waitUntilReadyForReading?: Promise; waitUntilReadyForWriting?: Promise; + isUsingDataStreams: boolean; } function getRuleDataClientOptions({ isWriteEnabled, isWriterCacheEnabled, waitUntilReadyForReading, waitUntilReadyForWriting, + isUsingDataStreams, }: GetRuleDataClientOptionsOpts): RuleDataClientConstructorOptions { return { indexInfo: new IndexInfo({ @@ -55,6 +57,7 @@ function getRuleDataClientOptions({ waitUntilReadyForWriting: waitUntilReadyForWriting ?? Promise.resolve(right(scopedClusterClient) as WaitResult), logger, + isUsingDataStreams, }; } @@ -65,331 +68,362 @@ describe('RuleDataClient', () => { jest.resetAllMocks(); }); - test('options are set correctly in constructor', () => { - const namespace = 'test'; - const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); - expect(ruleDataClient.indexName).toEqual(`.alerts-observability.apm.alerts`); - expect(ruleDataClient.kibanaVersion).toEqual('8.2.0'); - expect(ruleDataClient.indexNameWithNamespace(namespace)).toEqual( - `.alerts-observability.apm.alerts-${namespace}` - ); - expect(ruleDataClient.isWriteEnabled()).toEqual(true); - }); - - describe('getReader()', () => { - beforeEach(() => { - jest.resetAllMocks(); - getFieldsForWildcardMock.mockResolvedValue({ fields: ['foo'] }); - IndexPatternsFetcher.prototype.getFieldsForWildcard = getFieldsForWildcardMock; - }); - - afterAll(() => { - getFieldsForWildcardMock.mockRestore(); - }); - - test('waits until cluster client is ready before searching', async () => { - const ruleDataClient = new RuleDataClient( - getRuleDataClientOptions({ - waitUntilReadyForReading: new Promise((resolve) => - setTimeout(resolve, 3000, right(scopedClusterClient)) - ), - }) - ); - - const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; - const reader = ruleDataClient.getReader(); - await reader.search({ - body: query, + for (const isUsingDataStreams of [false, true]) { + const label = isUsingDataStreams ? 'data streams' : 'aliases'; + + describe(`using ${label} for alert indices`, () => { + test('options are set correctly in constructor', () => { + const namespace = 'test'; + const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({ isUsingDataStreams })); + expect(ruleDataClient.indexName).toEqual(`.alerts-observability.apm.alerts`); + expect(ruleDataClient.kibanaVersion).toEqual('8.2.0'); + expect(ruleDataClient.indexNameWithNamespace(namespace)).toEqual( + `.alerts-observability.apm.alerts-${namespace}` + ); + expect(ruleDataClient.isWriteEnabled()).toEqual(true); }); - expect(scopedClusterClient.search).toHaveBeenCalledWith({ - body: query, - ignore_unavailable: true, - index: `.alerts-observability.apm.alerts*`, - }); - }); - - test('getReader searchs an index pattern without a wildcard when the namespace is provided', async () => { - const ruleDataClient = new RuleDataClient( - getRuleDataClientOptions({ - waitUntilReadyForReading: new Promise((resolve) => - setTimeout(resolve, 3000, right(scopedClusterClient)) - ), - }) - ); - - const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; - const reader = ruleDataClient.getReader({ namespace: 'test' }); - await reader.search({ - body: query, + describe('getReader()', () => { + beforeEach(() => { + jest.resetAllMocks(); + getFieldsForWildcardMock.mockResolvedValue({ fields: ['foo'] }); + IndexPatternsFetcher.prototype.getFieldsForWildcard = getFieldsForWildcardMock; + }); + + afterAll(() => { + getFieldsForWildcardMock.mockRestore(); + }); + + test('waits until cluster client is ready before searching', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + isUsingDataStreams, + waitUntilReadyForReading: new Promise((resolve) => + setTimeout(resolve, 3000, right(scopedClusterClient)) + ), + }) + ); + + const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; + const reader = ruleDataClient.getReader(); + await reader.search({ + body: query, + }); + + expect(scopedClusterClient.search).toHaveBeenCalledWith({ + body: query, + ignore_unavailable: true, + index: `.alerts-observability.apm.alerts*`, + seq_no_primary_term: true, + }); + }); + + test('getReader searchs an index pattern without a wildcard when the namespace is provided', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + isUsingDataStreams, + waitUntilReadyForReading: new Promise((resolve) => + setTimeout(resolve, 3000, right(scopedClusterClient)) + ), + }) + ); + + const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; + const reader = ruleDataClient.getReader({ namespace: 'test' }); + await reader.search({ + body: query, + }); + + expect(scopedClusterClient.search).toHaveBeenCalledWith({ + body: query, + ignore_unavailable: true, + index: `.alerts-observability.apm.alerts-test`, + seq_no_primary_term: true, + }); + }); + + test('re-throws error when search throws error', async () => { + scopedClusterClient.search.mockRejectedValueOnce(new Error('something went wrong!')); + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ isUsingDataStreams }) + ); + const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; + const reader = ruleDataClient.getReader(); + + await expect( + reader.search({ + body: query, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong!"`); + + expect(logger.error).toHaveBeenCalledWith( + `Error performing search in RuleDataClient - something went wrong!` + ); + }); + + test('waits until cluster client is ready before getDynamicIndexPattern', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + isUsingDataStreams, + waitUntilReadyForReading: new Promise((resolve) => + setTimeout(resolve, 3000, right(scopedClusterClient)) + ), + }) + ); + + const reader = ruleDataClient.getReader(); + expect(await reader.getDynamicIndexPattern()).toEqual({ + fields: ['foo'], + timeFieldName: '@timestamp', + title: '.alerts-observability.apm.alerts*', + }); + }); + + test('re-throws generic errors from getFieldsForWildcard', async () => { + getFieldsForWildcardMock.mockRejectedValueOnce(new Error('something went wrong!')); + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ isUsingDataStreams }) + ); + const reader = ruleDataClient.getReader(); + + await expect(reader.getDynamicIndexPattern()).rejects.toThrowErrorMatchingInlineSnapshot( + `"something went wrong!"` + ); + + expect(logger.error).toHaveBeenCalledWith( + `Error fetching index patterns in RuleDataClient - something went wrong!` + ); + }); + + test('correct handles no_matching_indices errors from getFieldsForWildcard', async () => { + getFieldsForWildcardMock.mockRejectedValueOnce(createNoMatchingIndicesError([])); + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ isUsingDataStreams }) + ); + const reader = ruleDataClient.getReader(); + + expect(await reader.getDynamicIndexPattern()).toEqual({ + fields: [], + timeFieldName: '@timestamp', + title: '.alerts-observability.apm.alerts*', + }); + + expect(logger.error).not.toHaveBeenCalled(); + }); + + test('handles errors getting cluster client', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + isUsingDataStreams, + waitUntilReadyForReading: Promise.resolve( + left(new Error('could not get cluster client')) + ), + }) + ); + + const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; + const reader = ruleDataClient.getReader(); + await expect( + reader.search({ + body: query, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"could not get cluster client"`); + + await expect(reader.getDynamicIndexPattern()).rejects.toThrowErrorMatchingInlineSnapshot( + `"could not get cluster client"` + ); + }); }); - expect(scopedClusterClient.search).toHaveBeenCalledWith({ - body: query, - ignore_unavailable: true, - index: `.alerts-observability.apm.alerts-test`, + describe('getWriter()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('throws error if writing is disabled', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ isWriteEnabled: false, isUsingDataStreams }) + ); + + await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( + `"Rule registry writing is disabled. Make sure that \\"xpack.ruleRegistry.write.enabled\\" configuration is not set to false and \\"observability.apm\\" is not disabled in \\"xpack.ruleRegistry.write.disabledRegistrationContexts\\" within \\"kibana.yml\\"."` + ); + expect(logger.debug).toHaveBeenCalledWith( + `Writing is disabled, bulk() will not write any data.` + ); + }); + + test('throws error if initialization of writer fails due to index error', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + isUsingDataStreams, + waitUntilReadyForWriting: Promise.resolve( + left(new Error('could not get cluster client')) + ), + }) + ); + expect(ruleDataClient.isWriteEnabled()).toBe(true); + await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( + `"There has been a catastrophic error trying to install index level resources for the following registration context: observability.apm. This may have been due to a non-additive change to the mappings, removal and type changes are not permitted. Full error: Error: could not get cluster client"` + ); + expect(logger.error).toHaveBeenNthCalledWith( + 1, + new RuleDataWriterInitializationError( + 'index', + 'observability.apm', + new Error('could not get cluster client') + ) + ); + expect(logger.error).toHaveBeenNthCalledWith( + 2, + `The writer for the Rule Data Client for the observability.apm registration context was not initialized properly, bulk() cannot continue.` + ); + expect(ruleDataClient.isWriteEnabled()).not.toBe(false); + + // getting the writer again at this point should throw another error + await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( + `"There has been a catastrophic error trying to install index level resources for the following registration context: observability.apm. This may have been due to a non-additive change to the mappings, removal and type changes are not permitted. Full error: Error: could not get cluster client"` + ); + }); + + test('throws error if initialization of writer fails due to namespace error', async () => { + mockResourceInstaller.installAndUpdateNamespaceLevelResources.mockRejectedValue( + new Error('bad resource installation') + ); + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ isUsingDataStreams }) + ); + expect(ruleDataClient.isWriteEnabled()).toBe(true); + await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( + `"There has been a catastrophic error trying to install namespace level resources for the following registration context: observability.apm. This may have been due to a non-additive change to the mappings, removal and type changes are not permitted. Full error: Error: bad resource installation"` + ); + expect(logger.error).toHaveBeenNthCalledWith( + 1, + new RuleDataWriterInitializationError( + 'namespace', + 'observability.apm', + new Error('bad resource installation') + ) + ); + expect(logger.error).toHaveBeenNthCalledWith( + 2, + `The writer for the Rule Data Client for the observability.apm registration context was not initialized properly, bulk() cannot continue.` + ); + expect(ruleDataClient.isWriteEnabled()).not.toBe(false); + + // getting the writer again at this point should throw another error + await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( + `"There has been a catastrophic error trying to install namespace level resources for the following registration context: observability.apm. This may have been due to a non-additive change to the mappings, removal and type changes are not permitted. Full error: Error: bad resource installation"` + ); + }); + + test('uses cached cluster client when repeatedly initializing writer', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ isUsingDataStreams }) + ); + + await ruleDataClient.getWriter(); + await ruleDataClient.getWriter(); + await ruleDataClient.getWriter(); + await ruleDataClient.getWriter(); + await ruleDataClient.getWriter(); + + expect( + mockResourceInstaller.installAndUpdateNamespaceLevelResources + ).toHaveBeenCalledTimes(1); + }); }); - }); - - test('re-throws error when search throws error', async () => { - scopedClusterClient.search.mockRejectedValueOnce(new Error('something went wrong!')); - const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); - const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; - const reader = ruleDataClient.getReader(); - - await expect( - reader.search({ - body: query, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong!"`); - - expect(logger.error).toHaveBeenCalledWith( - `Error performing search in RuleDataClient - something went wrong!` - ); - }); - - test('waits until cluster client is ready before getDynamicIndexPattern', async () => { - const ruleDataClient = new RuleDataClient( - getRuleDataClientOptions({ - waitUntilReadyForReading: new Promise((resolve) => - setTimeout(resolve, 3000, right(scopedClusterClient)) - ), - }) - ); - - const reader = ruleDataClient.getReader(); - expect(await reader.getDynamicIndexPattern()).toEqual({ - fields: ['foo'], - timeFieldName: '@timestamp', - title: '.alerts-observability.apm.alerts*', - }); - }); - - test('re-throws generic errors from getFieldsForWildcard', async () => { - getFieldsForWildcardMock.mockRejectedValueOnce(new Error('something went wrong!')); - const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); - const reader = ruleDataClient.getReader(); - - await expect(reader.getDynamicIndexPattern()).rejects.toThrowErrorMatchingInlineSnapshot( - `"something went wrong!"` - ); - - expect(logger.error).toHaveBeenCalledWith( - `Error fetching index patterns in RuleDataClient - something went wrong!` - ); - }); - - test('correct handles no_matching_indices errors from getFieldsForWildcard', async () => { - getFieldsForWildcardMock.mockRejectedValueOnce(createNoMatchingIndicesError([])); - const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); - const reader = ruleDataClient.getReader(); - - expect(await reader.getDynamicIndexPattern()).toEqual({ - fields: [], - timeFieldName: '@timestamp', - title: '.alerts-observability.apm.alerts*', - }); - - expect(logger.error).not.toHaveBeenCalled(); - }); - - test('handles errors getting cluster client', async () => { - const ruleDataClient = new RuleDataClient( - getRuleDataClientOptions({ - waitUntilReadyForReading: Promise.resolve( - left(new Error('could not get cluster client')) - ), - }) - ); - - const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; - const reader = ruleDataClient.getReader(); - await expect( - reader.search({ - body: query, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"could not get cluster client"`); - - await expect(reader.getDynamicIndexPattern()).rejects.toThrowErrorMatchingInlineSnapshot( - `"could not get cluster client"` - ); - }); - }); - - describe('getWriter()', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('throws error if writing is disabled', async () => { - const ruleDataClient = new RuleDataClient( - getRuleDataClientOptions({ isWriteEnabled: false }) - ); - - await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( - `"Rule registry writing is disabled. Make sure that \\"xpack.ruleRegistry.write.enabled\\" configuration is not set to false and \\"observability.apm\\" is not disabled in \\"xpack.ruleRegistry.write.disabledRegistrationContexts\\" within \\"kibana.yml\\"."` - ); - expect(logger.debug).toHaveBeenCalledWith( - `Writing is disabled, bulk() will not write any data.` - ); - }); - - test('throws error if initialization of writer fails due to index error', async () => { - const ruleDataClient = new RuleDataClient( - getRuleDataClientOptions({ - waitUntilReadyForWriting: Promise.resolve( - left(new Error('could not get cluster client')) - ), - }) - ); - expect(ruleDataClient.isWriteEnabled()).toBe(true); - await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( - `"There has been a catastrophic error trying to install index level resources for the following registration context: observability.apm. This may have been due to a non-additive change to the mappings, removal and type changes are not permitted. Full error: Error: could not get cluster client"` - ); - expect(logger.error).toHaveBeenNthCalledWith( - 1, - new RuleDataWriterInitializationError( - 'index', - 'observability.apm', - new Error('could not get cluster client') - ) - ); - expect(logger.error).toHaveBeenNthCalledWith( - 2, - `The writer for the Rule Data Client for the observability.apm registration context was not initialized properly, bulk() cannot continue.` - ); - expect(ruleDataClient.isWriteEnabled()).not.toBe(false); - - // getting the writer again at this point should throw another error - await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( - `"There has been a catastrophic error trying to install index level resources for the following registration context: observability.apm. This may have been due to a non-additive change to the mappings, removal and type changes are not permitted. Full error: Error: could not get cluster client"` - ); - }); - - test('throws error if initialization of writer fails due to namespace error', async () => { - mockResourceInstaller.installAndUpdateNamespaceLevelResources.mockRejectedValue( - new Error('bad resource installation') - ); - const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); - expect(ruleDataClient.isWriteEnabled()).toBe(true); - await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( - `"There has been a catastrophic error trying to install namespace level resources for the following registration context: observability.apm. This may have been due to a non-additive change to the mappings, removal and type changes are not permitted. Full error: Error: bad resource installation"` - ); - expect(logger.error).toHaveBeenNthCalledWith( - 1, - new RuleDataWriterInitializationError( - 'namespace', - 'observability.apm', - new Error('bad resource installation') - ) - ); - expect(logger.error).toHaveBeenNthCalledWith( - 2, - `The writer for the Rule Data Client for the observability.apm registration context was not initialized properly, bulk() cannot continue.` - ); - expect(ruleDataClient.isWriteEnabled()).not.toBe(false); - - // getting the writer again at this point should throw another error - await expect(() => ruleDataClient.getWriter()).rejects.toThrowErrorMatchingInlineSnapshot( - `"There has been a catastrophic error trying to install namespace level resources for the following registration context: observability.apm. This may have been due to a non-additive change to the mappings, removal and type changes are not permitted. Full error: Error: bad resource installation"` - ); - }); - test('uses cached cluster client when repeatedly initializing writer', async () => { - const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); - - await ruleDataClient.getWriter(); - await ruleDataClient.getWriter(); - await ruleDataClient.getWriter(); - await ruleDataClient.getWriter(); - await ruleDataClient.getWriter(); - - expect(mockResourceInstaller.installAndUpdateNamespaceLevelResources).toHaveBeenCalledTimes( - 1 - ); - }); - }); - - describe('bulk()', () => { - test('logs debug and returns undefined if clusterClient is not defined', async () => { - const ruleDataClient = new RuleDataClient( - getRuleDataClientOptions({ - waitUntilReadyForWriting: new Promise((resolve) => - resolve(right(undefined as unknown as ElasticsearchClient)) - ), - }) - ); - const writer = await ruleDataClient.getWriter(); - - // Previously, a delay between calling getWriter() and using a writer function - // would cause an Unhandled promise rejection if there were any errors getting a writer - // Adding this delay in the tests to ensure this does not pop up again. - await delay(); - - expect(await writer.bulk({})).toEqual(undefined); - expect(logger.debug).toHaveBeenCalledWith( - `Writing is disabled, bulk() will not write any data.` - ); - }); - - test('throws and logs error if bulk function throws error', async () => { - const error = new Error('something went wrong!'); - scopedClusterClient.bulk.mockRejectedValueOnce(error); - const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); - expect(ruleDataClient.isWriteEnabled()).toBe(true); - const writer = await ruleDataClient.getWriter(); - - // Previously, a delay between calling getWriter() and using a writer function - // would cause an Unhandled promise rejection if there were any errors getting a writer - // Adding this delay in the tests to ensure this does not pop up again. - await delay(); - - await expect(() => writer.bulk({})).rejects.toThrowErrorMatchingInlineSnapshot( - `"something went wrong!"` - ); - expect(logger.error).toHaveBeenNthCalledWith(1, error); - expect(ruleDataClient.isWriteEnabled()).toBe(true); - }); - - test('waits until cluster client is ready before calling bulk', async () => { - scopedClusterClient.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - {} - ) as unknown as estypes.BulkResponse - ); - const ruleDataClient = new RuleDataClient( - getRuleDataClientOptions({ - waitUntilReadyForWriting: new Promise((resolve) => - setTimeout(resolve, 3000, right(scopedClusterClient)) - ), - }) - ); - - const writer = await ruleDataClient.getWriter(); - // Previously, a delay between calling getWriter() and using a writer function - // would cause an Unhandled promise rejection if there were any errors getting a writer - // Adding this delay in the tests to ensure this does not pop up again. - await delay(); - - const response = await writer.bulk({}); - - expect(response).toEqual({ - body: {}, - headers: { - 'x-elastic-product': 'Elasticsearch', - }, - meta: {}, - statusCode: 200, - warnings: [], + describe('bulk()', () => { + test('logs debug and returns undefined if clusterClient is not defined', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + isUsingDataStreams, + waitUntilReadyForWriting: new Promise((resolve) => + resolve(right(undefined as unknown as ElasticsearchClient)) + ), + }) + ); + const writer = await ruleDataClient.getWriter(); + + // Previously, a delay between calling getWriter() and using a writer function + // would cause an Unhandled promise rejection if there were any errors getting a writer + // Adding this delay in the tests to ensure this does not pop up again. + await delay(); + + expect(await writer.bulk({})).toEqual(undefined); + expect(logger.debug).toHaveBeenCalledWith( + `Writing is disabled, bulk() will not write any data.` + ); + }); + + test('throws and logs error if bulk function throws error', async () => { + const error = new Error('something went wrong!'); + scopedClusterClient.bulk.mockRejectedValueOnce(error); + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ isUsingDataStreams }) + ); + expect(ruleDataClient.isWriteEnabled()).toBe(true); + const writer = await ruleDataClient.getWriter(); + + // Previously, a delay between calling getWriter() and using a writer function + // would cause an Unhandled promise rejection if there were any errors getting a writer + // Adding this delay in the tests to ensure this does not pop up again. + await delay(); + + await expect(() => writer.bulk({})).rejects.toThrowErrorMatchingInlineSnapshot( + `"something went wrong!"` + ); + expect(logger.error).toHaveBeenNthCalledWith( + 1, + 'error writing to index: something went wrong!', + error + ); + expect(ruleDataClient.isWriteEnabled()).toBe(true); + }); + + test('waits until cluster client is ready before calling bulk', async () => { + scopedClusterClient.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + {} + ) as unknown as estypes.BulkResponse + ); + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + isUsingDataStreams, + waitUntilReadyForWriting: new Promise((resolve) => + setTimeout(resolve, 3000, right(scopedClusterClient)) + ), + }) + ); + + const writer = await ruleDataClient.getWriter(); + // Previously, a delay between calling getWriter() and using a writer function + // would cause an Unhandled promise rejection if there were any errors getting a writer + // Adding this delay in the tests to ensure this does not pop up again. + await delay(); + + const response = await writer.bulk({}); + + expect(response).toEqual({ + body: {}, + headers: { + 'x-elastic-product': 'Elasticsearch', + }, + meta: {}, + statusCode: 200, + warnings: [], + }); + + expect(scopedClusterClient.bulk).toHaveBeenCalledWith( + { + index: `.alerts-observability.apm.alerts-default`, + require_alias: isUsingDataStreams ? false : true, + }, + { meta: true } + ); + }); }); - - expect(scopedClusterClient.bulk).toHaveBeenCalledWith( - { - index: `.alerts-observability.apm.alerts-default`, - require_alias: true, - }, - { meta: true } - ); }); - }); + } }); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts index b56bc41efd292..b4d029f4bbe82 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts @@ -32,6 +32,7 @@ export interface RuleDataClientConstructorOptions { waitUntilReadyForReading: Promise; waitUntilReadyForWriting: Promise; logger: Logger; + isUsingDataStreams: boolean; } export type WaitResult = Either; @@ -39,6 +40,7 @@ export type WaitResult = Either; export class RuleDataClient implements IRuleDataClient { private _isWriteEnabled: boolean = false; private _isWriterCacheEnabled: boolean = true; + private readonly _isUsingDataStreams: boolean; // Writers cached by namespace private writerCache: Map; @@ -48,6 +50,7 @@ export class RuleDataClient implements IRuleDataClient { constructor(private readonly options: RuleDataClientConstructorOptions) { this.writeEnabled = this.options.isWriteEnabled; this.writerCacheEnabled = this.options.isWriterCacheEnabled; + this._isUsingDataStreams = this.options.isUsingDataStreams; this.writerCache = new Map(); } @@ -83,6 +86,10 @@ export class RuleDataClient implements IRuleDataClient { this._isWriterCacheEnabled = isEnabled; } + public isUsingDataStreams(): boolean { + return this._isUsingDataStreams; + } + public getReader(options: { namespace?: string } = {}): IRuleDataReader { const { indexInfo } = this.options; const indexPattern = indexInfo.getPatternForReading(options.namespace); @@ -109,6 +116,7 @@ export class RuleDataClient implements IRuleDataClient { ...request, index: indexPattern, ignore_unavailable: true, + seq_no_primary_term: true, })) as unknown as ESSearchResponse; } catch (err) { this.options.logger.error(`Error performing search in RuleDataClient - ${err.message}`); @@ -215,7 +223,7 @@ export class RuleDataClient implements IRuleDataClient { if (this.clusterClient) { const requestWithDefaultParameters = { ...request, - require_alias: true, + require_alias: !this._isUsingDataStreams, index: alias, }; @@ -223,6 +231,8 @@ export class RuleDataClient implements IRuleDataClient { meta: true, }); + // TODO: #160572 - add support for version conflict errors, in case alert was updated + // some other way between the time it was fetched and the time it was updated. if (response.body.errors) { const error = new errors.ResponseError(response); this.options.logger.error(error); @@ -232,7 +242,7 @@ export class RuleDataClient implements IRuleDataClient { this.options.logger.debug(`Writing is disabled, bulk() will not write any data.`); } } catch (error) { - this.options.logger.error(error); + this.options.logger.error(`error writing to index: ${error.message}`, error); throw error; } }, diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index dc8199c1e2963..a7da8069739f4 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -18,6 +18,7 @@ export interface IRuleDataClient { indexNameWithNamespace(namespace: string): string; kibanaVersion: string; isWriteEnabled(): boolean; + isUsingDataStreams(): boolean; getReader(options?: { namespace?: string }): IRuleDataReader; getWriter(options?: { namespace?: string }): Promise; } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts index b27b90713c99e..d2961eeee7580 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts @@ -9,10 +9,14 @@ import { type Subject, ReplaySubject } from 'rxjs'; import { ResourceInstaller } from './resource_installer'; import { loggerMock } from '@kbn/logging-mocks'; import { AlertConsumers } from '@kbn/rule-data-utils'; +import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Dataset } from './index_options'; import { IndexInfo } from './index_info'; import { ECS_COMPONENT_TEMPLATE_NAME } from '@kbn/alerting-plugin/server'; +import type { DataStreamAdapter } from '@kbn/alerting-plugin/server'; +import { getDataStreamAdapter } from '@kbn/alerting-plugin/server/alerts_service/lib/data_stream_adapter'; + import { elasticsearchServiceMock, ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../common/assets'; @@ -21,514 +25,578 @@ const frameworkAlertsService = { getContextInitializationPromise: async () => ({ result: false, error: `failed` }), }; +const GetAliasResponse = { + real_index: { + aliases: { + alias_1: { + is_hidden: true, + }, + alias_2: { + is_hidden: true, + }, + }, + }, +}; + +const GetDataStreamResponse: IndicesGetDataStreamResponse = { + data_streams: [ + { + name: 'ignored', + generation: 1, + timestamp_field: { name: 'ignored' }, + hidden: true, + indices: [{ index_name: 'ignored', index_uuid: 'ignored' }], + status: 'green', + template: 'ignored', + }, + ], +}; + describe('resourceInstaller', () => { let pluginStop$: Subject; + let dataStreamAdapter: DataStreamAdapter; - beforeEach(() => { - pluginStop$ = new ReplaySubject(1); - }); - - afterEach(() => { - pluginStop$.next(); - pluginStop$.complete(); - }); - - describe('if write is disabled', () => { - it('should not install common resources', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: false, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: frameworkAlertsService, - pluginStop$, - }); - installer.installCommonResources(); - expect(getClusterClient).not.toHaveBeenCalled(); - }); + for (const useDataStreamForAlerts of [false, true]) { + const label = useDataStreamForAlerts ? 'data streams' : 'aliases'; - it('should not install index level resources', () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: false, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: frameworkAlertsService, - pluginStop$, + describe(`using ${label} for alert indices`, () => { + beforeEach(() => { + pluginStop$ = new ReplaySubject(1); + dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts }); }); - const indexOptions = { - feature: AlertConsumers.LOGS, - registrationContext: 'observability.logs', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - }, - ], - }; - const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); - installer.installIndexLevelResources(indexInfo); - expect(mockClusterClient.cluster.putComponentTemplate).not.toHaveBeenCalled(); - }); - }); - - describe('if write is enabled', () => { - it('should install common resources', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: frameworkAlertsService, - pluginStop$, + afterEach(() => { + pluginStop$.next(); + pluginStop$.complete(); }); - await installer.installCommonResources(); - - expect(mockClusterClient.ilm.putLifecycle).toHaveBeenCalled(); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(2); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ name: ECS_COMPONENT_TEMPLATE_NAME }) - ); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ name: TECHNICAL_COMPONENT_TEMPLATE_NAME }) - ); - }); - - it('should install subset of common resources when framework alerts are enabled', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: { - ...frameworkAlertsService, - enabled: () => true, - }, - pluginStop$, + describe('if write is disabled', () => { + it('should not install common resources', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: false, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: frameworkAlertsService, + pluginStop$, + dataStreamAdapter, + }); + installer.installCommonResources(); + expect(getClusterClient).not.toHaveBeenCalled(); + }); + + it('should not install index level resources', () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: false, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: frameworkAlertsService, + pluginStop$, + dataStreamAdapter, + }); + const indexOptions = { + feature: AlertConsumers.LOGS, + registrationContext: 'observability.logs', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); + + installer.installIndexLevelResources(indexInfo); + expect(mockClusterClient.cluster.putComponentTemplate).not.toHaveBeenCalled(); + }); }); - await installer.installCommonResources(); - - // ILM policy should be handled by framework - expect(mockClusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); - // ECS component template should be handled by framework - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(1); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ name: TECHNICAL_COMPONENT_TEMPLATE_NAME }) - ); - }); - - it('should install index level resources', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: frameworkAlertsService, - pluginStop$, - }); - - const indexOptions = { - feature: AlertConsumers.LOGS, - registrationContext: 'observability.logs', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - }, - ], - }; - const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); - - await installer.installIndexLevelResources(indexInfo); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledWith( - expect.objectContaining({ name: '.alerts-observability.logs.alerts-mappings' }) - ); - }); - - it('should not install index level component template when framework alerts are enabled', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: { - ...frameworkAlertsService, - enabled: () => true, - }, - pluginStop$, - }); - - const indexOptions = { - feature: AlertConsumers.LOGS, - registrationContext: 'observability.logs', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - }, - ], - }; - const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); - - await installer.installIndexLevelResources(indexInfo); - expect(mockClusterClient.cluster.putComponentTemplate).not.toHaveBeenCalled(); - }); - - it('should install namespace level resources for the default space', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - mockClusterClient.indices.simulateTemplate.mockImplementation(async () => ({ - template: { - aliases: { - alias_name_1: { - is_hidden: true, + describe('if write is enabled', () => { + it('should install common resources', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: frameworkAlertsService, + pluginStop$, + dataStreamAdapter, + }); + + await installer.installCommonResources(); + + expect(mockClusterClient.ilm.putLifecycle).toHaveBeenCalledTimes( + useDataStreamForAlerts ? 0 : 1 + ); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(2); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ name: ECS_COMPONENT_TEMPLATE_NAME }) + ); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ name: TECHNICAL_COMPONENT_TEMPLATE_NAME }) + ); + }); + + it('should install subset of common resources when framework alerts are enabled', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: { + ...frameworkAlertsService, + enabled: () => true, }, - alias_name_2: { - is_hidden: true, + pluginStop$, + dataStreamAdapter, + }); + + await installer.installCommonResources(); + + // ILM policy should be handled by framework + expect(mockClusterClient.ilm.putLifecycle).not.toHaveBeenCalled(); + // ECS component template should be handled by framework + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(1); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ name: TECHNICAL_COMPONENT_TEMPLATE_NAME }) + ); + }); + + it('should install index level resources', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: frameworkAlertsService, + pluginStop$, + dataStreamAdapter, + }); + + const indexOptions = { + feature: AlertConsumers.LOGS, + registrationContext: 'observability.logs', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); + + await installer.installIndexLevelResources(indexInfo); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledWith( + expect.objectContaining({ name: '.alerts-observability.logs.alerts-mappings' }) + ); + }); + + it('should not install index level component template when framework alerts are enabled', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: { + ...frameworkAlertsService, + enabled: () => true, }, - }, - mappings: { enabled: false }, - settings: {}, - }, - })); - mockClusterClient.indices.getAlias.mockImplementation(async () => ({ - real_index: { - aliases: { - alias_1: { - is_hidden: true, + pluginStop$, + dataStreamAdapter, + }); + + const indexOptions = { + feature: AlertConsumers.LOGS, + registrationContext: 'observability.logs', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); + + await installer.installIndexLevelResources(indexInfo); + expect(mockClusterClient.cluster.putComponentTemplate).not.toHaveBeenCalled(); + }); + + it('should install namespace level resources for the default space', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + mockClusterClient.indices.simulateTemplate.mockImplementation(async () => ({ + template: { + aliases: { + alias_name_1: { + is_hidden: true, + }, + alias_name_2: { + is_hidden: true, + }, + }, + mappings: { enabled: false }, + settings: {}, }, - alias_2: { - is_hidden: true, + })); + mockClusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + mockClusterClient.indices.getDataStream.mockImplementation(async () => ({ + data_streams: [], + })); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: frameworkAlertsService, + pluginStop$, + dataStreamAdapter, + }); + + const indexOptions = { + feature: AlertConsumers.LOGS, + registrationContext: 'observability.logs', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); + + await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); + expect(mockClusterClient.indices.simulateTemplate).toHaveBeenCalledWith( + expect.objectContaining({ + name: '.alerts-observability.logs.alerts-default-index-template', + }) + ); + expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + expect.objectContaining({ + name: '.alerts-observability.logs.alerts-default-index-template', + }) + ); + if (useDataStreamForAlerts) { + expect(mockClusterClient.indices.getDataStream).toHaveBeenCalledWith( + expect.objectContaining({ + name: '.alerts-observability.logs.alerts-default', + expand_wildcards: 'all', + }) + ); + expect(mockClusterClient.indices.createDataStream).toHaveBeenCalledWith( + expect.objectContaining({ + name: '.alerts-observability.logs.alerts-default', + }) + ); + } else { + expect(mockClusterClient.indices.getAlias).toHaveBeenCalledWith( + expect.objectContaining({ name: '.alerts-observability.logs.alerts-*' }) + ); + expect(mockClusterClient.indices.create).toHaveBeenCalledWith( + expect.objectContaining({ + index: '.internal.alerts-observability.logs.alerts-default-000001', + }) + ); + } + }); + + it('should not install namespace level resources for the default space when framework alerts are available', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: { + ...frameworkAlertsService, + enabled: () => true, + getContextInitializationPromise: async () => ({ result: true }), }, - }, - }, - })); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: frameworkAlertsService, - pluginStop$, - }); - - const indexOptions = { - feature: AlertConsumers.LOGS, - registrationContext: 'observability.logs', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - }, - ], - }; - const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); - - await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); - expect(mockClusterClient.indices.simulateTemplate).toHaveBeenCalledWith( - expect.objectContaining({ - name: '.alerts-observability.logs.alerts-default-index-template', - }) - ); - expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( - expect.objectContaining({ - name: '.alerts-observability.logs.alerts-default-index-template', - }) - ); - expect(mockClusterClient.indices.getAlias).toHaveBeenCalledWith( - expect.objectContaining({ name: '.alerts-observability.logs.alerts-*' }) - ); - expect(mockClusterClient.indices.create).toHaveBeenCalledWith( - expect.objectContaining({ - index: '.internal.alerts-observability.logs.alerts-default-000001', - }) - ); - }); - - it('should not install namespace level resources for the default space when framework alerts are available', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: { - ...frameworkAlertsService, - enabled: () => true, - getContextInitializationPromise: async () => ({ result: true }), - }, - pluginStop$, - }); - - const indexOptions = { - feature: AlertConsumers.LOGS, - registrationContext: 'observability.logs', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - }, - ], - }; - const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); - - await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); - expect(mockClusterClient.indices.simulateTemplate).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.create).not.toHaveBeenCalled(); - }); - - it('should throw error if framework was unable to install namespace level resources', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: { - ...frameworkAlertsService, - enabled: () => true, - }, - pluginStop$, - }); - - const indexOptions = { - feature: AlertConsumers.LOGS, - registrationContext: 'observability.logs', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - }, - ], - }; - const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); - - await expect( - installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default') - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"There was an error in the framework installing namespace-level resources and creating concrete indices for .alerts-observability.logs.alerts-default - failed"` - ); - expect(mockClusterClient.indices.simulateTemplate).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.create).not.toHaveBeenCalled(); - }); - - it('should not install namespace level resources for non-default space when framework alerts are available', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); - const installer = new ResourceInstaller({ - logger: loggerMock.create(), - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient, - frameworkAlerts: { - ...frameworkAlertsService, - enabled: () => true, - getContextInitializationPromise: async () => ({ result: true }), - }, - pluginStop$, + pluginStop$, + dataStreamAdapter, + }); + + const indexOptions = { + feature: AlertConsumers.LOGS, + registrationContext: 'observability.logs', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); + + await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); + expect(mockClusterClient.indices.simulateTemplate).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.create).not.toHaveBeenCalled(); + }); + + it('should throw error if framework was unable to install namespace level resources', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: { + ...frameworkAlertsService, + enabled: () => true, + }, + pluginStop$, + dataStreamAdapter, + }); + + const indexOptions = { + feature: AlertConsumers.LOGS, + registrationContext: 'observability.logs', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); + + await expect( + installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default') + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"There was an error in the framework installing namespace-level resources and creating concrete indices for .alerts-observability.logs.alerts-default - failed"` + ); + expect(mockClusterClient.indices.simulateTemplate).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.create).not.toHaveBeenCalled(); + }); + + it('should not install namespace level resources for non-default space when framework alerts are available', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + const installer = new ResourceInstaller({ + logger: loggerMock.create(), + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient, + frameworkAlerts: { + ...frameworkAlertsService, + enabled: () => true, + getContextInitializationPromise: async () => ({ result: true }), + }, + pluginStop$, + dataStreamAdapter, + }); + + const indexOptions = { + feature: AlertConsumers.LOGS, + registrationContext: 'observability.logs', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); + + await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'my-staging-space'); + expect(mockClusterClient.indices.simulateTemplate).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.getAlias).not.toHaveBeenCalled(); + expect(mockClusterClient.indices.create).not.toHaveBeenCalled(); + }); }); - const indexOptions = { - feature: AlertConsumers.LOGS, - registrationContext: 'observability.logs', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - }, - ], - }; - const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.1.0' }); - - await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'my-staging-space'); - expect(mockClusterClient.indices.simulateTemplate).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.getAlias).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.create).not.toHaveBeenCalled(); - }); - }); - - // These tests only test the updateAliasWriteIndexMapping() - // method of ResourceInstaller, however to test that, you - // have to call installAndUpdateNamespaceLevelResources(). - // So there's a bit of setup. But the only real difference - // with the tests is what the es client simulateIndexTemplate() - // mock returns, as set in the test. - describe('updateAliasWriteIndexMapping()', () => { - const SimulateTemplateResponse = { - template: { - aliases: { - alias_name_1: { - is_hidden: true, - }, - alias_name_2: { - is_hidden: true, - }, - }, - mappings: { enabled: false }, - settings: {}, - }, - }; - - const GetAliasResponse = { - real_index: { - aliases: { - alias_1: { - is_hidden: true, - }, - alias_2: { - is_hidden: true, - }, - }, - }, - }; - - function setup(mockClusterClient: ElasticsearchClientMock) { - mockClusterClient.indices.simulateTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - mockClusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); - - const logger = loggerMock.create(); - const resourceInstallerParams = { - logger, - isWriteEnabled: true, - disabledRegistrationContexts: [], - getResourceName: jest.fn(), - getClusterClient: async () => mockClusterClient, - frameworkAlerts: frameworkAlertsService, - pluginStop$, - }; - const indexOptions = { - feature: AlertConsumers.OBSERVABILITY, - registrationContext: 'observability.metrics', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', + // These tests only test the updateAliasWriteIndexMapping() + // method of ResourceInstaller, however to test that, you + // have to call installAndUpdateNamespaceLevelResources(). + // So there's a bit of setup. But the only real difference + // with the tests is what the es client simulateIndexTemplate() + // mock returns, as set in the test. + describe('updateAliasWriteIndexMapping()', () => { + const SimulateTemplateResponse = { + template: { + aliases: { + alias_name_1: { + is_hidden: true, + }, + alias_name_2: { + is_hidden: true, + }, + }, + mappings: { enabled: false }, + settings: {}, }, - ], - }; - - const installer = new ResourceInstaller(resourceInstallerParams); - const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.4.0' }); - - return { installer, indexInfo, logger }; - } - - it('succeeds on the happy path', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - mockClusterClient.indices.simulateIndexTemplate.mockImplementation( - async () => SimulateTemplateResponse - ); - - const { installer, indexInfo } = setup(mockClusterClient); - - let error: string | undefined; - try { - await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); - } catch (err) { - error = err.message; - } - expect(error).toBeFalsy(); - }); - - it('gracefully fails on error simulating mappings', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - mockClusterClient.indices.simulateIndexTemplate.mockImplementation(async () => { - throw new Error('expecting simulateIndexTemplate() to throw'); + }; + + function setup(mockClusterClient: ElasticsearchClientMock) { + mockClusterClient.indices.simulateTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + mockClusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + mockClusterClient.indices.getDataStream.mockImplementation( + async () => GetDataStreamResponse + ); + + const logger = loggerMock.create(); + const resourceInstallerParams = { + logger, + isWriteEnabled: true, + disabledRegistrationContexts: [], + getResourceName: jest.fn(), + getClusterClient: async () => mockClusterClient, + frameworkAlerts: frameworkAlertsService, + pluginStop$, + dataStreamAdapter, + }; + const indexOptions = { + feature: AlertConsumers.OBSERVABILITY, + registrationContext: 'observability.metrics', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + + const installer = new ResourceInstaller(resourceInstallerParams); + const indexInfo = new IndexInfo({ indexOptions, kibanaVersion: '8.4.0' }); + + return { installer, indexInfo, logger }; + } + + it('succeeds on the happy path', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + mockClusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + const { installer, indexInfo } = setup(mockClusterClient); + + let error: string | undefined; + try { + await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); + } catch (err) { + error = err.message; + } + expect(error).toBeFalsy(); + }); + + it('gracefully fails on error simulating mappings', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + mockClusterClient.indices.simulateIndexTemplate.mockImplementation(async () => { + throw new Error('expecting simulateIndexTemplate() to throw'); + }); + + const { installer, indexInfo, logger } = setup(mockClusterClient); + + let error: string | undefined; + try { + await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); + } catch (err) { + error = err.message; + } + expect(error).toBeFalsy(); + + const errorMessages = loggerMock.collect(logger).error; + if (useDataStreamForAlerts) { + expect(errorMessages).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignored PUT mappings for .alerts-observability.metrics.alerts-default; error generating simulated mappings: expecting simulateIndexTemplate() to throw", + ], + ] + `); + } else { + expect(errorMessages).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignored PUT mappings for alias_1; error generating simulated mappings: expecting simulateIndexTemplate() to throw", + ], + Array [ + "Ignored PUT mappings for alias_2; error generating simulated mappings: expecting simulateIndexTemplate() to throw", + ], + ] + `); + } + }); + + it('gracefully fails on empty mappings', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + mockClusterClient.indices.simulateIndexTemplate.mockImplementation(async () => ({})); + + const { installer, indexInfo, logger } = setup(mockClusterClient); + + let error: string | undefined; + try { + await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); + } catch (err) { + error = err.message; + } + expect(error).toBeFalsy(); + const errorMessages = loggerMock.collect(logger).error; + if (useDataStreamForAlerts) { + expect(errorMessages).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignored PUT mappings for .alerts-observability.metrics.alerts-default; simulated mappings were empty", + ], + ] + `); + } else { + expect(errorMessages).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignored PUT mappings for alias_1; simulated mappings were empty", + ], + Array [ + "Ignored PUT mappings for alias_2; simulated mappings were empty", + ], + ] + `); + } + }); }); - - const { installer, indexInfo, logger } = setup(mockClusterClient); - - let error: string | undefined; - try { - await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); - } catch (err) { - error = err.message; - } - expect(error).toBeFalsy(); - - const errorMessages = loggerMock.collect(logger).error; - expect(errorMessages).toMatchInlineSnapshot(` - Array [ - Array [ - "Ignored PUT mappings for alias alias_1; error generating simulated mappings: expecting simulateIndexTemplate() to throw", - ], - Array [ - "Ignored PUT mappings for alias alias_2; error generating simulated mappings: expecting simulateIndexTemplate() to throw", - ], - ] - `); - }); - - it('gracefully fails on empty mappings', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - mockClusterClient.indices.simulateIndexTemplate.mockImplementation(async () => ({})); - - const { installer, indexInfo, logger } = setup(mockClusterClient); - - let error: string | undefined; - try { - await installer.installAndUpdateNamespaceLevelResources(indexInfo, 'default'); - } catch (err) { - error = err.message; - } - expect(error).toBeFalsy(); - const errorMessages = loggerMock.collect(logger).error; - expect(errorMessages).toMatchInlineSnapshot(` - Array [ - Array [ - "Ignored PUT mappings for alias alias_1; simulated mappings were empty", - ], - Array [ - "Ignored PUT mappings for alias alias_2; simulated mappings were empty", - ], - ] - `); }); - }); + } }); diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 2956552bd78d2..225df2ffe1b89 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -20,6 +20,7 @@ import { installWithTimeout, TOTAL_FIELDS_LIMIT, type PublicFrameworkAlertsService, + type DataStreamAdapter, } from '@kbn/alerting-plugin/server'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../common/assets'; import { technicalComponentTemplate } from '../../common/assets/component_templates/technical_component_template'; @@ -34,6 +35,7 @@ interface ConstructorOptions { disabledRegistrationContexts: string[]; frameworkAlerts: PublicFrameworkAlertsService; pluginStop$: Observable; + dataStreamAdapter: DataStreamAdapter; } export type IResourceInstaller = PublicMethodsOf; @@ -78,6 +80,7 @@ export class ResourceInstaller { esClient: clusterClient, name: DEFAULT_ALERTS_ILM_POLICY_NAME, policy: DEFAULT_ALERTS_ILM_POLICY, + dataStreamAdapter: this.options.dataStreamAdapter, }), createOrUpdateComponentTemplate({ logger, @@ -143,6 +146,7 @@ export class ResourceInstaller { esClient: clusterClient, name: indexInfo.getIlmPolicyName(), policy: ilmPolicy, + dataStreamAdapter: this.options.dataStreamAdapter, }); } @@ -245,6 +249,7 @@ export class ResourceInstaller { kibanaVersion: indexInfo.kibanaVersion, namespace, totalFieldsLimit: TOTAL_FIELDS_LIMIT, + dataStreamAdapter: this.options.dataStreamAdapter, }), }); @@ -253,6 +258,7 @@ export class ResourceInstaller { esClient: clusterClient, totalFieldsLimit: TOTAL_FIELDS_LIMIT, indexPatterns, + dataStreamAdapter: this.options.dataStreamAdapter, }); } } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts index 2ba9146f4f2db..b2736ee7f3cfe 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts @@ -14,6 +14,9 @@ import { Dataset } from './index_options'; import { RuleDataClient } from '../rule_data_client/rule_data_client'; import { createRuleDataClientMock as mockCreateRuleDataClient } from '../rule_data_client/rule_data_client.mock'; +import { createDataStreamAdapterMock } from '@kbn/alerting-plugin/server/mocks'; +import type { DataStreamAdapter } from '@kbn/alerting-plugin/server'; + jest.mock('../rule_data_client/rule_data_client', () => ({ RuleDataClient: jest.fn().mockImplementation(() => mockCreateRuleDataClient()), })); @@ -25,10 +28,12 @@ const frameworkAlertsService = { describe('ruleDataPluginService', () => { let pluginStop$: Subject; + let dataStreamAdapter: DataStreamAdapter; beforeEach(() => { jest.resetAllMocks(); pluginStop$ = new ReplaySubject(1); + dataStreamAdapter = createDataStreamAdapterMock(); }); afterEach(() => { @@ -50,6 +55,7 @@ describe('ruleDataPluginService', () => { isWriterCacheEnabled: true, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); expect(ruleDataService.isRegistrationContextDisabled('observability.logs')).toBe(true); }); @@ -67,6 +73,7 @@ describe('ruleDataPluginService', () => { isWriterCacheEnabled: true, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); expect(ruleDataService.isRegistrationContextDisabled('observability.apm')).toBe(false); }); @@ -86,6 +93,7 @@ describe('ruleDataPluginService', () => { isWriterCacheEnabled: true, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); expect(ruleDataService.isWriteEnabled('observability.logs')).toBe(false); @@ -106,6 +114,7 @@ describe('ruleDataPluginService', () => { isWriterCacheEnabled: true, frameworkAlerts: frameworkAlertsService, pluginStop$, + dataStreamAdapter, }); const indexOptions = { feature: AlertConsumers.LOGS, diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts index 62f8cc88ca221..b17b10d5b7d26 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts @@ -11,7 +11,7 @@ import type { ValidFeatureId } from '@kbn/rule-data-utils'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { type PublicFrameworkAlertsService } from '@kbn/alerting-plugin/server'; +import type { PublicFrameworkAlertsService, DataStreamAdapter } from '@kbn/alerting-plugin/server'; import { INDEX_PREFIX } from '../config'; import { type IRuleDataClient, RuleDataClient, WaitResult } from '../rule_data_client'; import { IndexInfo } from './index_info'; @@ -94,6 +94,7 @@ interface ConstructorOptions { disabledRegistrationContexts: string[]; frameworkAlerts: PublicFrameworkAlertsService; pluginStop$: Observable; + dataStreamAdapter: DataStreamAdapter; } export class RuleDataService implements IRuleDataService { @@ -116,6 +117,7 @@ export class RuleDataService implements IRuleDataService { isWriteEnabled: options.isWriteEnabled, frameworkAlerts: options.frameworkAlerts, pluginStop$: options.pluginStop$, + dataStreamAdapter: options.dataStreamAdapter, }); this.installCommonResources = Promise.resolve(right('ok')); @@ -222,6 +224,7 @@ export class RuleDataService implements IRuleDataService { waitUntilReadyForReading, waitUntilReadyForWriting, logger: this.options.logger, + isUsingDataStreams: this.options.dataStreamAdapter.isUsingDataStreams(), }); } diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index 993405dd33e1f..7e8e0ac73f907 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -97,7 +97,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert documents - { index: { _id: expect.any(String) } }, + { create: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -105,7 +105,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [TAGS]: ['source-tag1', 'source-tag2', 'rule-tag1', 'rule-tag2'], }), - { index: { _id: expect.any(String) } }, + { create: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -120,7 +120,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: expect.arrayContaining([ // evaluation documents - { index: {} }, + { create: {} }, expect.objectContaining({ [EVENT_KIND]: 'event', }), @@ -151,6 +151,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -168,6 +171,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 1, + _primary_term: 3, }, ], }, @@ -222,7 +228,15 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { + index: { + _id: 'TEST_ALERT_0_UUID', + _index: 'alerts-index-name', + if_primary_term: 2, + if_seq_no: 4, + require_alias: false, + }, + }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_WORKFLOW_STATUS]: 'closed', @@ -232,7 +246,15 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { + index: { + _id: 'TEST_ALERT_1_UUID', + _index: 'alerts-index-name', + if_primary_term: 3, + if_seq_no: 1, + require_alias: false, + }, + }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_WORKFLOW_STATUS]: 'open', @@ -279,6 +301,9 @@ describe('createLifecycleExecutor', () => { labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc [TAGS]: ['source-tag1', 'source-tag2'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -296,6 +321,9 @@ describe('createLifecycleExecutor', () => { labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc [TAGS]: ['source-tag3', 'source-tag4'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -347,7 +375,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: expect.arrayContaining([ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, @@ -356,7 +384,7 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'close', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -510,6 +538,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -527,6 +558,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -622,6 +656,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -639,6 +676,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -733,6 +773,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -749,6 +792,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -841,6 +887,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -857,6 +906,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -957,7 +1009,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert documents - { index: { _id: expect.any(String) } }, + { create: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -966,7 +1018,7 @@ describe('createLifecycleExecutor', () => { [TAGS]: ['source-tag1', 'source-tag2', 'rule-tag1', 'rule-tag2'], [ALERT_MAINTENANCE_WINDOW_IDS]: maintenanceWindowIds, }), - { index: { _id: expect.any(String) } }, + { create: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -1013,6 +1065,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1030,6 +1085,9 @@ describe('createLifecycleExecutor', () => { [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -1086,7 +1144,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_WORKFLOW_STATUS]: 'closed', @@ -1095,7 +1153,7 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_WORKFLOW_STATUS]: 'open', @@ -1141,6 +1199,9 @@ describe('createLifecycleExecutor', () => { labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc [TAGS]: ['source-tag1', 'source-tag2'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1158,6 +1219,9 @@ describe('createLifecycleExecutor', () => { labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc [TAGS]: ['source-tag3', 'source-tag4'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -1210,7 +1274,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: expect.arrayContaining([ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, @@ -1219,7 +1283,7 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'close', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -1269,6 +1333,9 @@ describe('createLifecycleExecutor', () => { [ALERT_WORKFLOW_STATUS]: 'closed', [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1285,6 +1352,9 @@ describe('createLifecycleExecutor', () => { [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1301,6 +1371,9 @@ describe('createLifecycleExecutor', () => { [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1317,6 +1390,9 @@ describe('createLifecycleExecutor', () => { [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -1432,7 +1508,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: [ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_WORKFLOW_STATUS]: 'closed', @@ -1441,7 +1517,7 @@ describe('createLifecycleExecutor', () => { [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_WORKFLOW_STATUS]: 'open', @@ -1450,7 +1526,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: false, }), - { index: { _id: 'TEST_ALERT_2_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', [ALERT_WORKFLOW_STATUS]: 'open', @@ -1459,7 +1535,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: true, }), - { index: { _id: 'TEST_ALERT_3_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', [ALERT_WORKFLOW_STATUS]: 'open', @@ -1493,6 +1569,9 @@ describe('createLifecycleExecutor', () => { [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1508,6 +1587,9 @@ describe('createLifecycleExecutor', () => { [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1523,6 +1605,9 @@ describe('createLifecycleExecutor', () => { [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, { _source: { @@ -1538,6 +1623,9 @@ describe('createLifecycleExecutor', () => { [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [SPACE_IDS]: ['fake-space-id'], }, + _index: 'alerts-index-name', + _seq_no: 4, + _primary_term: 2, }, ], }, @@ -1637,7 +1725,7 @@ describe('createLifecycleExecutor', () => { expect.objectContaining({ body: expect.arrayContaining([ // alert document - { index: { _id: 'TEST_ALERT_0_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, @@ -1645,7 +1733,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: false, }), - { index: { _id: 'TEST_ALERT_1_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, @@ -1653,7 +1741,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: false, }), - { index: { _id: 'TEST_ALERT_2_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, @@ -1661,7 +1749,7 @@ describe('createLifecycleExecutor', () => { [EVENT_KIND]: 'signal', [ALERT_FLAPPING]: true, }), - { index: { _id: 'TEST_ALERT_3_UUID' } }, + { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, expect.objectContaining({ [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', [ALERT_STATUS]: ALERT_STATUS_RECOVERED, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index ce2570f7e5bcc..f91f6dfdf72d0 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -216,10 +216,14 @@ export const createLifecycleExecutor = `[Rule Registry] Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)` ); - const trackedAlertsDataMap: Record< - string, - { indexName: string; fields: Partial } - > = {}; + interface TrackedAlertData { + indexName: string; + fields: Partial; + seqNo: number | undefined; + primaryTerm: number | undefined; + } + + const trackedAlertsDataMap: Record = {}; if (trackedAlertStates.length) { const result = await fetchExistingAlerts( @@ -230,10 +234,19 @@ export const createLifecycleExecutor = result.forEach((hit) => { const alertInstanceId = hit._source ? hit._source[ALERT_INSTANCE_ID] : void 0; if (alertInstanceId && hit._source) { - trackedAlertsDataMap[alertInstanceId] = { - indexName: hit._index, - fields: hit._source, - }; + const alertLabel = `${rule.ruleTypeId}:${rule.id} ${alertInstanceId}`; + if (hit._seq_no == null) { + logger.error(`missing _seq_no on alert instance ${alertLabel}`); + } else if (hit._primary_term == null) { + logger.error(`missing _primary_term on alert instance ${alertLabel}`); + } else { + trackedAlertsDataMap[alertInstanceId] = { + indexName: hit._index, + fields: hit._source, + seqNo: hit._seq_no, + primaryTerm: hit._primary_term, + }; + } } }); } @@ -308,6 +321,8 @@ export const createLifecycleExecutor = return { indexName: alertData?.indexName, + seqNo: alertData?.seqNo, + primaryTerm: alertData?.primaryTerm, event, flappingHistory, flapping, @@ -335,10 +350,22 @@ export const createLifecycleExecutor = logger.debug(`[Rule Registry] Preparing to index ${allEventsToIndex.length} alerts.`); await ruleDataClientWriter.bulk({ - body: allEventsToIndex.flatMap(({ event, indexName }) => [ + body: allEventsToIndex.flatMap(({ event, indexName, seqNo, primaryTerm }) => [ indexName - ? { index: { _id: event[ALERT_UUID]!, _index: indexName, require_alias: false } } - : { index: { _id: event[ALERT_UUID]! } }, + ? { + index: { + _id: event[ALERT_UUID]!, + _index: indexName, + if_seq_no: seqNo, + if_primary_term: primaryTerm, + require_alias: false, + }, + } + : { + create: { + _id: event[ALERT_UUID]!, + }, + }, event, ]), refresh: 'wait_for', diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 971b4ec735086..bbdd4806b55e7 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -16,7 +16,6 @@ import { } from '@kbn/rule-data-utils'; import { loggerMock } from '@kbn/logging-mocks'; import { castArray, omit } from 'lodash'; -import { RuleDataClient } from '../rule_data_client'; import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_factory'; import { ISearchStartSearchSource } from '@kbn/data-plugin/common'; @@ -30,7 +29,7 @@ function createRule(shouldWriteAlerts: boolean = true) { const ruleDataClientMock = createRuleDataClientMock(); const factory = createLifecycleRuleTypeFactory({ - ruleDataClient: ruleDataClientMock as unknown as RuleDataClient, + ruleDataClient: ruleDataClientMock, logger: loggerMock.create(), }); @@ -227,7 +226,7 @@ describe('createLifecycleRuleTypeFactory', () => { const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[0][0].body!; - const documents = body.filter((op: any) => !('index' in op)) as any[]; + const documents: any[] = body.filter((op: any) => !isOpDoc(op)); const evaluationDocuments = documents.filter((doc) => doc['event.kind'] === 'event'); const alertDocuments = documents.filter((doc) => doc['event.kind'] === 'signal'); @@ -347,9 +346,10 @@ describe('createLifecycleRuleTypeFactory', () => { ).bulk.mock.calls[0][0].body ?.concat() .reverse() - .find( - (doc: any) => !('index' in doc) && doc['service.name'] === 'opbeans-node' - ) as Record; + .find((doc: any) => !isOpDoc(doc) && doc['service.name'] === 'opbeans-node') as Record< + string, + any + >; // @ts-ignore 4.3.5 upgrade helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ @@ -390,7 +390,7 @@ describe('createLifecycleRuleTypeFactory', () => { expect((await helpers.ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledTimes(2); const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[1][0].body!; - const documents = body.filter((op: any) => !('index' in op)) as any[]; + const documents: any[] = body.filter((op: any) => !isOpDoc(op)); const evaluationDocuments = documents.filter((doc) => doc['event.kind'] === 'event'); const alertDocuments = documents.filter((doc) => doc['event.kind'] === 'signal'); @@ -429,13 +429,16 @@ describe('createLifecycleRuleTypeFactory', () => { ).bulk.mock.calls[0][0].body ?.concat() .reverse() - .find( - (doc: any) => !('index' in doc) && doc['service.name'] === 'opbeans-node' - ) as Record; + .find((doc: any) => !isOpDoc(doc) && doc['service.name'] === 'opbeans-node') as Record< + string, + any + >; helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ hits: { - hits: [{ _source: lastOpbeansNodeDoc } as any], + hits: [ + { _source: lastOpbeansNodeDoc, _index: 'a', _primary_term: 4, _seq_no: 2 } as any, + ], total: { value: 1, relation: 'eq', @@ -465,7 +468,7 @@ describe('createLifecycleRuleTypeFactory', () => { const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[1][0].body!; - const documents = body.filter((op: any) => !('index' in op)) as any[]; + const documents: any[] = body.filter((op: any) => !isOpDoc(op)); const opbeansJavaAlertDoc = documents.find( (doc) => castArray(doc['service.name'])[0] === 'opbeans-java' @@ -487,3 +490,9 @@ describe('createLifecycleRuleTypeFactory', () => { }); }); }); + +function isOpDoc(doc: any) { + if (doc?.index?._id) return true; + if (doc?.create?._id) return true; + return false; +} diff --git a/x-pack/plugins/saved_objects_tagging/server/plugin.ts b/x-pack/plugins/saved_objects_tagging/server/plugin.ts index 39ab63e1b01d4..f62d84f5742e9 100644 --- a/x-pack/plugins/saved_objects_tagging/server/plugin.ts +++ b/x-pack/plugins/saved_objects_tagging/server/plugin.ts @@ -54,11 +54,14 @@ export class SavedObjectTaggingPlugin features.registerKibanaFeature(savedObjectsTaggingFeature); if (usageCollection) { - const kibanaIndices = savedObjects.getAllIndices(); + const getKibanaIndices = () => + getStartServices() + .then(([core]) => core.savedObjects.getAllIndices()) + .catch(() => []); usageCollection.registerCollector( createTagUsageCollector({ usageCollection, - kibanaIndices, + getKibanaIndices, }) ); } diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts b/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts index 9af63f3cb6adb..d0d33e5b7d995 100644 --- a/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts +++ b/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts @@ -12,17 +12,17 @@ import { tagUsageCollectorSchema } from './schema'; export const createTagUsageCollector = ({ usageCollection, - kibanaIndices, + getKibanaIndices, }: { usageCollection: UsageCollectionSetup; - kibanaIndices: string[]; + getKibanaIndices: () => Promise; }) => { return usageCollection.makeUsageCollector({ type: 'saved_objects_tagging', isReady: () => true, schema: tagUsageCollectorSchema, fetch: async ({ esClient }) => { - return fetchTagUsageData({ esClient, kibanaIndices }); + return fetchTagUsageData({ esClient, kibanaIndices: await getKibanaIndices() }); }, }); }; diff --git a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap index b6e5caaf0d95d..b5486ba5d649d 100644 --- a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap +++ b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PromptPage renders as expected with additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; +exports[`PromptPage renders as expected with additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; -exports[`PromptPage renders as expected without additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; +exports[`PromptPage renders as expected without additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; diff --git a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap index 27612db490f4b..a752facec0213 100644 --- a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UnauthenticatedPage renders as expected 1`] = `"ElasticMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; +exports[`UnauthenticatedPage renders as expected 1`] = `"ElasticMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; -exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"My Company NameMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; +exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"My Company NameMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap index a6b592367d47b..cc55a03d84555 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSessionPage renders as expected 1`] = `"ElasticMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected 1`] = `"ElasticMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; -exports[`ResetSessionPage renders as expected with custom page title 1`] = `"My Company NameMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected with custom page title 1`] = `"My Company NameMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; diff --git a/x-pack/plugins/security/server/authorization/app_authorization.test.ts b/x-pack/plugins/security/server/authorization/app_authorization.test.ts index d745cf2f60769..b64226e8ecd06 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.test.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.test.ts @@ -11,17 +11,22 @@ import { httpServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; -import type { PluginSetupContract as FeaturesSetupContract } from '@kbn/features-plugin/server'; +import type { + PluginSetupContract as FeaturesSetupContract, + KibanaFeature, +} from '@kbn/features-plugin/server'; import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initAppAuthorization } from './app_authorization'; import { authorizationMock } from './index.mock'; -const createFeaturesSetupContractMock = (): FeaturesSetupContract => { +const createFeaturesSetupContractMock = ( + features: KibanaFeature[] = [ + { id: 'foo', name: 'Foo', app: ['foo'], privileges: {} } as unknown as KibanaFeature, + ] +): FeaturesSetupContract => { const mock = featuresPluginMock.createSetup(); - mock.getKibanaFeatures.mockReturnValue([ - { id: 'foo', name: 'Foo', app: ['foo'], privileges: {} } as any, - ]); + mock.getKibanaFeatures.mockReturnValue(features); return mock; }; @@ -97,6 +102,33 @@ describe('initAppAuthorization', () => { expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); + test(`unprotected route that starts with "/app/", from feature that opted out of RBAC, and "mode.useRbacForRequest()" returns true continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create(); + initAppAuthorization( + mockHTTPSetup, + mockAuthz, + loggingSystemMock.create().get(), + createFeaturesSetupContractMock([ + { id: 'foo', name: 'Foo', app: ['foo'], privileges: null } as unknown as KibanaFeature, + ]) + ); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', path: '/app/foo' }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + mockAuthz.mode.useRbacForRequest.mockReturnValue(true); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); + test(`protected route that starts with "/app/", "mode.useRbacForRequest()" returns true and user is authorized continues`, async () => { const mockHTTPSetup = coreMock.createSetup().http; const mockAuthz = authorizationMock.create({ version: '1.0.0-zeta1' }); diff --git a/x-pack/plugins/security/server/authorization/app_authorization.ts b/x-pack/plugins/security/server/authorization/app_authorization.ts index 0d6d1ef01d381..08630efc62241 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.ts @@ -20,10 +20,11 @@ class ProtectedApplications { // we wait until we actually need to consume these before getting them if (this.applications == null) { this.applications = new Set( - this.featuresService - .getKibanaFeatures() - .map((feature) => feature.app) - .flat() + this.featuresService.getKibanaFeatures().flatMap((feature) => { + // If the feature has explicitly opted out of our RBAC by setting the `privileges` field to `null`, we + // shouldn't check permissions when the app defined by such feature is accessed. + return feature.privileges === null ? [] : feature.app; + }) ); } diff --git a/x-pack/plugins/security/server/authorization/reset_session_page.tsx b/x-pack/plugins/security/server/authorization/reset_session_page.tsx index 9cb85f324fab9..85c78ddfcbaec 100644 --- a/x-pack/plugins/security/server/authorization/reset_session_page.tsx +++ b/x-pack/plugins/security/server/authorization/reset_session_page.tsx @@ -15,6 +15,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { PromptPage } from '../prompt_page'; +/** + * Static error page (rendered server-side) when user does not have permission to access the requested page. + * + * To trigger this error create a user without any roles and try to login. + */ export function ResetSessionPage({ logoutUrl, buildNumber, diff --git a/x-pack/plugins/security/server/prompt_page.tsx b/x-pack/plugins/security/server/prompt_page.tsx index 31c1f057942de..c0947aed8477b 100644 --- a/x-pack/plugins/security/server/prompt_page.tsx +++ b/x-pack/plugins/security/server/prompt_page.tsx @@ -5,13 +5,8 @@ * 2.0. */ -import { - EuiEmptyPrompt, - EuiPage, - EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiProvider, -} from '@elastic/eui'; +import 'css.escape'; // Polyfill required to render `EuiPageTemplate` server-side +import { EuiPageTemplate, EuiProvider } from '@elastic/eui'; // @ts-expect-error no definitions in component folder import { icon as EuiIconWarning } from '@elastic/eui/lib/components/icon/assets/warning'; // @ts-expect-error no definitions in component folder @@ -61,19 +56,15 @@ export function PromptPage({ const content = ( - - - - {title}} - body={body} - actions={actions} - /> - - - + + {title}} + body={body} + actions={actions} + /> + ); @@ -88,8 +79,6 @@ export function PromptPage({ const styleSheetPaths = [ `${regularBundlePath}/kbn-ui-shared-deps-src/${UiSharedDepsSrc.cssDistFilename}`, `${regularBundlePath}/kbn-ui-shared-deps-npm/${UiSharedDepsNpm.lightCssDistFilename('v8')}`, - `${basePath.serverBasePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, - `${basePath.serverBasePath}/ui/legacy_light_theme.css`, ]; return ( diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts index 1c5883c052135..f6a394e8c46c9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts @@ -127,14 +127,17 @@ const ensureEndpointRuleAlertsIndexExists = async (esClient: Client): Promise export const EnrichmentIcon: React.FC<{ type: string | undefined }> = ({ type }) => { return ( - + ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_no_data.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_no_data.tsx index 25cb3b4af89af..8241054ecc940 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_no_data.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_no_data.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; import styled from 'styled-components'; - +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink } from '@elastic/eui'; import * as i18n from './translations'; import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; @@ -19,9 +20,27 @@ export const EnrichmentNoData: React.FC<{ type?: ENRICHMENT_TYPES }> = ({ type } if (!type) return null; return ( - {type === ENRICHMENT_TYPES.IndicatorMatchRule - ? i18n.NO_ENRICHMENTS_FOUND_DESCRIPTION - : i18n.NO_INVESTIGATION_ENRICHMENTS_DESCRIPTION} + {type === ENRICHMENT_TYPES.IndicatorMatchRule ? ( + i18n.NO_ENRICHMENTS_FOUND_DESCRIPTION + ) : ( + + +
+ ), + }} + /> + )} ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx index 10d50cce74c9f..2ca1ba45e01cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx @@ -64,7 +64,7 @@ describe('ThreatSummaryView', () => { ); - expect(getByText('Enriched with Threat Intelligence')).toBeInTheDocument(); + expect(getByText('Enriched with threat intelligence')).toBeInTheDocument(); expect(getAllByTestId('EnrichedDataRow')).toHaveLength( enrichments.length + RISK_SCORE_DATA_ROWS diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts index b302a50937597..09c233c73cca1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -20,22 +20,21 @@ export const FEED_NAME_PREPOSITION = i18n.translate( export const INDICATOR_ENRICHMENT_TITLE = i18n.translate( 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTitle', { - defaultMessage: 'Threat Match Detected', + defaultMessage: 'Threat match detected', } ); export const INVESTIGATION_ENRICHMENT_TITLE = i18n.translate( 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle', { - defaultMessage: 'Enriched with Threat Intelligence', + defaultMessage: 'Enriched with threat intelligence', } ); export const INDICATOR_TOOLTIP_CONTENT = i18n.translate( 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent', { - defaultMessage: - 'This field value matched a threat intelligence indicator with a rule you created.', + defaultMessage: 'Shows available threat indicator matches.', } ); @@ -50,23 +49,21 @@ export const INVESTIGATION_TOOLTIP_CONTENT = i18n.translate( 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent', { defaultMessage: - 'This field value has additional information available from threat intelligence sources.', + 'Shows additional threat intelligence for the alert. The past 30 days were queried by default.', } ); export const NO_INVESTIGATION_ENRICHMENTS_DESCRIPTION = i18n.translate( 'xpack.securitySolution.alertDetails.noInvestigationEnrichmentsDescription', { - defaultMessage: - "We haven't found field value has additional information available from threat intelligence sources we searched in the past 30 days by default.", + defaultMessage: 'This alert does not have supplemental threat intelligence data.', } ); export const NO_ENRICHMENTS_FOUND_DESCRIPTION = i18n.translate( 'xpack.securitySolution.alertDetails.noEnrichmentsFoundDescription', { - defaultMessage: - 'We did not find threat intelligence that matches any of the indicator match rules, or any enrichment for this alert.', + defaultMessage: 'This alert does not have threat intelligence.', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 711ffc34e49d7..5133faaeb9f4f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -32,6 +32,7 @@ export const search = { incremental: true, placeholder: i18n.PLACEHOLDER, schema: true, + 'data-test-subj': 'search-input', }, }; diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx index d7e887d54ea6e..d8d57ba17ef0d 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx @@ -44,6 +44,7 @@ jest.mock('../../lib/kibana', () => { const originalKibanaLib = jest.requireActual('../../lib/kibana'); return { + ...originalKibanaLib, useKibana: () => ({ services: { application: { @@ -71,7 +72,6 @@ jest.mock('../../lib/kibana', () => { useNavigateTo: jest.fn().mockReturnValue({ navigateTo: jest.fn(), }), - useGetUserCasesPermissions: originalKibanaLib.useGetUserCasesPermissions, }; }); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index c2d1e6dc59ff8..e14a0c7f4c224 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -12,6 +12,7 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import type { AggregationsTermsAggregateBase } from '@elastic/elasticsearch/lib/api/types'; +import { isString } from 'lodash/fp'; import * as i18n from './translations'; import { HeaderSection } from '../header_section'; import { Panel } from '../panel'; @@ -66,6 +67,7 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps & scopeId?: string; title: string | GetTitle; hideQueryToggle?: boolean; + applyGlobalQueriesAndFilters?: boolean; }; const DEFAULT_PANEL_HEIGHT = 300; @@ -117,6 +119,7 @@ export const MatrixHistogramComponent: React.FC = yTickFormatter, skip, hideQueryToggle = false, + applyGlobalQueriesAndFilters = true, }) => { const visualizationId = `${id}-embeddable`; const dispatch = useDispatch(); @@ -258,9 +261,20 @@ export const MatrixHistogramComponent: React.FC = const timerange = useMemo(() => ({ from: startDate, to: endDate }), [startDate, endDate]); const extraVisualizationOptions = useMemo( - () => ({ dnsIsPtrIncluded: isPtrIncluded ?? false }), - [isPtrIncluded] + () => ({ + dnsIsPtrIncluded: isPtrIncluded ?? false, + filters: filterQuery + ? [ + { + query: isString(filterQuery) ? JSON.parse(filterQuery) : filterQuery, + meta: {}, + }, + ] + : undefined, + }), + [isPtrIncluded, filterQuery] ); + if (hideHistogram) { return null; } @@ -329,6 +343,7 @@ export const MatrixHistogramComponent: React.FC = {toggleStatus ? ( isChartEmbeddablesEnabled ? ( => { if (signalIds && signalIds.length > 0) { - return updateAlertStatusByIds({ status, signalIds, signal }).then(({ items }) => ({ - updated: items.length, + return updateAlertStatusByIds({ status, signalIds, signal }).then(({ updated }) => ({ + updated: updated ?? 0, version_conflicts: 0, })); } else if (query) { diff --git a/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_set_alert_tags.tsx b/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_set_alert_tags.tsx index 140e2d0227453..b24719c9dd0d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_set_alert_tags.tsx +++ b/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_set_alert_tags.tsx @@ -38,7 +38,7 @@ export const useSetAlertTags = (): ReturnSetAlertTags => { const setAlertTagsRef = useRef(null); const onUpdateSuccess = useCallback( - (updated: number) => addSuccess(i18n.UPDATE_ALERT_TAGS_SUCCESS_TOAST(updated)), + (updated: number = 0) => addSuccess(i18n.UPDATE_ALERT_TAGS_SUCCESS_TOAST(updated)), [addSuccess] ); @@ -60,7 +60,7 @@ export const useSetAlertTags = (): ReturnSetAlertTags => { if (!ignore) { onSuccess(); setTableLoading(false); - onUpdateSuccess(response.items.length); + onUpdateSuccess(response.updated); } } catch (error) { if (!ignore) { diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index db46bc0abf277..742cfafea6dcc 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -116,6 +116,7 @@ const TopNComponent: React.FC = ({ {view === 'raw' || view === 'all' ? ( { return { title: 'Events', @@ -55,7 +56,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( query: '', language: 'kuery', }, - filters: [], + filters: extraOptions.filters ?? [], datasourceStates: { formBased: { layers: { diff --git a/x-pack/plugins/security_solution/public/common/containers/alert_tags/api.ts b/x-pack/plugins/security_solution/public/common/containers/alert_tags/api.ts index 6ff7cd515cad2..daa378af198a2 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alert_tags/api.ts +++ b/x-pack/plugins/security_solution/public/common/containers/alert_tags/api.ts @@ -18,10 +18,13 @@ export const setAlertTags = async ({ tags: AlertTags; ids: string[]; signal: AbortSignal | undefined; -}): Promise => { - return KibanaServices.get().http.fetch(DETECTION_ENGINE_ALERT_TAGS_URL, { - method: 'POST', - body: JSON.stringify({ tags, ids }), - signal, - }); +}): Promise => { + return KibanaServices.get().http.fetch( + DETECTION_ENGINE_ALERT_TAGS_URL, + { + method: 'POST', + body: JSON.stringify({ tags, ids }), + signal, + } + ); }; diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx index 41479adb42623..ac26f9038d5d4 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx @@ -31,6 +31,13 @@ jest.mock('@kbn/dashboard-plugin/public', () => { }; }); +const mockUseObservable = jest.fn(); + +jest.mock('react-use', () => ({ + ...jest.requireActual('react-use'), + useObservable: () => mockUseObservable(), +})); + const DEFAULT_DASHBOARD_CAPABILITIES = { show: true, createNew: true }; const mockUseCapabilities = useCapabilities as jest.Mock; mockUseCapabilities.mockReturnValue(DEFAULT_DASHBOARD_CAPABILITIES); @@ -210,4 +217,13 @@ describe('Dashboards landing', () => { }); }); }); + + it('should render callout when available', async () => { + const DummyComponent = () => ; + mockUseObservable.mockReturnValue(); + + await renderDashboardLanding(); + + expect(screen.queryByTestId('test')).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx index 7d579c66e8191..8cb12d5895021 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx @@ -17,10 +17,11 @@ import React, { useCallback, useMemo } from 'react'; import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common/types'; import { DashboardListingTable, LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; import { LandingLinksImageCards } from '@kbn/security-solution-navigation/landing_links'; +import { useObservable } from 'react-use'; import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../../../common/constants'; -import { useCapabilities, useNavigateTo } from '../../../common/lib/kibana'; +import { useCapabilities, useKibana, useNavigateTo } from '../../../common/lib/kibana'; import { useRootNavLink } from '../../../common/links/nav_links'; import { Title } from '../../../common/components/header_page/title'; import { LinkButton } from '../../../common/components/links/helpers'; @@ -82,6 +83,8 @@ const Header: React.FC<{ canCreateDashboard: boolean }> = ({ canCreateDashboard }; export const DashboardsLandingPage = () => { + const { dashboardsLandingCalloutComponent$ } = useKibana().services; + const dashboardLandingCallout = useObservable(dashboardsLandingCalloutComponent$); const { links = [] } = useRootNavLink(SecurityPageName.dashboards) ?? {}; const urlState = useGlobalQueryString(); const { show: canReadDashboard, createNew: canCreateDashboard } = @@ -121,6 +124,13 @@ export const DashboardsLandingPage = () => {
+ {dashboardLandingCallout && ( + <> + {dashboardLandingCallout} + + + )} +

{i18n.DASHBOARDS_PAGE_SECTION_DEFAULT}

diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 5f9e1f17dba18..44e41f0f9ebd3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -410,7 +410,7 @@ const RuleDetailsPageComponent: React.FC = ({ ) : ( { - const { addError } = useAppToasts(); - - return useQuery( - ['GET', BASE_ACTION_API_PATH, 'connectors'], - ({ signal }) => fetchConnectors(signal), - { - onError: (error) => { - addError(error, { - title: i18n.CONNECTORS_FETCH_ERROR, - toastMessage: i18n.ACTIONS_FETCH_ERROR_DESCRIPTION, - }); - }, - } - ); -}; - -export const useFetchConnectorTypes = () => { +export const useFetchConnectorTypesQuery = () => { const { addError } = useAppToasts(); return useQuery( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_connectors_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_connectors_query.ts new file mode 100644 index 0000000000000..32df9d73b7cd8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_connectors_query.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 { useQuery } from '@tanstack/react-query'; +import { BASE_ACTION_API_PATH } from '@kbn/actions-plugin/common'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { fetchConnectors } from '../api'; +import * as i18n from './translations'; + +export const useFetchConnectorsQuery = () => { + const { addError } = useAppToasts(); + + return useQuery( + ['GET', BASE_ACTION_API_PATH, 'connectors'], + ({ signal }) => fetchConnectors(signal), + { + onError: (error) => { + addError(error, { + title: i18n.CONNECTORS_FETCH_ERROR, + toastMessage: i18n.ACTIONS_FETCH_ERROR_DESCRIPTION, + }); + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_coverage_overview.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_coverage_overview_query.ts similarity index 100% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_coverage_overview.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_coverage_overview_query.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings_query.ts similarity index 97% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings_query.ts index 8963baab0362b..8d2ca14d79f9b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings_query.ts @@ -23,7 +23,7 @@ const FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY = ['GET', INTERNAL_ALERTING_API_FIND_ * @param queryOptions - react-query options * @returns useQuery result */ -export const useFetchRulesSnoozeSettings = ( +export const useFetchRulesSnoozeSettingsQuery = ( ids: string[], queryOptions?: UseQueryOptions ) => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts index 932c50ff2a46f..8f3bd6aef95c0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts @@ -13,7 +13,7 @@ import { updateRule } from '../api'; import { useInvalidateFindRulesQuery } from './use_find_rules_query'; import { useUpdateRuleByIdCache } from './use_fetch_rule_by_id_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query'; -import { useInvalidateFetchCoverageOverviewQuery } from './use_fetch_coverage_overview'; +import { useInvalidateFetchCoverageOverviewQuery } from './use_fetch_coverage_overview_query'; import type { Rule } from '../../logic/types'; export const UPDATE_RULE_MUTATION_KEY = ['PUT', DETECTION_ENGINE_RULES_URL]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/rule_snooze_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/rule_snooze_badge.tsx index 9e45f70688a98..2f48e2ed6f356 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/rule_snooze_badge.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/rule_snooze_badge.tsx @@ -10,7 +10,7 @@ import type { RuleObjectId } from '../../../../../common/api/detection_engine/mo import { useUserData } from '../../../../detections/components/user_info'; import { hasUserCRUDPermission } from '../../../../common/utils/privileges'; import { useKibana } from '../../../../common/lib/kibana'; -import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../../api/hooks/use_fetch_rules_snooze_settings'; +import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../../api/hooks/use_fetch_rules_snooze_settings_query'; import { useRuleSnoozeSettings } from './use_rule_snooze_settings'; interface RuleSnoozeBadgeProps { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/use_rule_snooze_settings.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/use_rule_snooze_settings.ts index e31851a6a24b0..1fe40fd435c12 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/use_rule_snooze_settings.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/use_rule_snooze_settings.ts @@ -6,7 +6,7 @@ */ import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types'; -import { useFetchRulesSnoozeSettings } from '../../api/hooks/use_fetch_rules_snooze_settings'; +import { useFetchRulesSnoozeSettingsQuery } from '../../api/hooks/use_fetch_rules_snooze_settings_query'; import { useRulesTableContextOptional } from '../../../rule_management_ui/components/rules_table/rules_table/rules_table_context'; import * as i18n from './translations'; @@ -23,7 +23,7 @@ export function useRuleSnoozeSettings(id: string): UseRuleSnoozeSettingsResult { data: rulesSnoozeSettings, isFetching: isSingleSnoozeSettingsFetching, isError: isSingleSnoozeSettingsError, - } = useFetchRulesSnoozeSettings([id], { + } = useFetchRulesSnoozeSettingsQuery([id], { enabled: !rulesTableSnoozeSettings?.data[id] && !rulesTableSnoozeSettings?.isFetching, }); const snoozeSettings = rulesTableSnoozeSettings?.data[id] ?? rulesSnoozeSettings?.[id]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx index 64d82b50c9e84..0a9928200116c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx @@ -11,7 +11,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useUiSetting$ } from '../../../../../common/lib/kibana'; import type { Rule, RulesSnoozeSettingsMap } from '../../../../rule_management/logic'; import { useFindRules } from '../../../../rule_management/logic/use_find_rules'; -import { useFetchRulesSnoozeSettings } from '../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings'; +import { useFetchRulesSnoozeSettingsQuery } from '../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings_query'; import type { RulesTableState } from './rules_table_context'; import { RulesTableContextProvider, useRulesTableContext } from './rules_table_context'; import { @@ -26,7 +26,7 @@ import { useRulesTableSavedState } from './use_rules_table_saved_state'; jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../rule_management/logic/use_find_rules'); jest.mock('../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review'); -jest.mock('../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings'); +jest.mock('../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings_query'); jest.mock('./use_rules_table_saved_state'); function renderUseRulesTableContext({ @@ -48,7 +48,7 @@ function renderUseRulesTableContext({ isRefetching: false, isError: rules instanceof Error, }); - (useFetchRulesSnoozeSettings as jest.Mock).mockReturnValue({ + (useFetchRulesSnoozeSettingsQuery as jest.Mock).mockReturnValue({ data: rulesSnoozeSettings instanceof Error ? undefined : rulesSnoozeSettings, isError: rulesSnoozeSettings instanceof Error, }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx index 970dcd70bfc25..791a4b9872fb5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx @@ -15,7 +15,7 @@ import React, { useRef, useState, } from 'react'; -import { useFetchRulesSnoozeSettings } from '../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings'; +import { useFetchRulesSnoozeSettingsQuery } from '../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings_query'; import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants'; import { invariant } from '../../../../../../common/utils/invariant'; import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state'; @@ -307,7 +307,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide isFetching: isSnoozeSettingsFetching, isError: isSnoozeSettingsFetchError, refetch: refetchSnoozeSettings, - } = useFetchRulesSnoozeSettings( + } = useFetchRulesSnoozeSettingsQuery( rules.map((x) => x.id), { enabled: rules.length > 0 } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx index a761153c1e8d0..d072b1729688a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx @@ -7,7 +7,7 @@ import { render } from '@testing-library/react'; import React from 'react'; -import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview'; +import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview_query'; import { getMockCoverageOverviewDashboard } from '../../../rule_management/model/coverage_overview/__mocks__'; import { TestProviders } from '../../../../common/mock'; @@ -15,7 +15,7 @@ import { CoverageOverviewDashboard } from './coverage_overview_dashboard'; import { CoverageOverviewDashboardContextProvider } from './coverage_overview_dashboard_context'; jest.mock('../../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null })); -jest.mock('../../../rule_management/api/hooks/use_fetch_coverage_overview'); +jest.mock('../../../rule_management/api/hooks/use_fetch_coverage_overview_query'); (useFetchCoverageOverviewQuery as jest.Mock).mockReturnValue({ data: getMockCoverageOverviewDashboard(), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx index e76075535b193..3a2424664f8ab 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx @@ -27,7 +27,7 @@ import { SET_RULE_SEARCH_FILTER, createCoverageOverviewDashboardReducer, } from './coverage_overview_dashboard_reducer'; -import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview'; +import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview_query'; import { useExecuteBulkAction } from '../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; export interface CoverageOverviewDashboardActions { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index b27a322331fc7..d92ac003cfba9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -32,7 +32,7 @@ import { useInvalidateFindRulesQuery } from '../../../rule_management/api/hooks/ import { importRules } from '../../../rule_management/logic'; import { AllRules } from '../../components/rules_table'; import { RulesTableContextProvider } from '../../components/rules_table/rules_table/rules_table_context'; -import { useInvalidateFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview'; +import { useInvalidateFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview_query'; import { HeaderPage } from '../../../../common/components/header_page'; const RulesPageComponent: React.FC = () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_response_action.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_response_action.tsx index f8e1a60bda79f..9f9ad505ff332 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_response_action.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_response_action.tsx @@ -9,13 +9,13 @@ import React, { useMemo } from 'react'; import { EuiCode, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useIsMounted } from '@kbn/securitysolution-hook-utils'; +import { AppFeatureKey } from '@kbn/security-solution-features/keys'; import { useUpsellingComponent } from '../../../common/hooks/use_upselling'; -import { AppFeatureKey } from '../../../../common'; import { ResponseActionFormField } from './osquery_response_action_form_field'; import type { ArrayItem } from '../../../shared_imports'; +import { UseField } from '../../../shared_imports'; import { useKibana } from '../../../common/lib/kibana'; import { NOT_AVAILABLE, PERMISSION_DENIED, SHORT_EMPTY_TITLE } from './translations'; -import { UseField } from '../../../shared_imports'; interface OsqueryResponseActionProps { item: ArrayItem; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 9907aea709e58..b180856da2b29 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -57,31 +57,36 @@ const props = { timelineId: 'alerts-page', }; -jest.mock('../../../../common/lib/kibana', () => ({ - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - remove: jest.fn(), - }), - useKibana: () => ({ - services: { - timelines: { ...mockTimelines }, - application: { - capabilities: { siem: { crud_alerts: true, read_alerts: true } }, +jest.mock('../../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../../common/lib/kibana'); + + return { + ...original, + useToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + remove: jest.fn(), + }), + useKibana: () => ({ + services: { + timelines: { ...mockTimelines }, + application: { + capabilities: { siem: { crud_alerts: true, read_alerts: true } }, + }, + cases: mockCasesContract(), }, - cases: mockCasesContract(), - }, - }), - useGetUserCasesPermissions: jest.fn().mockReturnValue({ - all: true, - create: true, - read: true, - update: true, - delete: true, - push: true, - }), -})); + }), + useGetUserCasesPermissions: jest.fn().mockReturnValue({ + all: true, + create: true, + read: true, + update: true, + delete: true, + push: true, + }), + }; +}); jest.mock('../../../containers/detection_engine/alerts/use_alerts_privileges', () => ({ useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 0e1b32c3c77d9..a05c351f3d22d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { EuiButtonIcon, EuiPopover, EuiToolTip, EuiContextMenu } from '@elastic/eui'; +import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiToolTip } from '@elastic/eui'; import { indexOf } from 'lodash'; import type { ConnectedProps } from 'react-redux'; import { connect } from 'react-redux'; @@ -36,7 +36,7 @@ import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_ import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/event_filters_flyout'; import { useAlertsActions } from './use_alerts_actions'; import { useExceptionFlyout } from './use_add_exception_flyout'; -import { useExceptionActions } from './use_add_exception_actions'; +import { useAlertExceptionActions } from './use_add_exception_actions'; import { useEventFilterModal } from './use_event_filter_modal'; import type { Status } from '../../../../../common/api/detection_engine'; import { ATTACH_ALERT_TO_CASE_FOR_ROW } from '../../../../timelines/components/timeline/body/translations'; @@ -196,7 +196,7 @@ const AlertContextMenuComponent: React.FC { + const { exceptionActionItems } = useExceptionActions({ + isEndpointAlert, + onAddExceptionTypeClick, + }); + + const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } = + useListsConfig(); + const canReadEndpointExceptions = useHasSecurityCapability('crudEndpointExceptions'); + + const canWriteEndpointExceptions = useMemo( + () => !listsConfigLoading && !needsListsConfiguration && canReadEndpointExceptions, + [canReadEndpointExceptions, listsConfigLoading, needsListsConfiguration] + ); + // Endpoint exceptions are available for: + // Serverless Endpoint Essentials/Complete PLI and + // on ESS Security Kibana sub-feature Endpoint Exceptions (enabled when Security feature is enabled) + if (!canWriteEndpointExceptions) { + return { + exceptionActionItems: exceptionActionItems.map((item) => { + return { ...item, disabled: item.name === ACTION_ADD_ENDPOINT_EXCEPTION }; + }), + }; + } + return { exceptionActionItems }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx index c4d50062675d2..2ee3cbd169ecf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx @@ -12,7 +12,6 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiSpacer, EuiSuperDatePicker, EuiSuperUpdateButton, @@ -232,36 +231,32 @@ const RulePreviewComponent: React.FC = ({ )} - - - +

{i18n.QUERY_PREVIEW_LABEL}

+ + + + + + - - - - -
+ + {isPreviewRequestInProgress && } {!isPreviewRequestInProgress && previewId && spaceId && ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index de37209fe14c3..31b0529fee4c3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -26,10 +26,8 @@ import type { FormHook } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; import { RuleActionsField } from '../rule_actions_field'; import { useKibana } from '../../../../common/lib/kibana'; -import { - useFetchConnectors, - useFetchConnectorTypes, -} from '../../../../detection_engine/rule_management/api/hooks/use_fetch_connectors'; +import { useFetchConnectorsQuery } from '../../../../detection_engine/rule_management/api/hooks/use_fetch_connectors_query'; +import { useFetchConnectorTypesQuery } from '../../../../detection_engine/rule_management/api/hooks/use_fetch_connector_types_query'; import * as i18n from './translations'; import { RuleSnoozeSection } from './rule_snooze_section'; import { NotificationAction } from './notification_action'; @@ -159,8 +157,8 @@ const StepRuleActionsReadOnlyComponent: FC = ({ const actionTypeRegistry = triggersActionsUi.actionTypeRegistry as ActionTypeRegistryContract; - const { data: connectors } = useFetchConnectors(); - const { data: connectorTypes } = useFetchConnectorTypes(); + const { data: connectors } = useFetchConnectorsQuery(); + const { data: connectorTypes } = useFetchConnectorTypesQuery(); const notificationActions = ruleActionsData.actions; const responseActions = ruleActionsData.responseActions || []; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index e7703c86f23bb..67175f05ece2e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -10,6 +10,7 @@ import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui'; import type { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { TableId } from '@kbn/securitysolution-data-table'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step'; import { AlertsCasesTourSteps, @@ -17,9 +18,8 @@ import { } from '../../../common/components/guided_onboarding_tour/tour_config'; import { isActiveTimeline } from '../../../helpers'; import { useResponderActionItem } from '../endpoint_responder'; -import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TAKE_ACTION } from '../alerts_table/additional_filters_action/translations'; -import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; +import { useAlertExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; import { useInvestigateInTimeline } from '../alerts_table/timeline_actions/use_investigate_in_timeline'; @@ -157,7 +157,7 @@ export const TakeActionDropdown = React.memo( [onAddExceptionTypeClick] ); - const { exceptionActionItems } = useExceptionActions({ + const { exceptionActionItems } = useAlertExceptionActions({ isEndpointAlert: isAlertFromEndpointAlert({ ecsData }), onAddExceptionTypeClick: handleOnAddExceptionTypeClick, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 91430d4818317..20f258679f76f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -109,7 +109,7 @@ export const updateAlertStatusByIds = async ({ signalIds, status, signal, -}: UpdateAlertStatusByIdsProps): Promise => +}: UpdateAlertStatusByIdsProps): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', body: JSON.stringify({ status, signal_ids: signalIds }), diff --git a/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx b/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx index c1fdf61a57cdc..0e457d520984c 100644 --- a/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx @@ -5,41 +5,41 @@ * 2.0. */ -import React, { useMemo, useEffect, useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { EuiSearchBarProps } from '@elastic/eui'; - import { + EuiButton, EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, - EuiPagination, - EuiPopover, - EuiButton, EuiFlexGroup, EuiFlexItem, - EuiSpacer, - EuiPageHeader, EuiHorizontalRule, + EuiPageHeader, + EuiPagination, + EuiPopover, + EuiSpacer, EuiText, } from '@elastic/eui'; -import type { NamespaceType, ExceptionListFilter } from '@kbn/securitysolution-io-ts-list-types'; +import type { ExceptionListFilter, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks'; -import { ViewerStatus, EmptyViewerState } from '@kbn/securitysolution-exception-list-components'; +import { EmptyViewerState, ViewerStatus } from '@kbn/securitysolution-exception-list-components'; +import { useHasSecurityCapability } from '../../../helper_hooks'; import { AutoDownload } from '../../../common/components/auto_download/auto_download'; import { useKibana } from '../../../common/lib/kibana'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import * as i18n from '../../translations/shared_list'; import { - ExceptionsTableUtilityBar, - ListsSearchBar, + CreateSharedListFlyout, ExceptionsListCard, + ExceptionsTableUtilityBar, ImportExceptionListFlyout, - CreateSharedListFlyout, + ListsSearchBar, } from '../../components'; import { useAllExceptionLists } from '../../hooks/use_all_exception_lists'; import { ReferenceErrorModal } from '../../../detections/components/value_lists_management_flyout/reference_error_modal'; @@ -82,9 +82,15 @@ const SORT_FIELDS: Array<{ field: string; label: string; defaultOrder: 'asc' | ' export const SharedLists = React.memo(() => { const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData(); - const { loading: listsConfigLoading } = useListsConfig(); + const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } = + useListsConfig(); const loading = userInfoLoading || listsConfigLoading; + const canShowEndpointExceptions = useHasSecurityCapability('showEndpointExceptions'); + const canAccessEndpointExceptions = useMemo( + () => !listsConfigLoading && !needsListsConfiguration && canShowEndpointExceptions, + [canShowEndpointExceptions, listsConfigLoading, needsListsConfiguration] + ); const { services: { http, @@ -103,6 +109,13 @@ export const SharedLists = React.memo(() => { const [viewerStatus, setViewStatus] = useState(ViewerStatus.LOADING); + const exceptionListTypes = useMemo(() => { + const lists = [ExceptionListTypeEnum.DETECTION]; + if (canAccessEndpointExceptions) { + lists.push(ExceptionListTypeEnum.ENDPOINT); + } + return lists; + }, [canAccessEndpointExceptions]); const [ loadingExceptions, exceptions, @@ -115,7 +128,7 @@ export const SharedLists = React.memo(() => { errorMessage: i18n.ERROR_EXCEPTION_LISTS, filterOptions: { ...filters, - types: [ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT], + types: exceptionListTypes, }, http, namespaceTypes: ['single', 'agnostic'], diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx index dbb39b25c79ef..4099db06d6c5a 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { ReactNode } from 'react'; import React, { type FC, useMemo, useCallback } from 'react'; import { type Criteria, EuiBasicTable, formatDate } from '@elastic/eui'; import { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; @@ -73,6 +74,10 @@ export interface CorrelationsDetailsAlertsTableProps { * Id of the document */ eventId: string; + /** + * No data message to render if the table is empty + */ + noItemsMessage?: ReactNode; /** * Data test subject string for testing */ @@ -88,6 +93,7 @@ export const CorrelationsDetailsAlertsTable: FC { const { @@ -142,18 +148,19 @@ export const CorrelationsDetailsAlertsTable: FC - - {ACTION_INVESTIGATE_IN_TIMELINE} - - - ), + headerContent: + alertIds && alertIds.length && alertIds.length > 0 ? ( +
+ + {ACTION_INVESTIGATE_IN_TIMELINE} + +
+ ) : null, }} content={{ error }} expand={{ @@ -170,6 +177,7 @@ export const CorrelationsDetailsAlertsTable: FC ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.test.tsx index bbec7d1d4d169..825117c01641b 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.test.tsx @@ -254,7 +254,7 @@ describe('', () => { ); expect(getByTestId(HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID).textContent).toContain( - 'No items found' + 'No users identified' ); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx index c2f9d263ff560..5941c39cb8c8d 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx @@ -267,7 +267,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s - + @@ -287,6 +287,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s loading={isRelatedUsersLoading} data-test-subj={HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID} pagination={pagination} + message={i18n.RELATED_USERS_TABLE_NO_DATA} /> {
), diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx index e1512b8b7ada1..5c7a900765fe3 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx @@ -11,11 +11,18 @@ import { LeftPanelContext } from '../context'; import { PrevalenceDetails } from './prevalence_details'; import { PREVALENCE_DETAILS_LOADING_TEST_ID, + PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID, + PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_TEST_ID, + PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, } from './test_ids'; import { usePrevalence } from '../../shared/hooks/use_prevalence'; import { TestProviders } from '../../../common/mock'; +import { licenseService } from '../../../common/hooks/use_license'; jest.mock('../../shared/hooks/use_prevalence'); @@ -27,6 +34,17 @@ jest.mock('react-redux', () => { useDispatch: () => mockDispatch, }; }); +jest.mock('../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); const panelContextValue = { eventId: 'event id', @@ -36,7 +54,13 @@ const panelContextValue = { } as unknown as LeftPanelContext; describe('PrevalenceDetails', () => { - it('should render the table', () => { + const licenseServiceMock = licenseService as jest.Mocked; + + beforeEach(() => { + licenseServiceMock.isPlatinumPlus.mockReturnValue(true); + }); + + it('should render the table with all columns if license is platinum', () => { const field1 = 'field1'; const field2 = 'field2'; (usePrevalence as jest.Mock).mockReturnValue({ @@ -62,7 +86,7 @@ describe('PrevalenceDetails', () => { ], }); - const { getByTestId } = render( + const { getByTestId, getAllByTestId, queryByTestId } = render( @@ -71,6 +95,74 @@ describe('PrevalenceDetails', () => { ); expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument(); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID).length).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID).length).toBeGreaterThan( + 1 + ); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(queryByTestId(`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`)).not.toBeInTheDocument(); + }); + + it('should render the table with only basic columns if license is not platinum', () => { + const field1 = 'field1'; + const field2 = 'field2'; + (usePrevalence as jest.Mock).mockReturnValue({ + loading: false, + error: false, + data: [ + { + field: field1, + value: 'value1', + alertCount: 1, + docCount: 1, + hostPrevalence: 0.05, + userPrevalence: 0.1, + }, + { + field: field2, + value: 'value2', + alertCount: 1, + docCount: 1, + hostPrevalence: 0.5, + userPrevalence: 0.05, + }, + ], + }); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + + const { getByTestId, getAllByTestId } = render( + + + + + + ); + + expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument(); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID).length).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID).length).toBeGreaterThan( + 1 + ); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getByTestId(`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`)).toBeInTheDocument(); }); it('should render loading', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx index a5e7907c13c8c..11f370e9572a6 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx @@ -5,18 +5,25 @@ * 2.0. */ -import React, { useState } from 'react'; +import dateMath from '@elastic/datemath'; +import React, { useMemo, useState } from 'react'; import type { EuiBasicTableColumn, OnTimeChangeProps } from '@elastic/eui'; import { + EuiCallOut, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, + EuiLink, EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiSuperDatePicker, + EuiText, + EuiToolTip, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useLicense } from '../../../common/hooks/use_license'; import { InvestigateInTimelineButton } from '../../../common/components/event_details/table/investigate_in_timeline_button'; import type { PrevalenceData } from '../../shared/hooks/use_prevalence'; import { usePrevalence } from '../../shared/hooks/use_prevalence'; @@ -32,6 +39,10 @@ import { PREVALENCE_TABLE_FIELD_COLUMN_TITLE, USER_TITLE, PREVALENCE_NO_DATA_MESSAGE, + PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE_TOOLTIP, + PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE_TOOLTIP, + HOST_PREVALENCE_COLUMN_TITLE_TOOLTIP, + USER_PREVALENCE_COLUMN_TITLE_TOOLTIP, } from './translations'; import { PREVALENCE_DETAILS_LOADING_TEST_ID, @@ -58,26 +69,43 @@ export const PREVALENCE_TAB_ID = 'prevalence-details'; const DEFAULT_FROM = 'now-30d'; const DEFAULT_TO = 'now'; -const columns: Array> = [ +interface PrevalenceDetailsRow extends PrevalenceData { + /** + * From datetime selected in the date picker to pass to timeline + */ + from: string; + /** + * To datetime selected in the date picker to pass to timeline + */ + to: string; +} + +const columns: Array> = [ { field: 'field', name: PREVALENCE_TABLE_FIELD_COLUMN_TITLE, 'data-test-subj': PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID, + render: (field: string) => {field}, + width: '20%', }, { field: 'value', name: PREVALENCE_TABLE_VALUE_COLUMN_TITLE, 'data-test-subj': PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, + render: (value: string) => {value}, + width: '20%', }, { name: ( - - {PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE} - {PREVALENCE_TABLE_COUNT_COLUMN_TITLE} - + + + {PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE} + {PREVALENCE_TABLE_COUNT_COLUMN_TITLE} + + ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID, - render: (data: PrevalenceData) => { + render: (data: PrevalenceDetailsRow) => { const dataProviders = [ getDataProvider(data.field, `timeline-indicator-${data.field}-${data.value}`, data.value), ]; @@ -86,6 +114,7 @@ const columns: Array> = [ asEmptyButton={true} dataProviders={dataProviders} filters={[]} + timeRange={{ kind: 'absolute', from: data.from, to: data.to }} > <>{data.alertCount} @@ -97,13 +126,15 @@ const columns: Array> = [ }, { name: ( - - {PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE} - {PREVALENCE_TABLE_COUNT_COLUMN_TITLE} - + + + {PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE} + {PREVALENCE_TABLE_COUNT_COLUMN_TITLE} + + ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID, - render: (data: PrevalenceData) => { + render: (data: PrevalenceDetailsRow) => { const dataProviders = [ { ...getDataProvider( @@ -127,6 +158,7 @@ const columns: Array> = [ asEmptyButton={true} dataProviders={dataProviders} filters={[]} + timeRange={{ kind: 'absolute', from: data.from, to: data.to }} keepDataView // changing dataview from only detections to include non-alerts docs > <>{data.docCount} @@ -140,34 +172,32 @@ const columns: Array> = [ { field: 'hostPrevalence', name: ( - - {HOST_TITLE} - {PREVALENCE_TABLE_PREVALENCE_COLUMN_TITLE} - + + + {HOST_TITLE} + {PREVALENCE_TABLE_PREVALENCE_COLUMN_TITLE} + + ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID, render: (hostPrevalence: number) => ( - <> - {Math.round(hostPrevalence * 100)} - {'%'} - + {`${Math.round(hostPrevalence * 100)}%`} ), width: '10%', }, { field: 'userPrevalence', name: ( - - {USER_TITLE} - {PREVALENCE_TABLE_PREVALENCE_COLUMN_TITLE} - + + + {USER_TITLE} + {PREVALENCE_TABLE_PREVALENCE_COLUMN_TITLE} + + ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID, render: (userPrevalence: number) => ( - <> - {Math.round(userPrevalence * 100)} - {'%'} - + {`${Math.round(userPrevalence * 100)}%`} ), width: '10%', }, @@ -180,12 +210,38 @@ export const PrevalenceDetails: React.FC = () => { const { browserFields, dataFormattedForFieldBrowser, eventId, investigationFields } = useLeftPanelContext(); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + + // these two are used by the usePrevalence hook to fetch the data const [start, setStart] = useState(DEFAULT_FROM); const [end, setEnd] = useState(DEFAULT_TO); - const onTimeChange = ({ start: s, end: e }: OnTimeChangeProps) => { + // these two are used to pass to timeline + const [absoluteStart, setAbsoluteStart] = useState( + (dateMath.parse(DEFAULT_FROM) || new Date()).toISOString() + ); + const [absoluteEnd, setAbsoluteEnd] = useState( + (dateMath.parse(DEFAULT_TO) || new Date()).toISOString() + ); + + // TODO update the logic to use a single set of start/end dates + // currently as we're using this InvestigateInTimelineButton component we need to pass the timeRange + // as an AbsoluteTimeRange, which requires from/to values + const onTimeChange = ({ start: s, end: e, isInvalid }: OnTimeChangeProps) => { + if (isInvalid) return; + setStart(s); setEnd(e); + + const from = dateMath.parse(s); + if (from && from.isValid()) { + setAbsoluteStart(from.toISOString()); + } + + const to = dateMath.parse(e); + if (to && to.isValid()) { + setAbsoluteEnd(to.toISOString()); + } }; const { loading, error, data } = usePrevalence({ @@ -197,6 +253,12 @@ export const PrevalenceDetails: React.FC = () => { }, }); + // add timeRange to pass it down to timeline + const items = useMemo( + () => data.map((item) => ({ ...item, from: absoluteStart, to: absoluteEnd })), + [data, absoluteStart, absoluteEnd] + ); + if (loading) { return ( { ); } + const upsell = ( + <> + + + + + ), + }} + /> + + + + ); + return ( <> + {!isPlatinumPlus && upsell} { {data.length > 0 ? ( ) : (
diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_ancestry.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_ancestry.tsx index 517f62ef590ba..ac0ac0a351e9a 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_ancestry.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_ancestry.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { RELATED_ALERTS_BY_ANCESTRY_NO_DATA } from './translations'; import { CorrelationsDetailsAlertsTable } from './correlations_details_alerts_table'; import { CORRELATIONS_ANCESTRY_ALERTS } from '../../shared/translations'; import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_related_alerts_by_ancestry'; @@ -57,6 +58,7 @@ export const RelatedAlertsByAncestry: React.VFC = alertIds={data} scopeId={scopeId} eventId={eventId} + noItemsMessage={RELATED_ALERTS_BY_ANCESTRY_NO_DATA} data-test-subj={CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID} /> ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_same_source_event.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_same_source_event.tsx index eea480301043f..0a0cdeb8fd29c 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_same_source_event.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_same_source_event.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { RELATED_ALERTS_BY_SOURCE_EVENT_NO_DATA } from './translations'; import { CORRELATIONS_SAME_SOURCE_ALERTS } from '../../shared/translations'; import { useFetchRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_fetch_related_alerts_by_same_source_event'; import { CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID } from './test_ids'; @@ -51,6 +52,7 @@ export const RelatedAlertsBySameSourceEvent: React.VFC ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_session.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_session.tsx index 726390529fe63..8a55aa3651708 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_session.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/related_alerts_by_session.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { RELATED_ALERTS_BY_SESSION_NO_DATA } from './translations'; import { CORRELATIONS_SESSION_ALERTS } from '../../shared/translations'; import { CorrelationsDetailsAlertsTable } from './correlations_details_alerts_table'; import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session'; @@ -51,6 +52,7 @@ export const RelatedAlertsBySession: React.VFC = ({ alertIds={data} scopeId={scopeId} eventId={eventId} + noItemsMessage={RELATED_ALERTS_BY_SESSION_NO_DATA} data-test-subj={CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID} /> ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/related_cases.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/related_cases.tsx index 71b6edf46df4f..707087d6360e8 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/related_cases.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/related_cases.tsx @@ -20,6 +20,7 @@ import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { CORRELATIONS_CASE_NAME_COLUMN_TITLE, CORRELATIONS_CASE_STATUS_COLUMN_TITLE, + RELATED_CASES_NO_DATA, } from './translations'; const ICON = 'warning'; @@ -82,6 +83,7 @@ export const RelatedCases: React.VFC = ({ eventId }) => { items={data} columns={columns} pagination={true} + message={RELATED_CASES_NO_DATA} data-test-subj={CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID} /> diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx index e3c7b6b9de3e8..6281a34e0d78f 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n-react'; import { RESPONSE_DETAILS_TEST_ID, RESPONSE_EMPTY_TEST_ID } from './test_ids'; import { expandDottedObject } from '../../../../common/utils/expand_dotted'; import type { @@ -61,7 +62,25 @@ export const ResponseDetails: React.FC = () => { {!responseActions ? ( - {i18n.RESPONSE_EMPTY} + + + + + ), + }} + /> + ) : ( {endpointResponseActionsEnabled ? responseActionsView?.content : osqueryView?.content} diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts index 9e7cf56db7c05..1b37fef66ec71 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const ENTITIES_NO_DATA_MESSAGE = i18n.translate( 'xpack.securitySolution.flyout.entitiesNoDataMessage', { - defaultMessage: 'No user or host data available', + defaultMessage: 'Host and user information are unavailable for this alert.', } ); @@ -39,10 +39,17 @@ export const USER_TITLE = i18n.translate('xpack.securitySolution.flyout.entities defaultMessage: 'User', }); +export const USER_PREVALENCE_COLUMN_TITLE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.flyout.entities.userPrevalenceColumTitleTooltip', + { + defaultMessage: 'Percentage of unique users with identical field value pairs', + } +); + export const USERS_INFO_TITLE = i18n.translate( 'xpack.securitySolution.flyout.entities.usersInfoTitle', { - defaultMessage: 'User info', + defaultMessage: 'User information', } ); @@ -53,13 +60,20 @@ export const RELATED_HOSTS_TITLE = i18n.translate( } ); -export const RELATED_HOSTS_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.flyout.entities.relatedHostsToolTip', +export const RELATED_HOSTS_TABLE_NO_DATA = i18n.translate( + 'xpack.securitySolution.flyout.entities.relatedHostsTableNoData', { - defaultMessage: 'The user successfully authenticated to these hosts after the alert.', + defaultMessage: 'No hosts identified', } ); +export const RELATED_HOSTS_TOOL_TIP = (userName: string) => + i18n.translate('xpack.securitySolution.flyout.entities.relatedHostsToolTip', { + defaultMessage: + 'After this alert was generated, {userName} logged into these hosts. Check if this activity is normal.', + values: { userName }, + }); + export const RELATED_ENTITIES_NAME_COLUMN_TITLE = i18n.translate( 'xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn', { @@ -78,10 +92,17 @@ export const HOST_TITLE = i18n.translate('xpack.securitySolution.flyout.entities defaultMessage: 'Host', }); +export const HOST_PREVALENCE_COLUMN_TITLE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.flyout.entities.hostPrevalenceColumTitleTooltip', + { + defaultMessage: 'Percentage of unique hosts with identical field value pairs', + } +); + export const HOSTS_INFO_TITLE = i18n.translate( 'xpack.securitySolution.flyout.entities.hostsInfoTitle', { - defaultMessage: 'Host info', + defaultMessage: 'Host information', } ); @@ -92,13 +113,20 @@ export const RELATED_USERS_TITLE = i18n.translate( } ); -export const RELATED_USERS_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.flyout.entities.relatedUsersToolTip', +export const RELATED_USERS_TABLE_NO_DATA = i18n.translate( + 'xpack.securitySolution.flyout.entities.relatedUsersTableNoData', { - defaultMessage: 'These users successfully authenticated to the affected host after the alert.', + defaultMessage: 'No users identified', } ); +export const RELATED_USERS_TOOL_TIP = (hostName: string) => + i18n.translate('xpack.securitySolution.flyout.entities.relatedUsersToolTip', { + defaultMessage: + 'After this alert was generated, these users logged into {hostName}. Check if this activity is normal.', + values: { hostName }, + }); + export const PREVALENCE_ERROR_MESSAGE = i18n.translate( 'xpack.securitySolution.flyout.prevalenceErrorMessage', { @@ -134,6 +162,13 @@ export const PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE = i18n.translate( } ); +export const PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitleTooltip', + { + defaultMessage: 'Total number of alerts with identical field value pairs', + } +); + export const PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE = i18n.translate( 'xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle', { @@ -141,6 +176,13 @@ export const PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE = i18n.translate( } ); +export const PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitleTooltip', + { + defaultMessage: 'Total number of event documents with identical field value pairs', + } +); + export const PREVALENCE_TABLE_COUNT_COLUMN_TITLE = i18n.translate( 'xpack.securitySolution.flyout.prevalenceTableCountColumnTitle', { @@ -159,10 +201,6 @@ export const RESPONSE_TITLE = i18n.translate('xpack.securitySolution.flyout.resp defaultMessage: 'Responses', }); -export const RESPONSE_EMPTY = i18n.translate('xpack.securitySolution.flyout.response.empty', { - defaultMessage: 'There are no response actions defined for this event.', -}); - export const CORRELATIONS_TIMESTAMP_COLUMN_TITLE = i18n.translate( 'xpack.securitySolution.flyout.correlations.timestampColumnTitle', { @@ -211,3 +249,31 @@ export const CORRELATIONS_DETAILS_TABLE_FILTER = i18n.translate( defaultMessage: 'Correlations Details Table Alert IDs', } ); + +export const RELATED_ALERTS_BY_ANCESTRY_NO_DATA = i18n.translate( + 'xpack.securitySolution.flyout.correlations.relatedAlertsByAncestryNoData', + { + defaultMessage: 'No alerts related by ancestry', + } +); + +export const RELATED_ALERTS_BY_SOURCE_EVENT_NO_DATA = i18n.translate( + 'xpack.securitySolution.flyout.correlations.relatedAlertsBySourceEventNoData', + { + defaultMessage: 'No related source events', + } +); + +export const RELATED_ALERTS_BY_SESSION_NO_DATA = i18n.translate( + 'xpack.securitySolution.flyout.correlations.relatedAlertsBySessionNoData', + { + defaultMessage: 'No alerts related by session', + } +); + +export const RELATED_CASES_NO_DATA = i18n.translate( + 'xpack.securitySolution.flyout.correlations.relatedCasesNoData', + { + defaultMessage: 'No related cases', + } +); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.test.tsx index 296ae53fc779a..845856ab04f3b 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.test.tsx @@ -233,7 +233,7 @@ describe('', () => { ); expect(getByTestId(USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID).textContent).toContain( - 'No items found' + 'No hosts identified' ); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx index 9437858ca211d..c5a135eb621bb 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx @@ -270,7 +270,7 @@ export const UserDetails: React.FC = ({ userName, timestamp, s - + @@ -290,6 +290,7 @@ export const UserDetails: React.FC = ({ userName, timestamp, s loading={isRelatedHostLoading} data-test-subj={USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID} pagination={pagination} + message={i18n.RELATED_HOSTS_TABLE_NO_DATA} /> ', () => { expect(mockUseAlertPrevalenceFromProcessTree).toHaveBeenCalledWith({ isActiveTimeline: false, documentId: 'ancestors-id', - indices: ['rule-parameters-index'], + indices: ['rule-indices'], }); expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx index 54fa7d6907bdc..3d872640a5a22 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx @@ -10,7 +10,7 @@ import { EuiTreeView } from '@elastic/eui'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; import { getTreeNodes } from '../utils/analyzer_helpers'; import { ANALYZER_PREVIEW_TITLE } from './translations'; -import { ANCESTOR_ID, RULE_PARAMETERS_INDEX } from '../../shared/constants/field_names'; +import { ANCESTOR_ID, RULE_INDICES } from '../../shared/constants/field_names'; import { useRightPanelContext } from '../context'; import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; @@ -38,7 +38,7 @@ export const AnalyzerPreview: React.FC = () => { const processDocumentId = documentId && Array.isArray(documentId.values) ? documentId.values[0] : ''; - const index = find({ category: 'kibana', field: RULE_PARAMETERS_INDEX }, data); + const index = find({ category: 'kibana', field: RULE_INDICES }, data); const indices = index?.values ?? []; const { statsNodes } = useAlertPrevalenceFromProcessTree({ diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview_container.tsx index d4698e962ccb3..632a34e169f71 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview_container.tsx @@ -8,6 +8,8 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { TimelineTabs } from '@kbn/securitysolution-data-table'; +import { EuiLink, EuiMark } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; import { ALERTS_ACTIONS } from '../../../common/lib/apm/user_actions'; @@ -17,7 +19,7 @@ import { useRightPanelContext } from '../context'; import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { AnalyzerPreview } from './analyzer_preview'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; -import { ANALYZER_PREVIEW_ERROR, ANALYZER_PREVIEW_TITLE } from './translations'; +import { ANALYZER_PREVIEW_TITLE } from './translations'; import { ExpandablePanel } from '../../shared/components/expandable_panel'; const timelineId = 'timeline-1'; @@ -65,7 +67,27 @@ export const AnalyzerPreviewContainer: React.FC = () => { {isEnabled ? ( ) : ( -
{ANALYZER_PREVIEW_ERROR}
+
+ {'sysmon'}, + winlogbeat: {'winlogbeat'}, + link: ( + + + + ), + }} + /> +
)} ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx index 4a0405c06b7e1..8a1724dd98d5a 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx @@ -54,11 +54,13 @@ const columns: Array> = [ field: 'field', name: HIGHLIGHTED_FIELDS_FIELD_COLUMN, 'data-test-subj': 'fieldCell', + width: '50%', }, { field: 'description', name: HIGHLIGHTED_FIELDS_VALUE_COLUMN, 'data-test-subj': 'valueCell', + width: '50%', render: (description: { field: string; values: string[] | null | undefined; @@ -111,7 +113,7 @@ export const HighlightedFields: FC = () => { - + diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_row.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_row.tsx index 4e7df1aa6a5c5..f3d8e6d25d9d5 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_row.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_row.tsx @@ -45,7 +45,7 @@ export interface InsightsSummaryRowProps { /** * Panel showing summary information as an icon, a count and text as well as a severity colored dot. - * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results components under the Insights section. + * Should be used for Entities, Threat intelligence, Prevalence, Correlations and Results components under the Insights section. * The colored dot is currently optional but will ultimately be mandatory (waiting on PM and UIUX). */ export const InsightsSummaryRow: VFC = ({ diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/response_button.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/response_button.tsx index 0db15a342e71b..b1e4e94296a46 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/response_button.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/response_button.tsx @@ -42,16 +42,20 @@ export const ResponseButton: React.FC = () => { }); }, [eventId, indexName, openLeftPanel, scopeId]); - if (!responseActions) return
{RESPONSE_EMPTY}
; - return ( - - {RESPONSE_TITLE} - + <> + {!responseActions ? ( +
{RESPONSE_EMPTY}
+ ) : ( + + {RESPONSE_TITLE} + + )} + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/session_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview_container.tsx index af4d192b45bf6..49a90921e2998 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/session_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview_container.tsx @@ -8,6 +8,9 @@ import React, { type FC, useCallback } from 'react'; import { TimelineTabs } from '@kbn/securitysolution-data-table'; import { useDispatch } from 'react-redux'; +import { EuiLink, useEuiTheme } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/css/dist/emotion-css.cjs'; import { useLicense } from '../../../common/hooks/use_license'; import { SessionPreview } from './session_preview'; import { useSessionPreview } from '../hooks/use_session_preview'; @@ -16,11 +19,7 @@ import { useRightPanelContext } from '../context'; import { ALERTS_ACTIONS } from '../../../common/lib/apm/user_actions'; import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { SESSION_PREVIEW_TEST_ID } from './test_ids'; -import { - SESSION_PREVIEW_ERROR, - SESSION_PREVIEW_TITLE, - SESSION_PREVIEW_UPSELL, -} from './translations'; +import { SESSION_PREVIEW_TITLE } from './translations'; import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions'; import { getScopedActions } from '../../../helpers'; @@ -65,10 +64,57 @@ export const SessionPreviewContainer: FC = () => { startTransaction, ]); + const { euiTheme } = useEuiTheme(); + const noSessionMessage = !isEnterprisePlus ? ( -
{SESSION_PREVIEW_UPSELL}
+
+ + + + ), + }} + /> +
) : !sessionViewConfig ? ( -
{SESSION_PREVIEW_ERROR}
+
+ + + + ), + link: ( + + + + ), + }} + /> +
) : null; return ( diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index 9c810f7bfc696..6f0ee15d7db01 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -99,7 +99,7 @@ export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_HOST_OVERVI export const TECHNICAL_PREVIEW_ICON_TEST_ID = 'securitySolutionDocumentDetailsFlyoutTechnicalPreviewIcon'; -/* Insights Threat Intelligence */ +/* Insights Threat intelligence */ export const INSIGHTS_THREAT_INTELLIGENCE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsThreatIntelligence'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx index 53fda9b40c3c4..70d8fce2d5c64 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx @@ -83,7 +83,7 @@ describe('', () => { const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); - expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Threat Intelligence'); + expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Threat intelligence'); expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent('1 threat match detected'); expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent( '1 field enriched with threat intelligence' @@ -99,7 +99,7 @@ describe('', () => { const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); - expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Threat Intelligence'); + expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Threat intelligence'); expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent('2 threat matches detected'); expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent( '2 fields enriched with threat intelligence' diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx index 38ac0a0015eba..6e8c1f3c6dded 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx @@ -25,7 +25,7 @@ import { LeftPanelKey, LeftPanelInsightsTab } from '../../left'; import { THREAT_INTELLIGENCE_TAB_ID } from '../../left/components/threat_intelligence_details'; /** - * Threat Intelligence section under Insights section, overview tab. + * Threat intelligence section under Insights section, overview tab. * The component fetches the necessary data, then pass it down to the InsightsSubSection component for loading and error state, * and the SummaryPanel component for data rendering. */ diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index 4d1390701ad10..b9c6fd2692ac3 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -41,7 +41,7 @@ export const RISK_SCORE_TITLE = i18n.translate( export const RULE_SUMMARY_TEXT = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.ruleSummaryText', { - defaultMessage: 'Rule summary', + defaultMessage: 'Show rule summary', } ); @@ -133,13 +133,13 @@ export const ENTITIES_TITLE = i18n.translate( export const ENTITIES_NO_DATA_MESSAGE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.entitiesNoDataMessage', { - defaultMessage: 'No user or host data available', + defaultMessage: 'Host and user information are unavailable for this alert', } ); export const THREAT_INTELLIGENCE_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.threatIntelligenceTitle', - { defaultMessage: 'Threat Intelligence' } + { defaultMessage: 'Threat intelligence' } ); export const INSIGHTS_TITLE = i18n.translate( @@ -166,7 +166,10 @@ export const PREVALENCE_TITLE = i18n.translate( export const PREVALENCE_NO_DATA = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.prevalenceNoData', - { defaultMessage: 'No field/value pairs are uncommon' } + { + defaultMessage: + 'Over the last 30 days, the highlighted fields for this alert were observed frequently on other host and user events.', + } ); export const THREAT_MATCH_DETECTED = i18n.translate( @@ -214,13 +217,6 @@ export const ANALYZER_PREVIEW_TITLE = i18n.translate( { defaultMessage: 'Analyzer preview' } ); -export const ANALYZER_PREVIEW_ERROR = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.analyzerPreview.error', - { - defaultMessage: 'No analyzer graph data available', - } -); - export const SHARE = i18n.translate('xpack.securitySolution.flyout.documentDetails.share', { defaultMessage: 'Share Alert', }); @@ -242,7 +238,7 @@ export const INVESTIGATION_GUIDE_BUTTON = i18n.translate( export const INVESTIGATION_GUIDE_NO_DATA = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.investigationGuideNoData', { - defaultMessage: 'An investigation guide has not been created for this rule.', + defaultMessage: 'There’s no investigation guide for this rule.', } ); @@ -253,21 +249,6 @@ export const SESSION_PREVIEW_TITLE = i18n.translate( } ); -export const SESSION_PREVIEW_UPSELL = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.sessionPreview.upsell', - { - defaultMessage: - 'Session preview is disabled because your license does not support it. Please upgrade your license.', - } -); - -export const SESSION_PREVIEW_ERROR = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.sessionPreview.error', - { - defaultMessage: 'No session view data available', - } -); - export const SESSION_PREVIEW_PROCESS_TEXT = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.sessionPreview.processText', { @@ -304,7 +285,7 @@ export const RESPONSE_TITLE = i18n.translate( ); export const RESPONSE_EMPTY = i18n.translate('xpack.securitySolution.flyout.response.empty', { - defaultMessage: 'There are no response actions defined for this event.', + defaultMessage: 'This alert did not generate an external notification.', }); export const TECHNICAL_PREVIEW_TITLE = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/flyout/shared/constants/field_names.ts b/x-pack/plugins/security_solution/public/flyout/shared/constants/field_names.ts index e9a896a73dded..b663ea41e2069 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/constants/field_names.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/constants/field_names.ts @@ -7,6 +7,7 @@ export const ANCESTOR_ID = 'kibana.alert.ancestors.id'; export const RULE_PARAMETERS_INDEX = 'kibana.alert.rule.parameters.index'; +export const RULE_INDICES = 'kibana.alert.rule.indices'; export const ORIGINAL_EVENT_ID = 'kibana.alert.original_event.id'; export const ENTRY_LEADER_ENTITY_ID = 'process.entry_leader.entity_id'; export const ENTRY_LEADER_START = 'process.entry_leader.start'; diff --git a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts index 804784728c3f7..3a0f5f824f4b2 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts @@ -11,6 +11,9 @@ import { useQuery } from '@tanstack/react-query'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { createFetchData } from '../utils/fetch_data'; import { useKibana } from '../../../common/lib/kibana'; +import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters'; +import { isActiveTimeline } from '../../../helpers'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; const QUERY_KEY = 'useFetchFieldValuePairWithAggregation'; @@ -99,7 +102,10 @@ export const useFetchPrevalence = ({ }, } = useKibana(); - const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to); + // retrieves detections and non-detections indices (for example, the alert security index from the current space and 'logs-*' indices) + const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(SourcererScopeName.default)); + + const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to, selectedPatterns); const { data, isLoading, isError } = useQuery( [QUERY_KEY, highlightedFieldsFilters, from, to], @@ -120,7 +126,8 @@ export const useFetchPrevalence = ({ const buildSearchRequest = ( highlightedFieldsFilters: Record, from: string, - to: string + to: string, + selectedPatterns: string[] ): IEsSearchRequest => { const query = buildEsQuery( undefined, @@ -146,14 +153,16 @@ const buildSearchRequest = ( ] ); - return buildAggregationSearchRequest(query, highlightedFieldsFilters); + return buildAggregationSearchRequest(query, highlightedFieldsFilters, selectedPatterns); }; const buildAggregationSearchRequest = ( query: QueryDslQueryContainer, - highlightedFieldsFilters: Record + highlightedFieldsFilters: Record, + selectedPatterns: string[] ): IEsSearchRequest => ({ params: { + index: selectedPatterns, body: { query, aggs: { diff --git a/x-pack/plugins/security_solution/public/flyout/shared/mocks/mock_context.ts b/x-pack/plugins/security_solution/public/flyout/shared/mocks/mock_context.ts index 8280fb64df927..46fa9cff49a31 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/mocks/mock_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/mocks/mock_context.ts @@ -81,6 +81,13 @@ export const mockDataFormattedForFieldBrowser = [ originalValue: ['rule-parameters-index'], isObjectArray: false, }, + { + category: 'kibana', + field: 'kibana.alert.rule.indices', + values: ['rule-indices'], + originalValue: ['rule-indices'], + isObjectArray: false, + }, { category: 'process', field: 'process.entity_id', diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_table.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_table.tsx index 2504392d5ef10..4e7e443972abc 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_table.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_table.tsx @@ -7,19 +7,19 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { - EuiI18nNumber, + type CriteriaWithPagination, EuiAvatar, EuiBasicTable, EuiButtonIcon, EuiFacetButton, EuiHorizontalRule, - RIGHT_ALIGNMENT, + EuiI18nNumber, EuiScreenReaderOnly, + EuiSkeletonText, EuiText, EuiToolTip, type HorizontalAlignment, - type CriteriaWithPagination, - EuiSkeletonText, + RIGHT_ALIGNMENT, } from '@elastic/eui'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -30,14 +30,13 @@ import { SecuritySolutionLinkAnchor } from '../../../../common/components/links' import type { ActionListApiResponse } from '../../../../../common/endpoint/types'; import type { EndpointActionListRequestQuery } from '../../../../../common/api/endpoint'; import { FormattedDate } from '../../../../common/components/formatted_date'; -import { TABLE_COLUMN_NAMES, UX_MESSAGES, ARIA_LABELS } from '../translations'; +import { ARIA_LABELS, TABLE_COLUMN_NAMES, UX_MESSAGES } from '../translations'; import { getActionStatus, getUiCommand } from './hooks'; import { getEmptyValue } from '../../../../common/components/empty_value'; import { StatusBadge } from './status_badge'; import { ActionsLogExpandedTray } from './action_log_expanded_tray'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; -import { useActionHistoryUrlParams } from './use_action_history_url_params'; import { useUrlPagination } from '../../../hooks/use_url_pagination'; const emptyValue = getEmptyValue(); @@ -292,22 +291,19 @@ export const ActionsLogTable = memo( }) => { const getTestId = useTestIdGenerator(dataTestSubj); const { pagination: paginationFromUrlParams } = useUrlPagination(); - const { withOutputs: withOutputsFromUrl } = useActionHistoryUrlParams(); const [expandedRowMap, setExpandedRowMap] = useState({}); - const actionIdsWithOpenTrays = useMemo((): string[] => { - // get the list of action ids from URL params on the history page - if (!isFlyout) { - return withOutputsFromUrl ?? []; - } - // get the list of action ids form the query params for flyout view - return queryParams.withOutputs - ? typeof queryParams.withOutputs === 'string' - ? [queryParams.withOutputs] - : queryParams.withOutputs - : []; - }, [isFlyout, queryParams.withOutputs, withOutputsFromUrl]); + const actionIdsWithOpenTrays = useMemo( + (): string[] => + // get the list of action ids from the query params for flyout view + queryParams.withOutputs + ? typeof queryParams.withOutputs === 'string' + ? [queryParams.withOutputs] + : queryParams.withOutputs + : [], + [queryParams.withOutputs] + ); const redoOpenTrays = useCallback(() => { if (actionIdsWithOpenTrays.length && items.length) { diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx index 476a86a6561dd..1e835e34ac7ef 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx @@ -7,12 +7,13 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import type { IHttpFetchError } from '@kbn/core-http-browser'; import { - createAppRootMockRenderer, type AppContextTestRender, + createAppRootMockRenderer, } from '../../../../common/mock/endpoint'; import { ResponseActionsLog } from '../response_actions_log'; import type { @@ -29,7 +30,6 @@ import { v4 as uuidv4 } from 'uuid'; import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '../../../../../common/endpoint/service/response_actions/constants'; import { useUserPrivileges as _useUserPrivileges } from '../../../../common/components/user_privileges'; import { responseActionsHttpMocks } from '../../../mocks/response_actions_http_mocks'; -import { waitFor } from '@testing-library/react'; import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz/mocks'; import { useGetEndpointActionList as _useGetEndpointActionList } from '../../../hooks/response_actions/use_get_endpoint_action_list'; import { OUTPUT_MESSAGES } from '../translations'; @@ -470,7 +470,7 @@ describe('Response actions history', () => { ); }); - it('should expand each row to show details', async () => { + it('should expand/collapse each row to show/hide details', async () => { render(); const { getAllByTestId, queryAllByTestId } = renderResult; @@ -961,8 +961,7 @@ describe('Response actions history', () => { const expandButtons = getAllByTestId(`${testPrefix}-expand-button`); expandButtons.map((button) => userEvent.click(button)); - const outputs = getAllByTestId(`${testPrefix}-details-tray-output`); - return outputs; + return getAllByTestId(`${testPrefix}-details-tray-output`); }; it.each(RESPONSE_ACTION_API_COMMANDS_NAMES)( diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/reponse_actions_history.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/reponse_actions_history.cy.ts index cb6c7c3a96eee..5c4bb0f3efb8c 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/reponse_actions_history.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/reponse_actions_history.cy.ts @@ -50,4 +50,25 @@ describe('Response actions history page', () => { cy.getByTestSubj('pagination-button-0').click(); cy.getByTestSubj('response-actions-list-details-tray').should('exist'); }); + + it('collapses expanded tray with a single click', () => { + loadPage(`/app/security/administration/response_actions_history`); + // 2nd row on 1st page + const row = cy.getByTestSubj('response-actions-list-expand-button').eq(1); + + // expand the row + row.click(); + cy.getByTestSubj('response-actions-list-details-tray').should('exist'); + cy.url().should('include', 'withOutputs'); + + // collapse the row + cy.intercept('GET', '/api/endpoint/action*').as('getResponses'); + row.click(); + // wait for the API response to come back + // and then see if the tray is actually closed + cy.wait('@getResponses', { timeout: 500 }).then(() => { + cy.getByTestSubj('response-actions-list-details-tray').should('not.exist'); + cy.url().should('not.include', 'withOutputs'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/links.test.ts b/x-pack/plugins/security_solution/public/management/links.test.ts index 17116530e6467..28d3c727ce2a4 100644 --- a/x-pack/plugins/security_solution/public/management/links.test.ts +++ b/x-pack/plugins/security_solution/public/management/links.test.ts @@ -93,7 +93,8 @@ describe('links', () => { SecurityPageName.hostIsolationExceptions, SecurityPageName.policies, SecurityPageName.responseActionsHistory, - SecurityPageName.trustedApps + SecurityPageName.trustedApps, + SecurityPageName.cloudDefendPolicies ) ); }); @@ -234,7 +235,9 @@ describe('links', () => { const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins()); - expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.policies)); + expect(filteredLinks).toEqual( + getLinksWithout(SecurityPageName.policies, SecurityPageName.cloudDefendPolicies) + ); }); }); diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 309e9a093979b..60c4c93d43fa6 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -235,6 +235,7 @@ export const getManagementFilteredLinks = async ( if (!canReadPolicyManagement) { linksToExclude.push(SecurityPageName.policies); + linksToExclude.push(SecurityPageName.cloudDefendPolicies); } if (!canReadActionsLogManagement) { diff --git a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx index ebde74b17be76..ea720f79a66c6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx @@ -236,7 +236,7 @@ describe('Response actions history page', () => { return { id: `agent-id-${i}`, name: `Host-name-${i}`, - selected: [0, 1, 3, 5].includes(i) ? true : false, + selected: [0, 1, 3, 5].includes(i), }; }), page: 0, diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts index d7c8b719a2acd..4439ac771fda3 100644 --- a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts @@ -69,6 +69,7 @@ describe('Exceptions List Api Client', () => { expect(fakeHttpServices.post).toHaveBeenCalledWith( INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL, { + version: '1', body: JSON.stringify(getFakeListDefinition()), } ); diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts index a5ea0c0c4802a..4576fa74e5386 100644 --- a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts @@ -62,6 +62,7 @@ export class ExceptionsListApiClient { const asyncFunction = async () => { try { await this.http.post(INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL, { + version: '1', body: JSON.stringify({ ...this.listDefinition, list_id: this.listId }), }); diff --git a/x-pack/plugins/security_solution/public/mocks.ts b/x-pack/plugins/security_solution/public/mocks.ts index b48da210d6756..9e0757047879a 100644 --- a/x-pack/plugins/security_solution/public/mocks.ts +++ b/x-pack/plugins/security_solution/public/mocks.ts @@ -28,6 +28,7 @@ const startMock = (): PluginStart => ({ ), setExtraRoutes: jest.fn(), getUpselling: () => upselling, + setDashboardsLandingCallout: jest.fn(), }); export const securitySolutionMock = { diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index 360cfbbce9825..360c95ec94019 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -60,6 +60,7 @@ interface Props extends Pick void; hideQueryToggle?: boolean; + applyGlobalQueriesAndFilters?: boolean; } const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ @@ -95,6 +96,7 @@ const EventsByDatasetComponent: React.FC = ({ to, toggleTopN, hideQueryToggle = false, + applyGlobalQueriesAndFilters = true, }) => { const uniqueQueryId = useMemo(() => `${ID}-${queryType}`, [queryType]); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index c482c7cfa1616..a384999c32b54 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -194,7 +194,7 @@ export class Plugin implements IPlugin SecuritySolutionTemplateWrapper, }, - savedObjectsManagement: startPluginsDeps.savedObjectsManagement, + contentManagement: startPluginsDeps.contentManagement, telemetry: this.telemetry.start(), customDataService, topValuesPopover: new TopValuesPopoverService(), diff --git a/x-pack/plugins/security_solution/public/plugin_contract.ts b/x-pack/plugins/security_solution/public/plugin_contract.ts index a96572e006f89..d21eabe738f4f 100644 --- a/x-pack/plugins/security_solution/public/plugin_contract.ts +++ b/x-pack/plugins/security_solution/public/plugin_contract.ts @@ -17,6 +17,7 @@ export class PluginContract { public isILMAvailable$: BehaviorSubject; public isSidebarEnabled$: BehaviorSubject; public getStartedComponent$: BehaviorSubject; + public dashboardsLandingCallout$: BehaviorSubject; public upsellingService: UpsellingService; public extraRoutes$: BehaviorSubject; public appLinksSwitcher: AppLinksSwitcher; @@ -26,6 +27,8 @@ export class PluginContract { this.isILMAvailable$ = new BehaviorSubject(true); this.isSidebarEnabled$ = new BehaviorSubject(true); this.getStartedComponent$ = new BehaviorSubject(null); + this.dashboardsLandingCallout$ = new BehaviorSubject(null); + this.upsellingService = new UpsellingService(); this.appLinksSwitcher = (appLinks) => appLinks; } @@ -36,6 +39,7 @@ export class PluginContract { isILMAvailable$: this.isILMAvailable$.asObservable(), isSidebarEnabled$: this.isSidebarEnabled$.asObservable(), getStartedComponent$: this.getStartedComponent$.asObservable(), + dashboardsLandingCalloutComponent$: this.dashboardsLandingCallout$.asObservable(), upselling: this.upsellingService, }; } @@ -59,6 +63,9 @@ export class PluginContract { setGetStartedPage: (getStartedComponent) => { this.getStartedComponent$.next(getStartedComponent); }, + setDashboardsLandingCallout: (dashboardsLandingCallout) => { + this.dashboardsLandingCallout$.next(dashboardsLandingCallout); + }, getBreadcrumbsNav$: () => breadcrumbsNav$, getUpselling: () => this.upsellingService, }; diff --git a/x-pack/plugins/security_solution/public/rules/links.ts b/x-pack/plugins/security_solution/public/rules/links.ts index c65b7ec3517de..23334456ac04f 100644 --- a/x-pack/plugins/security_solution/public/rules/links.ts +++ b/x-pack/plugins/security_solution/public/rules/links.ts @@ -94,7 +94,7 @@ export const links: LinkItem = { description: i18n.translate( 'xpack.securitySolution.appLinks.coverageOverviewDashboardDescription', { - defaultMessage: 'Review and maintain your protections MITRE ATT&CK® coverage', + defaultMessage: 'Review and maintain your protections MITRE ATT&CK® coverage.', } ), path: COVERAGE_OVERVIEW_PATH, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx index ab24a16ca1a93..91a2904287ffc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx @@ -15,8 +15,8 @@ import { mockAlertDetailsData } from '../../../../../common/components/event_det import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; import { KibanaServices, - useKibana, useGetUserCasesPermissions, + useKibana, } from '../../../../../common/lib/kibana'; import { coreMock } from '@kbn/core/public/mocks'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; @@ -132,6 +132,15 @@ describe('event details footer component', () => { query: jest.fn(), }, cases: mockCasesContract(), + application: { + ...coreStartMock.application, + capabilities: { + ...coreStartMock.application.capabilities, + siem: { + crudEndpointExceptions: true, + }, + }, + }, }, }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index e399a21575667..1911b45fe475a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -12,7 +12,7 @@ import { TestProviders } from '../../../../../common/mock'; import { EventColumnView } from './event_column_view'; import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; -import { TimelineTabs, TimelineId } from '../../../../../../common/types/timeline'; +import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import { TimelineType } from '../../../../../../common/api/timeline'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; @@ -47,6 +47,7 @@ jest.mock('../../../../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../../../../common/lib/kibana'); return { + ...originalModule, useKibana: () => ({ services: { timelines: { ...mockTimelines }, @@ -71,7 +72,6 @@ jest.mock('../../../../../common/lib/kibana', () => { addWarning: jest.fn(), remove: jest.fn(), }), - useGetUserCasesPermissions: originalModule.useGetUserCasesPermissions, }; }); diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index b1fc18359f4f6..6b16958f65808 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -48,7 +48,7 @@ import type { ThreatIntelligencePluginStart } from '@kbn/threat-intelligence-plu import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common'; import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public'; import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; -import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { RouteProps } from 'react-router-dom'; import type { DiscoverStart } from '@kbn/discover-plugin/public'; @@ -133,7 +133,7 @@ export interface StartPlugins { } export interface StartPluginsDependencies extends StartPlugins { - savedObjectsManagement: SavedObjectsManagementPluginStart; + contentManagement: ContentManagementPublicStart; savedObjectsTaggingOss: SavedObjectTaggingOssPluginStart; } @@ -142,6 +142,7 @@ export interface ContractStartServices { isILMAvailable$: Observable; isSidebarEnabled$: Observable; getStartedComponent$: Observable; + dashboardsLandingCalloutComponent$: Observable; upselling: UpsellingService; } @@ -161,7 +162,7 @@ export type StartServices = CoreStart & securityLayout: { getPluginWrapper: () => typeof SecuritySolutionTemplateWrapper; }; - savedObjectsManagement: SavedObjectsManagementPluginStart; + contentManagement: ContentManagementPublicStart; telemetry: TelemetryClientStart; customDataService: DataPublicPluginStart; topValuesPopover: TopValuesPopoverService; @@ -178,6 +179,7 @@ export interface PluginStart { setIsILMAvailable: (isILMAvailable: boolean) => void; setIsSidebarEnabled: (isSidebarEnabled: boolean) => void; setGetStartedPage: (getStartedComponent: React.ComponentType) => void; + setDashboardsLandingCallout: (dashboardsLandingCallout: React.ComponentType) => void; getBreadcrumbsNav$: () => Observable; getUpselling: () => UpsellingService; } diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/get_ftr_config.ts b/x-pack/plugins/security_solution/scripts/run_cypress/get_ftr_config.ts new file mode 100644 index 0000000000000..11f39f6b4a27d --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/run_cypress/get_ftr_config.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import _ from 'lodash'; + +import { EsVersion, readConfigFile } from '@kbn/test'; +import type { ToolingLog } from '@kbn/tooling-log'; +import { getLocalhostRealIp } from '../endpoint/common/localhost_services'; +import type { parseTestFileConfig } from './utils'; + +export const getFTRConfig = ({ + log, + esPort, + kibanaPort, + fleetServerPort, + ftrConfigFilePath, + specFilePath, + specFileFTRConfig, + isOpen, +}: { + log: ToolingLog; + esPort: number; + kibanaPort: number; + fleetServerPort: number; + ftrConfigFilePath: string; + specFilePath: string; + specFileFTRConfig: ReturnType; + isOpen: boolean; +}) => + readConfigFile( + log, + EsVersion.getDefault(), + ftrConfigFilePath, + { + servers: { + elasticsearch: { + port: esPort, + }, + kibana: { + port: kibanaPort, + }, + fleetserver: { + port: fleetServerPort, + }, + }, + // CAUTION: Do not override here kbnTestServer.serverArgs + // or important configs like ssl key and certificate will be lost. + // Please do it in the section bellow on extendedSettings + // + // kbnTestServer: { + // serverArgs: [ + // ... + // ], + // }, + }, + (vars) => { + const hostRealIp = getLocalhostRealIp(); + + const hasFleetServerArgs = _.some( + vars.kbnTestServer.serverArgs, + (value) => + value.includes('--xpack.fleet.agents.fleet_server.hosts') || + value.includes('--xpack.fleet.agents.elasticsearch.host') + ); + + vars.kbnTestServer.serverArgs = _.filter( + vars.kbnTestServer.serverArgs, + (value) => + !( + value.includes('--elasticsearch.hosts') || + value.includes('--xpack.fleet.agents.fleet_server.hosts') || + value.includes('--xpack.fleet.agents.elasticsearch.host') || + value.includes('--server.port') + ) + ); + + // NOTE: extending server args here as settingOverrides above is removing some important SSL configs + // like key and certificate + vars.kbnTestServer.serverArgs.push( + `--server.port=${kibanaPort}`, + `--elasticsearch.hosts=http://localhost:${esPort}` + ); + + // apply right protocol on hosts + vars.kbnTestServer.serverArgs = _.map(vars.kbnTestServer.serverArgs, (value) => { + if ( + vars.servers.elasticsearch.protocol === 'https' && + value.includes('--elasticsearch.hosts=http') + ) { + return value.replace('http', 'https'); + } + + if ( + vars.servers.kibana.protocol === 'https' && + (value.includes('--elasticsearch.hosts=http') || + value.includes('--server.publicBaseUrl=http')) + ) { + return value.replace('http', 'https'); + } + + return value; + }); + + if ( + specFileFTRConfig?.enableExperimental?.length && + _.some(vars.kbnTestServer.serverArgs, (value) => + value.includes('--xpack.securitySolution.enableExperimental') + ) + ) { + vars.kbnTestServer.serverArgs = _.filter( + vars.kbnTestServer.serverArgs, + (value) => !value.includes('--xpack.securitySolution.enableExperimental') + ); + vars.kbnTestServer.serverArgs.push( + `--xpack.securitySolution.enableExperimental=${JSON.stringify( + specFileFTRConfig?.enableExperimental + )}` + ); + } + + if (specFileFTRConfig?.license) { + if (vars.serverless) { + log.warning( + `'ftrConfig.license' ignored. Value does not apply to kibana when running in serverless.\nFile: ${specFilePath}` + ); + } else { + vars.esTestCluster.license = specFileFTRConfig.license; + } + } + + if (hasFleetServerArgs) { + vars.kbnTestServer.serverArgs.push( + `--xpack.fleet.agents.fleet_server.hosts=["https://${hostRealIp}:${fleetServerPort}"]` + ); + vars.kbnTestServer.serverArgs.push( + `--xpack.fleet.agents.elasticsearch.host=http://${hostRealIp}:${esPort}` + ); + + if (vars.serverless) { + vars.kbnTestServer.serverArgs.push(`--xpack.fleet.internal.fleetServerStandalone=false`); + } + } + + // Serverless Specific + if (vars.serverless) { + log.info(`Serverless mode detected`); + + vars.kbnTestServer.serverArgs.push( + `--elasticsearch.hosts=https://localhost:${esPort}`, + `--server.publicBaseUrl=https://localhost:${kibanaPort}` + ); + vars.esTestCluster.serverArgs.push( + `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.entity_id=http://host.docker.internal:${kibanaPort}`, + `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.logout=http://host.docker.internal:${kibanaPort}/logout`, + `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.acs=http://host.docker.internal:${kibanaPort}/api/security/saml/callback` + ); + } else { + vars.kbnTestServer.serverArgs.push( + `--elasticsearch.hosts=http://localhost:${esPort}`, + `--server.publicBaseUrl=http://localhost:${kibanaPort}` + ); + } + + if (specFileFTRConfig?.productTypes) { + if (vars.serverless) { + vars.kbnTestServer.serverArgs.push( + `--xpack.securitySolutionServerless.productTypes=${JSON.stringify([ + ...specFileFTRConfig.productTypes, + // Why spread it twice? + // The `serverless.security.yml` file by default includes two product types as of this change. + // Because it's an array, we need to ensure that existing values are "removed" and the ones + // defined here are added. To do that, we duplicate the `productTypes` passed so that all array + // elements in that YAML file are updated. The Security serverless plugin has code in place to + // dedupe. + ...specFileFTRConfig.productTypes, + ])}` + ); + } else { + log.warning( + `'ftrConfig.productTypes' ignored. Value applies only when running kibana is serverless.\nFile: ${specFilePath}` + ); + } + } + + return vars; + } + ); 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 f029fc79ca0e8..45b48a5d428e3 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts @@ -14,17 +14,10 @@ import { ToolingLog } from '@kbn/tooling-log'; import { withProcRunner } from '@kbn/dev-proc-runner'; import cypress from 'cypress'; import { findChangedFiles } from 'find-cypress-specs'; -import minimatch from 'minimatch'; import path from 'path'; import grep from '@cypress/grep/src/plugin'; -import { - EsVersion, - FunctionalTestRunner, - readConfigFile, - runElasticsearch, - runKibanaServer, -} from '@kbn/test'; +import { EsVersion, FunctionalTestRunner, runElasticsearch, runKibanaServer } from '@kbn/test'; import { Lifecycle, @@ -35,8 +28,8 @@ import { import { createFailError } from '@kbn/dev-cli-errors'; import pRetry from 'p-retry'; import { renderSummaryTable } from './print_run'; -import { getLocalhostRealIp } from '../endpoint/common/localhost_services'; import { isSkipped, parseTestFileConfig } from './utils'; +import { getFTRConfig } from './get_ftr_config'; /** * Retrieve test files using a glob pattern. @@ -70,57 +63,58 @@ const retrieveIntegrations = (integrationsPaths: string[]) => { export const cli = () => { run( async () => { - const { argv } = yargs(process.argv.slice(2)).coerce('env', (arg: string) => - arg.split(',').reduce((acc, curr) => { - const [key, value] = curr.split('='); - if (key === 'burn') { - acc[key] = parseInt(value, 10); - } else { - acc[key] = value; - } - return acc; - }, {} as Record) - ); + const { argv } = yargs(process.argv.slice(2)) + .coerce('spec', (arg) => (_.isArray(arg) ? [_.last(arg)] : [arg])) + .coerce('env', (arg: string) => + arg.split(',').reduce((acc, curr) => { + const [key, value] = curr.split('='); + if (key === 'burn') { + acc[key] = parseInt(value, 10); + } else { + acc[key] = value; + } + return acc; + }, {} as Record) + ); const isOpen = argv._[0] === 'open'; const cypressConfigFilePath = require.resolve( `../../${_.isArray(argv.configFile) ? _.last(argv.configFile) : argv.configFile}` ) as string; const cypressConfigFile = await import(cypressConfigFilePath); - const spec: string | undefined = argv?.spec as string; const grepSpecPattern = grep({ ...cypressConfigFile, - specPattern: spec ?? cypressConfigFile.e2e.specPattern, + specPattern: argv.spec ?? cypressConfigFile.e2e.specPattern, excludeSpecPattern: [], }).specPattern; let files = retrieveIntegrations( _.isArray(grepSpecPattern) ? grepSpecPattern - : globby.sync(spec ? [spec] : cypressConfigFile.e2e.specPattern) + : globby.sync(argv.spec ?? cypressConfigFile.e2e.specPattern) ); if (argv.changedSpecsOnly) { - const basePath = process.cwd().split('kibana/')[1]; - files = findChangedFiles('main', false) - .filter( - minimatch.filter(path.join(basePath, cypressConfigFile?.e2e?.specPattern), { - matchBase: true, - }) - ) - .map((filePath: string) => filePath.replace(basePath, '.')); - - if (!files?.length) { - // eslint-disable-next-line no-process-exit - return process.exit(0); - } + files = (findChangedFiles('main', false) as string[]).reduce((acc, itemPath) => { + const existing = files.find((grepFilePath) => grepFilePath.includes(itemPath)); + if (existing) { + acc.push(existing); + } + return acc; + }, [] as string[]); // to avoid running too many tests, we limit the number of files to 3 // we may extend this in the future files = files.slice(0, 3); } + const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); + if (!files?.length) { + log.info('No tests found'); // eslint-disable-next-line no-process-exit return process.exit(0); } @@ -178,13 +172,6 @@ export const cli = () => { _.pull(fleetServerPorts, fleetServerPort); }; - const log = new ToolingLog({ - level: 'info', - writeTo: process.stdout, - }); - - const hostRealIp = getLocalhostRealIp(); - await pMap( files, async (filePath) => { @@ -203,120 +190,21 @@ export const cli = () => { const esPort: number = getEsPort(); const kibanaPort: number = getKibanaPort(); const fleetServerPort: number = getFleetServerPort(); - const configFromTestFile = parseTestFileConfig(filePath); + const specFileFTRConfig = parseTestFileConfig(filePath); + const ftrConfigFilePath = path.resolve( + _.isArray(argv.ftrConfigFile) ? _.last(argv.ftrConfigFile) : argv.ftrConfigFile + ); - const config = await readConfigFile( + const config = await getFTRConfig({ log, - EsVersion.getDefault(), - path.resolve( - _.isArray(argv.ftrConfigFile) ? _.last(argv.ftrConfigFile) : argv.ftrConfigFile - ), - { - servers: { - elasticsearch: { - port: esPort, - }, - kibana: { - port: kibanaPort, - }, - fleetserver: { - port: fleetServerPort, - }, - }, - kbnTestServer: { - serverArgs: [ - `--server.port=${kibanaPort}`, - `--elasticsearch.hosts=http://localhost:${esPort}`, - ], - }, - }, - (vars) => { - const hasFleetServerArgs = _.some( - vars.kbnTestServer.serverArgs, - (value) => - value.includes('--xpack.fleet.agents.fleet_server.hosts') || - value.includes('--xpack.fleet.agents.elasticsearch.host') - ); - - vars.kbnTestServer.serverArgs = _.filter( - vars.kbnTestServer.serverArgs, - (value) => - !( - value.includes('--elasticsearch.hosts=http://localhost:9220') || - value.includes('--xpack.fleet.agents.fleet_server.hosts') || - value.includes('--xpack.fleet.agents.elasticsearch.host') - ) - ); - - if ( - configFromTestFile?.enableExperimental?.length && - _.some(vars.kbnTestServer.serverArgs, (value) => - value.includes('--xpack.securitySolution.enableExperimental') - ) - ) { - vars.kbnTestServer.serverArgs = _.filter( - vars.kbnTestServer.serverArgs, - (value) => !value.includes('--xpack.securitySolution.enableExperimental') - ); - vars.kbnTestServer.serverArgs.push( - `--xpack.securitySolution.enableExperimental=${JSON.stringify( - configFromTestFile?.enableExperimental - )}` - ); - } - - if (configFromTestFile?.license) { - if (vars.serverless) { - log.warning( - `'ftrConfig.license' ignored. Value does not apply to kibana when running in serverless.\nFile: ${filePath}` - ); - } else { - vars.esTestCluster.license = configFromTestFile.license; - } - } - - if (hasFleetServerArgs) { - vars.kbnTestServer.serverArgs.push( - `--xpack.fleet.agents.fleet_server.hosts=["https://${hostRealIp}:${fleetServerPort}"]` - ); - vars.kbnTestServer.serverArgs.push( - `--xpack.fleet.agents.elasticsearch.host=http://${hostRealIp}:${esPort}` - ); - - if (vars.serverless) { - vars.kbnTestServer.serverArgs.push( - `--xpack.fleet.internal.fleetServerStandalone=false` - ); - } - } - - // Serverless Specific - if (vars.serverless) { - log.info(`Serverless mode detected`); - - if (configFromTestFile?.productTypes) { - vars.kbnTestServer.serverArgs.push( - `--xpack.securitySolutionServerless.productTypes=${JSON.stringify([ - ...configFromTestFile.productTypes, - // Why spread it twice? - // The `serverless.security.yml` file by default includes two product types as of this change. - // Because it's an array, we need to ensure that existing values are "removed" and the ones - // defined here are added. To do that, we duplicate the `productTypes` passed so that all array - // elements in that YAML file are updated. The Security serverless plugin has code in place to - // dedupe. - ...configFromTestFile.productTypes, - ])}` - ); - } - } else if (configFromTestFile?.productTypes) { - log.warning( - `'ftrConfig.productTypes' ignored. Value applies only when running kibana is serverless.\nFile: ${filePath}` - ); - } - - return vars; - } - ); + esPort, + kibanaPort, + fleetServerPort, + ftrConfigFilePath, + specFilePath: filePath, + specFileFTRConfig, + isOpen, + }); log.info(` ---------------------------------------------- @@ -350,26 +238,22 @@ ${JSON.stringify(config.getAll(), null, 2)} config, log, name: `ftr-${esPort}`, - esFrom: 'snapshot', + esFrom: config.get('esTestCluster')?.from || 'snapshot', onEarlyExit, }), { retries: 2, forever: false } ); - await pRetry( - async () => - runKibanaServer({ - procs, - config, - installDir: options?.installDir, - extraKbnOpts: - options?.installDir || options?.ci || !isOpen - ? [] - : ['--dev', '--no-dev-config', '--no-dev-credentials'], - onEarlyExit, - }), - { retries: 2, forever: false } - ); + await runKibanaServer({ + procs, + config, + installDir: options?.installDir, + extraKbnOpts: + options?.installDir || options?.ci || !isOpen + ? [] + : ['--dev', '--no-dev-config', '--no-dev-credentials'], + onEarlyExit, + }); await providers.loadAll(); @@ -438,6 +322,8 @@ ${JSON.stringify(config.getAll(), null, 2)} KIBANA_USERNAME: config.get('servers.kibana.username'), KIBANA_PASSWORD: config.get('servers.kibana.password'), + IS_SERVERLESS: config.get('serverless'), + ...argv.env, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 058f8892013a0..d0d55753ba336 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -17,7 +17,6 @@ import type { import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types'; -import type { AppFeatures } from '../lib/app_features'; import { getPackagePolicyCreateCallback, getPackagePolicyUpdateCallback, @@ -43,9 +42,11 @@ import { calculateEndpointAuthz } from '../../common/endpoint/service/authz'; import type { FeatureUsageService } from './services/feature_usage/service'; import type { ExperimentalFeatures } from '../../common/experimental_features'; import type { ActionCreateService } from './services/actions/create/types'; +import type { AppFeaturesService } from '../lib/app_features_service/app_features_service'; export interface EndpointAppContextServiceSetupContract { securitySolutionRequestContextFactory: IRequestContextFactory; + cloud: CloudSetup; } export interface EndpointAppContextServiceStartContract { @@ -68,9 +69,8 @@ export interface EndpointAppContextServiceStartContract { experimentalFeatures: ExperimentalFeatures; messageSigningService: MessageSigningServiceInterface | undefined; actionCreateService: ActionCreateService | undefined; - cloud: CloudSetup; esClient: ElasticsearchClient; - appFeatures: AppFeatures; + appFeaturesService: AppFeaturesService; } /** @@ -102,13 +102,12 @@ export class EndpointAppContextService { logger, manifestManager, alerting, - cloud, licenseService, exceptionListsClient, featureUsageService, endpointMetadataService, esClient, - appFeatures, + appFeaturesService, } = dependencies; registerIngestCallback( @@ -120,8 +119,8 @@ export class EndpointAppContextService { alerting, licenseService, exceptionListsClient, - cloud, - appFeatures + this.setupDependencies.cloud, + appFeaturesService ) ); @@ -137,9 +136,9 @@ export class EndpointAppContextService { licenseService, featureUsageService, endpointMetadataService, - cloud, + this.setupDependencies.cloud, esClient, - appFeatures + appFeaturesService ) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts index 1d39b72670b98..519d864b726e7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts @@ -9,29 +9,32 @@ import { createMockEndpointAppContextServiceStartContract } from '../mocks'; import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; -import type { AppFeatures } from '../../lib/app_features'; -import { createAppFeaturesMock } from '../../lib/app_features/mocks'; -import { ALL_APP_FEATURE_KEYS } from '../../../common'; + +import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; import { turnOffPolicyProtectionsIfNotSupported } from './turn_off_policy_protections'; import { FleetPackagePolicyGenerator } from '../../../common/endpoint/data_generators/fleet_package_policy_generator'; import type { PolicyData } from '../../../common/endpoint/types'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { PromiseResolvedValue } from '../../../common/endpoint/types/utility_types'; import { ensureOnlyEventCollectionIsAllowed } from '../../../common/endpoint/models/policy_config_helpers'; +import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service'; +import { createAppFeaturesServiceMock } from '../../lib/app_features_service/mocks'; describe('Turn Off Policy Protections Migration', () => { let esClient: ElasticsearchClient; let fleetServices: EndpointInternalFleetServicesInterface; - let appFeatures: AppFeatures; + let appFeatureService: AppFeaturesService; let logger: Logger; const callTurnOffPolicyProtections = () => - turnOffPolicyProtectionsIfNotSupported(esClient, fleetServices, appFeatures, logger); + turnOffPolicyProtectionsIfNotSupported(esClient, fleetServices, appFeatureService, logger); beforeEach(() => { const endpointContextStartContract = createMockEndpointAppContextServiceStartContract(); - ({ esClient, appFeatures, logger } = endpointContextStartContract); + ({ esClient, logger } = endpointContextStartContract); + + appFeatureService = endpointContextStartContract.appFeaturesService; fleetServices = endpointContextStartContract.endpointFleetServicesFactory.asInternalUser(); }); @@ -70,7 +73,7 @@ describe('Turn Off Policy Protections Migration', () => { policyGenerator = new FleetPackagePolicyGenerator('seed'); const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock; - appFeatures = createAppFeaturesMock( + appFeatureService = createAppFeaturesServiceMock( ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') ); diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts index c4a63b8ec841c..13bdb1496da48 100644 --- a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts @@ -8,20 +8,20 @@ import type { Logger, ElasticsearchClient } from '@kbn/core/server'; import type { UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys'; import { isPolicySetToEventCollectionOnly, ensureOnlyEventCollectionIsAllowed, } from '../../../common/endpoint/models/policy_config_helpers'; import type { PolicyData } from '../../../common/endpoint/types'; -import { AppFeatureSecurityKey } from '../../../common/types/app_features'; import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; -import type { AppFeatures } from '../../lib/app_features'; import { getPolicyDataForUpdate } from '../../../common/endpoint/service/policy'; +import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service'; export const turnOffPolicyProtectionsIfNotSupported = async ( esClient: ElasticsearchClient, fleetServices: EndpointInternalFleetServicesInterface, - appFeaturesService: AppFeatures, + appFeaturesService: AppFeaturesService, logger: Logger ): Promise => { const log = logger.get('endpoint', 'policyProtections'); diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 5a3c9ee2297ac..0cd5c02a770ec 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -17,12 +17,12 @@ import { savedObjectsServiceMock, } from '@kbn/core/server/mocks'; import type { + IRouter, KibanaRequest, - RouteConfig, - SavedObjectsClientContract, RequestHandler, - IRouter, + RouteConfig, RouteMethod, + SavedObjectsClientContract, } from '@kbn/core/server'; import { listMock } from '@kbn/lists-plugin/server/mocks'; import { securityMock } from '@kbn/security-plugin/server/mocks'; @@ -30,14 +30,14 @@ import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; import type { FleetStartContract } from '@kbn/fleet-plugin/server'; import { - createPackagePolicyServiceMock, + createFleetActionsClientMock, + createFleetFromHostFilesClientMock, + createFleetToHostFilesClientMock, + createMessageSigningServiceMock, createMockAgentPolicyService, createMockAgentService, createMockPackageService, - createMessageSigningServiceMock, - createFleetFromHostFilesClientMock, - createFleetToHostFilesClientMock, - createFleetActionsClientMock, + createPackagePolicyServiceMock, } from '@kbn/fleet-plugin/server/mocks'; import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks'; import type { RequestFixtureOptions, RouterMock } from '@kbn/core-http-router-server-mocks'; @@ -45,7 +45,7 @@ import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-ser import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { casesPluginMock } from '@kbn/cases-plugin/server/mocks'; import { createCasesClientMock } from '@kbn/cases-plugin/server/client/mocks'; -import type { VersionedRouteConfig, AddVersionOpts } from '@kbn/core-http-server'; +import type { AddVersionOpts, VersionedRouteConfig } from '@kbn/core-http-server'; import { createActionCreateServiceMock } from './services/actions/mocks'; import { getEndpointAuthzInitialStateMock } from '../../common/endpoint/service/authz/mocks'; import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__'; @@ -71,7 +71,7 @@ import type { EndpointAuthz } from '../../common/endpoint/types/authz'; import { EndpointFleetServicesFactory } from './services/fleet'; import { createLicenseServiceMock } from '../../common/license/mocks'; import { createFeatureUsageServiceMock } from './services/feature_usage/mocks'; -import { createAppFeaturesMock } from '../lib/app_features/mocks'; +import { createAppFeaturesServiceMock } from '../lib/app_features_service/mocks'; /** * Creates a mocked EndpointAppContext. @@ -132,6 +132,7 @@ export const createMockEndpointAppContextServiceSetupContract = (): jest.Mocked => { return { securitySolutionRequestContextFactory: requestContextFactoryMock.create(), + cloud: cloudMock.createSetup(), }; }; @@ -165,7 +166,12 @@ export const createMockEndpointAppContextServiceStartContract = savedObjectsStart ); const experimentalFeatures = config.experimentalFeatures; - const appFeatures = createAppFeaturesMock(undefined, experimentalFeatures, undefined, logger); + const appFeaturesService = createAppFeaturesServiceMock( + undefined, + experimentalFeatures, + undefined, + logger + ); packagePolicyService.list.mockImplementation(async (_, options) => { return { @@ -208,14 +214,13 @@ export const createMockEndpointAppContextServiceStartContract = >(), exceptionListsClient: listMock.getExceptionListClient(), cases: casesMock, - cloud: cloudMock.createSetup(), featureUsageService: createFeatureUsageServiceMock(), experimentalFeatures, messageSigningService: createMessageSigningServiceMock(), actionCreateService: undefined, createFleetActionsClient: jest.fn((_) => fleetActionsClientMock), esClient: elasticsearchClientMock.createElasticsearchClient(), - appFeatures, + appFeaturesService, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 4ddb81bfacd49..442708e625d84 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -6,9 +6,9 @@ */ import { - savedObjectsClientMock, - loggingSystemMock, elasticsearchServiceMock, + loggingSystemMock, + savedObjectsClientMock, } from '@kbn/core/server/mocks'; import type { Logger } from '@kbn/core/server'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; @@ -16,20 +16,20 @@ import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import { listMock } from '@kbn/lists-plugin/server/mocks'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { AppFeatureKeys } from '@kbn/security-solution-features'; import { - createPackagePolicyWithManifestMock, createPackagePolicyWithInitialManifestMock, - getMockManifest, - getMockArtifactsWithDiff, + createPackagePolicyWithManifestMock, getEmptyMockArtifacts, + getMockArtifactsWithDiff, + getMockManifest, } from '../../../lib/artifacts/mocks'; import { createEndpointArtifactClientMock, getManifestClientMock } from '../mocks'; import type { ManifestManagerContext } from './manifest_manager'; import { ManifestManager } from './manifest_manager'; import { parseExperimentalConfigValue } from '../../../../../common/experimental_features'; -import { createAppFeaturesMock } from '../../../../lib/app_features/mocks'; -import type { AppFeatureKeys } from '../../../../../common/types/app_features'; -import type { AppFeatures } from '../../../../lib/app_features/app_features'; +import { createAppFeaturesServiceMock } from '../../../../lib/app_features_service/mocks'; +import type { AppFeaturesService } from '../../../../lib/app_features_service/app_features_service'; export const createExceptionListResponse = (data: ExceptionListItemSchema[], total?: number) => ({ data, @@ -71,7 +71,7 @@ export interface ManifestManagerMockOptions { exceptionListClient: ExceptionListClient; packagePolicyService: jest.Mocked; savedObjectsClient: ReturnType; - appFeatures: AppFeatures; + appFeaturesService: AppFeaturesService; } export const buildManifestManagerMockOptions = ( @@ -83,7 +83,7 @@ export const buildManifestManagerMockOptions = ( exceptionListClient: listMock.getExceptionListClient(savedObjectMock), packagePolicyService: createPackagePolicyServiceMock(), savedObjectsClient: savedObjectMock, - appFeatures: createAppFeaturesMock(customAppFeatures), + appFeaturesService: createAppFeaturesServiceMock(customAppFeatures), ...opts, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 379a098bb1613..47753c7db0c97 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -7,11 +7,11 @@ import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { + ENDPOINT_BLOCKLISTS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, - ENDPOINT_EVENT_FILTERS_LIST_ID, - ENDPOINT_BLOCKLISTS_LIST_ID, } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import type { PackagePolicy } from '@kbn/fleet-plugin/common/types/models'; @@ -27,10 +27,10 @@ import { toArtifactRecords, } from '../../../lib/artifacts/mocks'; import { - ManifestConstants, getArtifactId, - translateToEndpointExceptions, Manifest, + ManifestConstants, + translateToEndpointExceptions, } from '../../../lib/artifacts'; import { @@ -43,7 +43,7 @@ import type { EndpointArtifactClientInterface } from '../artifact_client'; import { InvalidInternalManifestError } from '../errors'; import { EndpointError } from '../../../../../common/endpoint/errors'; import type { Artifact } from '@kbn/fleet-plugin/server'; -import { AppFeatureSecurityKey } from '../../../../../common/types/app_features'; +import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types/src/response/exception_list_item_schema'; const getArtifactObject = (artifact: InternalArtifactSchema) => @@ -257,7 +257,7 @@ describe('ManifestManager', () => { ( manifestManagerContext.artifactClient as jest.Mocked - ).listArtifacts.mockImplementation(async (id) => { + ).listArtifacts.mockImplementation(async () => { // report the MACOS Exceptions artifact as not found return { items: [ diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 865a75fc4b5ab..1cda8bdd9533b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -6,43 +6,46 @@ */ import semver from 'semver'; -import { isEqual, isEmpty, chunk, keyBy } from 'lodash'; +import { chunk, isEmpty, isEqual, keyBy } from 'lodash'; import type { ElasticsearchClient } from '@kbn/core/server'; import { type Logger, type SavedObjectsClientContract } from '@kbn/core/server'; import { - ENDPOINT_EVENT_FILTERS_LIST_ID, - ENDPOINT_TRUSTED_APPS_LIST_ID, ENDPOINT_BLOCKLISTS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, } from '@kbn/securitysolution-list-constants'; import type { ListResult, PackagePolicy } from '@kbn/fleet-plugin/common'; import type { Artifact, PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { AppFeatureKey } from '../../../../../common/types/app_features'; -import type { AppFeatures } from '../../../../lib/app_features'; +import { AppFeatureKey } from '@kbn/security-solution-features/keys'; +import type { AppFeaturesService } from '../../../../lib/app_features_service/app_features_service'; +import type { ExperimentalFeatures } from '../../../../../common'; import type { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common'; -import type { ManifestSchema } from '../../../../../common/endpoint/schema/manifest'; -import { manifestDispatchSchema } from '../../../../../common/endpoint/schema/manifest'; +import { + manifestDispatchSchema, + type ManifestSchema, +} from '../../../../../common/endpoint/schema/manifest'; -import type { ArtifactListId } from '../../../lib/artifacts'; import { ArtifactConstants, + type ArtifactListId, buildArtifact, + convertExceptionsToEndpointFormat, getAllItemsFromEndpointExceptionList, getArtifactId, Manifest, - convertExceptionsToEndpointFormat, } from '../../../lib/artifacts'; -import type { - InternalArtifactCompleteSchema, - WrappedTranslatedExceptionList, + +import { + internalArtifactCompleteSchema, + type InternalArtifactCompleteSchema, + type WrappedTranslatedExceptionList, } from '../../../schemas/artifacts'; -import { internalArtifactCompleteSchema } from '../../../schemas/artifacts'; import type { EndpointArtifactClientInterface } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; -import type { ExperimentalFeatures } from '../../../../../common/experimental_features'; import { InvalidInternalManifestError } from '../errors'; import { wrapErrorIfNeeded } from '../../../utils'; import { EndpointError } from '../../../../../common/endpoint/errors'; @@ -99,7 +102,7 @@ export interface ManifestManagerContext { experimentalFeatures: ExperimentalFeatures; packagerTaskPackagePolicyUpdateBatchSize: number; esClient: ElasticsearchClient; - appFeatures: AppFeatures; + appFeaturesService: AppFeaturesService; } const getArtifactIds = (manifest: ManifestSchema) => @@ -121,7 +124,7 @@ export class ManifestManager { protected cachedExceptionsListsByOs: Map; protected packagerTaskPackagePolicyUpdateBatchSize: number; protected esClient: ElasticsearchClient; - protected appFeatures: AppFeatures; + protected appFeaturesService: AppFeaturesService; constructor(context: ManifestManagerContext) { this.artifactClient = context.artifactClient; @@ -135,7 +138,7 @@ export class ManifestManager { this.packagerTaskPackagePolicyUpdateBatchSize = context.packagerTaskPackagePolicyUpdateBatchSize; this.esClient = context.esClient; - this.appFeatures = context.appFeatures; + this.appFeaturesService = context.appFeaturesService; } /** @@ -167,9 +170,9 @@ export class ManifestManager { let itemsByListId: ExceptionListItemSchema[] = []; if ( (listId === ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID && - this.appFeatures.isEnabled(AppFeatureKey.endpointResponseActions)) || + this.appFeaturesService.isEnabled(AppFeatureKey.endpointResponseActions)) || (listId !== ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID && - this.appFeatures.isEnabled(AppFeatureKey.endpointArtifactManagement)) + this.appFeaturesService.isEnabled(AppFeatureKey.endpointArtifactManagement)) ) { itemsByListId = await getAllItemsFromEndpointExceptionList({ elClient, diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 0ff3692971ad4..e2ce386337a85 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -25,11 +25,12 @@ import { import { buildManifestManagerMock } from '../endpoint/services/artifacts/manifest_manager/manifest_manager.mock'; import { getPackagePolicyCreateCallback, - getPackagePolicyPostCreateCallback, getPackagePolicyDeleteCallback, + getPackagePolicyPostCreateCallback, getPackagePolicyUpdateCallback, } from './fleet_integration'; import type { KibanaRequest } from '@kbn/core/server'; +import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; import { requestContextMock } from '../lib/detection_engine/routes/__mocks__'; import { requestContextFactoryMock } from '../request_context_factory.mock'; import type { EndpointAppContextServiceStartContract } from '../endpoint/endpoint_app_context_services'; @@ -54,9 +55,8 @@ import { createMockPolicyData } from '../endpoint/services/feature_usage/mocks'; import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../common/endpoint/service/artifacts/constants'; import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { disableProtections } from '../../common/endpoint/models/policy_config_helpers'; -import type { AppFeatures } from '../lib/app_features'; -import { createAppFeaturesMock } from '../lib/app_features/mocks'; -import { ALL_APP_FEATURE_KEYS } from '../../common'; +import type { AppFeaturesService } from '../lib/app_features_service/app_features_service'; +import { createAppFeaturesServiceMock } from '../lib/app_features_service/mocks'; jest.mock('uuid', () => ({ v4: (): string => 'NEW_UUID', @@ -77,7 +77,7 @@ describe('ingest_integration tests ', () => { }); const generator = new EndpointDocGenerator(); const cloudService = cloudMock.createSetup(); - let appFeatures: AppFeatures; + let appFeaturesService: AppFeaturesService; beforeEach(() => { endpointAppContextMock = createMockEndpointAppContextServiceStartContract(); @@ -86,7 +86,7 @@ describe('ingest_integration tests ', () => { licenseEmitter = new Subject(); licenseService = new LicenseService(); licenseService.start(licenseEmitter); - appFeatures = endpointAppContextMock.appFeatures; + appFeaturesService = endpointAppContextMock.appFeaturesService; jest .spyOn(endpointAppContextMock.endpointMetadataService, 'getFleetEndpointPackagePolicy') @@ -143,7 +143,7 @@ describe('ingest_integration tests ', () => { licenseService, exceptionListClient, cloudService, - appFeatures + appFeaturesService ); return callback( @@ -395,7 +395,7 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.endpointMetadataService, cloudService, esClient, - appFeatures + appFeaturesService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -414,7 +414,7 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.endpointMetadataService, cloudService, esClient, - appFeatures + appFeaturesService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -448,7 +448,7 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.endpointMetadataService, cloudService, esClient, - appFeatures + appFeaturesService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -463,7 +463,7 @@ describe('ingest_integration tests ', () => { }); it('should turn off protections if endpointPolicyProtections appFeature is disabled', async () => { - appFeatures = createAppFeaturesMock( + appFeaturesService = createAppFeaturesServiceMock( ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') ); const callback = getPackagePolicyUpdateCallback( @@ -473,7 +473,7 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.endpointMetadataService, cloudService, esClient, - appFeatures + appFeaturesService ); const updatedPolicy = await callback( @@ -552,7 +552,7 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.endpointMetadataService, cloudService, esClient, - appFeatures + appFeaturesService ); const policyConfig = generator.generatePolicyPackagePolicy(); @@ -589,7 +589,7 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.endpointMetadataService, cloudService, esClient, - appFeatures + appFeaturesService ); const policyConfig = generator.generatePolicyPackagePolicy(); // values should be updated diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index a9da860a5008e..554417eee480d 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -22,12 +22,11 @@ import type { } from '@kbn/fleet-plugin/common'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; -import { AppFeatureSecurityKey } from '../../common/types/app_features'; +import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys'; import { isPolicySetToEventCollectionOnly, ensureOnlyEventCollectionIsAllowed, } from '../../common/endpoint/models/policy_config_helpers'; -import type { AppFeatures } from '../lib/app_features'; import type { NewPolicyData, PolicyConfig } from '../../common/endpoint/types'; import type { LicenseService } from '../../common/license'; import type { ManifestManager } from '../endpoint/services'; @@ -44,6 +43,7 @@ import { notifyProtectionFeatureUsage } from './notify_protection_feature_usage' import type { AnyPolicyCreateConfig } from './types'; import { ENDPOINT_INTEGRATION_CONFIG_KEY } from './constants'; import { createEventFilters } from './handlers/create_event_filters'; +import type { AppFeaturesService } from '../lib/app_features_service/app_features_service'; const isEndpointPackagePolicy = ( packagePolicy: T @@ -81,7 +81,7 @@ export const getPackagePolicyCreateCallback = ( licenseService: LicenseService, exceptionsClient: ExceptionListClient | undefined, cloud: CloudSetup, - appFeatures: AppFeatures + appFeatures: AppFeaturesService ): PostPackagePolicyCreateCallback => { return async ( newPackagePolicy, @@ -186,7 +186,7 @@ export const getPackagePolicyUpdateCallback = ( endpointMetadataService: EndpointMetadataService, cloud: CloudSetup, esClient: ElasticsearchClient, - appFeatures: AppFeatures + appFeatures: AppFeaturesService ): PutPackagePolicyUpdateCallback => { return async (newPackagePolicy: NewPackagePolicy): Promise => { if (!isEndpointPackagePolicy(newPackagePolicy)) { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts index 2e847eff58f93..03abd2a6b1fb1 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts @@ -8,6 +8,7 @@ import { Subject } from 'rxjs'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; +import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; import { LicenseService } from '../../../common/license'; import { createDefaultPolicy } from './create_default_policy'; import { ProtectionModes } from '../../../common/endpoint/types'; @@ -19,9 +20,8 @@ import type { PolicyCreateCloudConfig, PolicyCreateEndpointConfig, } from '../types'; -import type { AppFeatures } from '../../lib/app_features'; -import { createAppFeaturesMock } from '../../lib/app_features/mocks'; -import { ALL_APP_FEATURE_KEYS } from '../../../common'; +import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service'; +import { createAppFeaturesServiceMock } from '../../lib/app_features_service/mocks'; describe('Create Default Policy tests ', () => { const cloud = cloudMock.createSetup(); @@ -31,7 +31,7 @@ describe('Create Default Policy tests ', () => { const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold', uid: '' } }); let licenseEmitter: Subject; let licenseService: LicenseService; - let appFeatures: AppFeatures; + let appFeaturesService: AppFeaturesService; const createDefaultPolicyCallback = async ( config: AnyPolicyCreateConfig | undefined @@ -39,7 +39,7 @@ describe('Create Default Policy tests ', () => { const esClientInfo = await elasticsearchServiceMock.createClusterClient().asInternalUser.info(); esClientInfo.cluster_name = ''; esClientInfo.cluster_uuid = ''; - return createDefaultPolicy(licenseService, config, cloud, esClientInfo, appFeatures); + return createDefaultPolicy(licenseService, config, cloud, esClientInfo, appFeaturesService); }; beforeEach(() => { @@ -47,7 +47,7 @@ describe('Create Default Policy tests ', () => { licenseService = new LicenseService(); licenseService.start(licenseEmitter); licenseEmitter.next(Platinum); // set license level to platinum - appFeatures = createAppFeaturesMock(); + appFeaturesService = createAppFeaturesServiceMock(); }); describe('When no config is set', () => { @@ -211,7 +211,7 @@ describe('Create Default Policy tests ', () => { }); it('should set policy to event collection only if endpointPolicyProtections appFeature is disabled', async () => { - appFeatures = createAppFeaturesMock( + appFeaturesService = createAppFeaturesServiceMock( ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') ); diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts index cd78b4c46493a..0dda27872af0e 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts @@ -7,8 +7,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; -import { AppFeatureSecurityKey } from '../../../common/types/app_features'; -import type { AppFeatures } from '../../lib/app_features'; +import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys'; import { policyFactory as policyConfigFactory, policyFactoryWithoutPaidFeatures as policyConfigFactoryWithoutPaidFeatures, @@ -26,6 +25,7 @@ import { disableProtections, ensureOnlyEventCollectionIsAllowed, } from '../../../common/endpoint/models/policy_config_helpers'; +import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service'; /** * Create the default endpoint policy based on the current license and configuration type @@ -35,7 +35,7 @@ export const createDefaultPolicy = ( config: AnyPolicyCreateConfig | undefined, cloud: CloudSetup, esClientInfo: InfoResponse, - appFeatures: AppFeatures + appFeatures: AppFeaturesService ): PolicyConfig => { // Pass license and cloud information to use in Policy creation const factoryPolicy = policyConfigFactory( diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts b/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts deleted file mode 100644 index 1951f6d8b00fa..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AppFeatures } from '.'; -import type { AppFeatureKeys, ExperimentalFeatures } from '../../../common'; -import type { PluginSetupContract } from '@kbn/features-plugin/server'; -import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; - -const SECURITY_BASE_CONFIG = { - foo: 'foo', -}; - -const SECURITY_APP_FEATURE_CONFIG = { - 'test-base-feature': { - privileges: { - all: { - ui: ['test-capability'], - api: ['test-capability'], - }, - read: { - ui: ['test-capability'], - api: ['test-capability'], - }, - }, - }, -}; - -const CASES_BASE_CONFIG = { - bar: 'bar', -}; - -const CASES_APP_FEATURE_CONFIG = { - 'test-cases-feature': { - privileges: { - all: { - ui: ['test-cases-capability'], - api: ['test-cases-capability'], - }, - read: { - ui: ['test-cases-capability'], - api: ['test-cases-capability'], - }, - }, - }, -}; - -const ASSISTANT_BASE_CONFIG = { - bar: 'bar', -}; - -const ASSISTANT_APP_FEATURE_CONFIG = { - 'test-assistant-feature': { - privileges: { - all: { - ui: ['test-assistant-capability'], - api: ['test-assistant-capability'], - }, - read: { - ui: ['test-assistant-capability'], - api: ['test-assistant-capability'], - }, - }, - }, -}; - -jest.mock('./security_kibana_features', () => { - return { - getSecurityBaseKibanaFeature: jest.fn(() => SECURITY_BASE_CONFIG), - getSecurityBaseKibanaSubFeatureIds: jest.fn(() => ['subFeature1']), - getSecurityAppFeaturesConfig: jest.fn(() => SECURITY_APP_FEATURE_CONFIG), - }; -}); -jest.mock('./security_kibana_sub_features', () => { - return { - securitySubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]), - }; -}); - -jest.mock('./security_cases_kibana_features', () => { - return { - getCasesBaseKibanaFeature: jest.fn(() => CASES_BASE_CONFIG), - getCasesBaseKibanaSubFeatureIds: jest.fn(() => ['subFeature1']), - getCasesAppFeaturesConfig: jest.fn(() => CASES_APP_FEATURE_CONFIG), - }; -}); - -jest.mock('./security_cases_kibana_sub_features', () => { - return { - casesSubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]), - }; -}); - -jest.mock('./security_assistant_kibana_features', () => { - return { - getAssistantBaseKibanaFeature: jest.fn(() => ASSISTANT_BASE_CONFIG), - getAssistantBaseKibanaSubFeatureIds: jest.fn(() => ['subFeature1']), - getAssistantAppFeaturesConfig: jest.fn(() => ASSISTANT_APP_FEATURE_CONFIG), - }; -}); - -jest.mock('./security_assistant_kibana_sub_features', () => { - return { - assistantSubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]), - }; -}); - -describe('AppFeatures', () => { - it('should register enabled kibana features', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - getKibanaFeatures: jest.fn(), - } as unknown as PluginSetupContract; - - const appFeatureKeys = ['test-base-feature'] as unknown as AppFeatureKeys; - - const appFeatures = new AppFeatures( - loggingSystemMock.create().get('mock'), - [] as unknown as ExperimentalFeatures - ); - appFeatures.init(featuresSetup); - appFeatures.set(appFeatureKeys); - - expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ - ...SECURITY_BASE_CONFIG, - ...SECURITY_APP_FEATURE_CONFIG['test-base-feature'], - subFeatures: [{ baz: 'baz' }], - }); - }); - - it('should register enabled cases features', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - } as unknown as PluginSetupContract; - - const appFeatureKeys = ['test-cases-feature'] as unknown as AppFeatureKeys; - - const appFeatures = new AppFeatures( - loggingSystemMock.create().get('mock'), - [] as unknown as ExperimentalFeatures - ); - appFeatures.init(featuresSetup); - appFeatures.set(appFeatureKeys); - - expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ - ...CASES_BASE_CONFIG, - ...CASES_APP_FEATURE_CONFIG['test-cases-feature'], - subFeatures: [{ baz: 'baz' }], - }); - }); - - it('should register enabled assistant features', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - } as unknown as PluginSetupContract; - - const appFeatureKeys = ['test-assistant-feature'] as unknown as AppFeatureKeys; - - const appFeatures = new AppFeatures( - loggingSystemMock.create().get('mock'), - [] as unknown as ExperimentalFeatures - ); - appFeatures.init(featuresSetup); - appFeatures.set(appFeatureKeys); - - expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ - ...ASSISTANT_BASE_CONFIG, - ...ASSISTANT_APP_FEATURE_CONFIG['test-assistant-feature'], - subFeatures: [{ baz: 'baz' }], - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts deleted file mode 100644 index 0b17f6d71d00d..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from '@kbn/core/server'; -import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; -import type { AppFeatureKey, AppFeatureKeys, ExperimentalFeatures } from '../../../common'; -import type { AppFeatureKibanaConfig, AppFeaturesConfig } from './types'; -import { - getSecurityAppFeaturesConfig, - getSecurityBaseKibanaFeature, - getSecurityBaseKibanaSubFeatureIds, -} from './security_kibana_features'; -import { - getCasesBaseKibanaFeature, - getCasesAppFeaturesConfig, - getCasesBaseKibanaSubFeatureIds, -} from './security_cases_kibana_features'; -import { AppFeaturesConfigMerger } from './app_features_config_merger'; -import { casesSubFeaturesMap } from './security_cases_kibana_sub_features'; -import { securitySubFeaturesMap } from './security_kibana_sub_features'; -import { assistantSubFeaturesMap } from './security_assistant_kibana_sub_features'; -import { - getAssistantAppFeaturesConfig, - getAssistantBaseKibanaFeature, - getAssistantBaseKibanaSubFeatureIds, -} from './security_assistant_kibana_features'; - -export class AppFeatures { - private securityFeatureConfigMerger: AppFeaturesConfigMerger; - private assistantFeatureConfigMerger: AppFeaturesConfigMerger; - private casesFeatureConfigMerger: AppFeaturesConfigMerger; - private appFeatures?: Set; - private featuresSetup?: FeaturesPluginSetup; - - constructor( - private readonly logger: Logger, - private readonly experimentalFeatures: ExperimentalFeatures - ) { - this.securityFeatureConfigMerger = new AppFeaturesConfigMerger( - this.logger, - securitySubFeaturesMap - ); - this.casesFeatureConfigMerger = new AppFeaturesConfigMerger(this.logger, casesSubFeaturesMap); - this.assistantFeatureConfigMerger = new AppFeaturesConfigMerger( - this.logger, - assistantSubFeaturesMap - ); - } - - public init(featuresSetup: FeaturesPluginSetup) { - this.featuresSetup = featuresSetup; - } - - public set(appFeatureKeys: AppFeatureKeys) { - if (this.appFeatures) { - throw new Error('AppFeatures has already been initialized'); - } - this.appFeatures = new Set(appFeatureKeys); - this.registerEnabledKibanaFeatures(); - } - - public isEnabled(appFeatureKey: AppFeatureKey): boolean { - if (!this.appFeatures) { - throw new Error('AppFeatures has not been initialized'); - } - return this.appFeatures.has(appFeatureKey); - } - - protected registerEnabledKibanaFeatures() { - if (this.featuresSetup == null) { - throw new Error( - 'Cannot sync kibana features as featuresSetup is not present. Did you call init?' - ); - } - // register main security Kibana features - const securityBaseKibanaFeature = getSecurityBaseKibanaFeature(); - const securityBaseKibanaSubFeatureIds = getSecurityBaseKibanaSubFeatureIds( - this.experimentalFeatures - ); - const enabledSecurityAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs( - getSecurityAppFeaturesConfig(this.experimentalFeatures) - ); - const completeAppFeatureConfig = this.securityFeatureConfigMerger.mergeAppFeatureConfigs( - securityBaseKibanaFeature, - securityBaseKibanaSubFeatureIds, - enabledSecurityAppFeaturesConfigs - ); - - this.logger.debug(JSON.stringify(completeAppFeatureConfig)); - - this.featuresSetup.registerKibanaFeature(completeAppFeatureConfig); - - // register security cases Kibana features - const securityCasesBaseKibanaFeature = getCasesBaseKibanaFeature(); - const securityCasesBaseKibanaSubFeatureIds = getCasesBaseKibanaSubFeatureIds(); - const enabledCasesAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs( - getCasesAppFeaturesConfig() - ); - const completeCasesAppFeatureConfig = this.casesFeatureConfigMerger.mergeAppFeatureConfigs( - securityCasesBaseKibanaFeature, - securityCasesBaseKibanaSubFeatureIds, - enabledCasesAppFeaturesConfigs - ); - - this.logger.info(JSON.stringify(completeCasesAppFeatureConfig)); - - this.featuresSetup.registerKibanaFeature(completeCasesAppFeatureConfig); - - // register security assistant Kibana features - const securityAssistantBaseKibanaFeature = getAssistantBaseKibanaFeature(); - const securityAssistantBaseKibanaSubFeatureIds = getAssistantBaseKibanaSubFeatureIds(); - const enabledAssistantAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs( - getAssistantAppFeaturesConfig() - ); - const completeAssistantAppFeatureConfig = - this.assistantFeatureConfigMerger.mergeAppFeatureConfigs( - securityAssistantBaseKibanaFeature, - securityAssistantBaseKibanaSubFeatureIds, - enabledAssistantAppFeaturesConfigs - ); - - this.logger.info(JSON.stringify(completeAssistantAppFeatureConfig)); - - this.featuresSetup.registerKibanaFeature(completeAssistantAppFeatureConfig); - } - - private getEnabledAppFeaturesConfigs( - appFeaturesConfigs: Partial - ): AppFeatureKibanaConfig[] { - return Object.entries(appFeaturesConfigs).reduce( - (acc, [appFeatureKey, appFeatureConfig]) => { - if (this.isEnabled(appFeatureKey as AppFeatureKey)) { - acc.push(appFeatureConfig); - } - return acc; - }, - [] - ); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/app_features/mocks.ts b/x-pack/plugins/security_solution/server/lib/app_features/mocks.ts deleted file mode 100644 index 1a5efc9c64e37..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/app_features/mocks.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from '@kbn/core/server'; -import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; -import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; -import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; -import { AppFeatures } from './app_features'; -import type { AppFeatureKeys, ExperimentalFeatures } from '../../../common'; -import { ALL_APP_FEATURE_KEYS, allowedExperimentalValues } from '../../../common'; - -class AppFeaturesMock extends AppFeatures { - protected registerEnabledKibanaFeatures() { - // NOOP - } -} - -export const createAppFeaturesMock = ( - /** What features keys should be enabled. Default is all */ - enabledFeatureKeys: AppFeatureKeys = [...ALL_APP_FEATURE_KEYS], - experimentalFeatures: ExperimentalFeatures = { ...allowedExperimentalValues }, - featuresPluginSetupContract: FeaturesPluginSetup = featuresPluginMock.createSetup(), - logger: Logger = loggingSystemMock.create().get('appFeatureMock') -) => { - const appFeatures = new AppFeaturesMock(logger, experimentalFeatures); - - appFeatures.init(featuresPluginSetupContract); - - if (enabledFeatureKeys) { - appFeatures.set(enabledFeatureKeys); - } - - return appFeatures; -}; diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts deleted file mode 100644 index 1927591da202f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -import type { AppFeaturesAssistantConfig, BaseKibanaFeatureConfig } from './types'; -import { APP_ID, ASSISTANT_FEATURE_ID } from '../../../common/constants'; -import { AppFeatureAssistantKey } from '../../../common/types/app_features'; -import type { AssistantSubFeatureId } from './security_assistant_kibana_sub_features'; - -export const getAssistantBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ - id: ASSISTANT_FEATURE_ID, - name: i18n.translate( - 'xpack.securitySolution.featureRegistry.linkSecuritySolutionAssistantTitle', - { - defaultMessage: 'Elastic AI Assistant', - } - ), - order: 1100, - category: DEFAULT_APP_CATEGORIES.security, - app: [ASSISTANT_FEATURE_ID, 'kibana'], - catalogue: [APP_ID], - minimumLicense: 'enterprise', - privileges: { - all: { - api: [], - app: [ASSISTANT_FEATURE_ID, 'kibana'], - catalogue: [APP_ID], - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - read: { - // No read-only mode currently supported - disabled: true, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - }, -}); - -export const getAssistantBaseKibanaSubFeatureIds = (): AssistantSubFeatureId[] => [ - // This is a sample sub-feature that can be used for future implementations - // AssistantSubFeatureId.createConversation, -]; - -/** - * Maps the AppFeatures keys to Kibana privileges that will be merged - * into the base privileges config for the Security app. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Security Assistant feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Assistant subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Assistant subFeature with the privilege `id` specified. - */ -export const getAssistantAppFeaturesConfig = (): AppFeaturesAssistantConfig => ({ - [AppFeatureAssistantKey.assistant]: { - privileges: { - all: { - ui: ['ai-assistant'], - }, - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_features.ts deleted file mode 100644 index a2bf02c59b306..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_features.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; -import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -import { - createUICapabilities as createCasesUICapabilities, - getApiTags as getCasesApiTags, -} from '@kbn/cases-plugin/common'; -import { - CASES_CONNECTORS_CAPABILITY, - GET_CONNECTORS_CONFIGURE_API_TAG, -} from '@kbn/cases-plugin/common/constants'; -import type { AppFeaturesCasesConfig, BaseKibanaFeatureConfig } from './types'; -import { APP_ID, CASES_FEATURE_ID } from '../../../common/constants'; -import { CasesSubFeatureId } from './security_cases_kibana_sub_features'; -import { AppFeatureCasesKey } from '../../../common/types/app_features'; - -const casesCapabilities = createCasesUICapabilities(); -const casesApiTags = getCasesApiTags(APP_ID); - -export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => { - // On SecuritySolution essentials cases does not have the connector feature - const casesAllUICapabilities = casesCapabilities.all.filter( - (capability) => capability !== CASES_CONNECTORS_CAPABILITY - ); - - const casesReadUICapabilities = casesCapabilities.read.filter( - (capability) => capability !== CASES_CONNECTORS_CAPABILITY - ); - - const casesAllAPICapabilities = casesApiTags.all.filter( - (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG - ); - - const casesReadAPICapabilities = casesApiTags.read.filter( - (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG - ); - - return { - id: CASES_FEATURE_ID, - name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', { - defaultMessage: 'Cases', - }), - order: 1100, - category: DEFAULT_APP_CATEGORIES.security, - app: [CASES_FEATURE_ID, 'kibana'], - catalogue: [APP_ID], - cases: [APP_ID], - privileges: { - all: { - api: casesAllAPICapabilities, - app: [CASES_FEATURE_ID, 'kibana'], - catalogue: [APP_ID], - cases: { - create: [APP_ID], - read: [APP_ID], - update: [APP_ID], - }, - savedObject: { - all: [...filesSavedObjectTypes], - read: [...filesSavedObjectTypes], - }, - ui: casesAllUICapabilities, - }, - read: { - api: casesReadAPICapabilities, - app: [CASES_FEATURE_ID, 'kibana'], - catalogue: [APP_ID], - cases: { - read: [APP_ID], - }, - savedObject: { - all: [], - read: [...filesSavedObjectTypes], - }, - ui: casesReadUICapabilities, - }, - }, - }; -}; - -export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ - CasesSubFeatureId.deleteCases, -]; - -/** - * Maps the AppFeatures keys to Kibana privileges that will be merged - * into the base privileges config for the Security Cases app. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Security Cases feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Cases subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Cases subFeature with the privilege `id` specified. - */ -export const getCasesAppFeaturesConfig = (): AppFeaturesCasesConfig => ({ - [AppFeatureCasesKey.casesConnectors]: { - privileges: { - all: { - api: [GET_CONNECTORS_CONFIGURE_API_TAG], // Add cases connector get connectors API privileges - ui: [CASES_CONNECTORS_CAPABILITY], // Add cases connector UI privileges - cases: { - push: [APP_ID], // Add cases connector push privileges - }, - }, - read: { - api: [GET_CONNECTORS_CONFIGURE_API_TAG], // Add cases connector get connectors API privileges - ui: [CASES_CONNECTORS_CAPABILITY], // Add cases connector UI privileges - }, - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_sub_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_sub_features.ts deleted file mode 100644 index ab3069a59cc88..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_sub_features.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'; -import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; -import type { SubFeatureConfig } from '@kbn/features-plugin/common'; -import { - createUICapabilities as createCasesUICapabilities, - getApiTags as getCasesApiTags, -} from '@kbn/cases-plugin/common'; -import { APP_ID } from '../../../common/constants'; - -const casesCapabilities = createCasesUICapabilities(); -const casesApiTags = getCasesApiTags(APP_ID); - -const deleteCasesSubFeature: SubFeatureConfig = { - name: i18n.translate('xpack.securitySolution.featureRegistry.deleteSubFeatureName', { - defaultMessage: 'Delete', - }), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - api: casesApiTags.delete, - id: 'cases_delete', - name: i18n.translate('xpack.securitySolution.featureRegistry.deleteSubFeatureDetails', { - defaultMessage: 'Delete cases and comments', - }), - includeIn: 'all', - savedObject: { - all: [...filesSavedObjectTypes], - read: [...filesSavedObjectTypes], - }, - cases: { - delete: [APP_ID], - }, - ui: casesCapabilities.delete, - }, - ], - }, - ], -}; - -export enum CasesSubFeatureId { - deleteCases = 'deleteCasesSubFeature', -} - -// Defines all the ordered Security Cases subFeatures available -export const casesSubFeaturesMap = Object.freeze( - new Map([ - [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], - ]) -); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_kibana_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_kibana_features.ts deleted file mode 100644 index 0a77e5a9e5d7e..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/app_features/security_kibana_features.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; -import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; -import { - EQL_RULE_TYPE_ID, - INDICATOR_RULE_TYPE_ID, - ML_RULE_TYPE_ID, - NEW_TERMS_RULE_TYPE_ID, - QUERY_RULE_TYPE_ID, - SAVED_QUERY_RULE_TYPE_ID, - THRESHOLD_RULE_TYPE_ID, -} from '@kbn/securitysolution-rules'; -import type { ExperimentalFeatures } from '../../../common'; -import { SecuritySubFeatureId } from './security_kibana_sub_features'; -import { APP_ID, LEGACY_NOTIFICATIONS_ID, SERVER_APP_ID } from '../../../common/constants'; -import { savedObjectTypes } from '../../saved_objects'; -import type { AppFeaturesSecurityConfig, BaseKibanaFeatureConfig } from './types'; -import { AppFeatureSecurityKey } from '../../../common/types/app_features'; - -// Same as the plugin id defined by Cloud Security Posture -const CLOUD_POSTURE_APP_ID = 'csp'; -// Same as the saved-object type for rules defined by Cloud Security Posture -const CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE = 'csp_rule'; - -const SECURITY_RULE_TYPES = [ - LEGACY_NOTIFICATIONS_ID, - EQL_RULE_TYPE_ID, - INDICATOR_RULE_TYPE_ID, - ML_RULE_TYPE_ID, - QUERY_RULE_TYPE_ID, - SAVED_QUERY_RULE_TYPE_ID, - THRESHOLD_RULE_TYPE_ID, - NEW_TERMS_RULE_TYPE_ID, -]; - -export const getSecurityBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ - id: SERVER_APP_ID, - name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle', { - defaultMessage: 'Security', - }), - order: 1100, - category: DEFAULT_APP_CATEGORIES.security, - app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'], - catalogue: [APP_ID], - management: { - insightsAndAlerting: ['triggersActions'], - }, - alerting: SECURITY_RULE_TYPES, - privileges: { - all: { - app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'], - catalogue: [APP_ID], - api: [ - APP_ID, - 'lists-all', - 'lists-read', - 'lists-summary', - 'rac', - 'cloud-security-posture-all', - 'cloud-security-posture-read', - ], - savedObject: { - all: [ - 'alert', - 'exception-list', - EXCEPTION_LIST_NAMESPACE_AGNOSTIC, - DATA_VIEW_SAVED_OBJECT_TYPE, - ...savedObjectTypes, - CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE, - ], - read: [], - }, - alerting: { - rule: { - all: SECURITY_RULE_TYPES, - }, - alert: { - all: SECURITY_RULE_TYPES, - }, - }, - management: { - insightsAndAlerting: ['triggersActions'], - }, - ui: ['show', 'crud'], - }, - read: { - app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'], - catalogue: [APP_ID], - api: [APP_ID, 'lists-read', 'rac', 'cloud-security-posture-read'], - savedObject: { - all: [], - read: [ - 'exception-list', - EXCEPTION_LIST_NAMESPACE_AGNOSTIC, - DATA_VIEW_SAVED_OBJECT_TYPE, - ...savedObjectTypes, - CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE, - ], - }, - alerting: { - rule: { - read: SECURITY_RULE_TYPES, - }, - alert: { - all: SECURITY_RULE_TYPES, - }, - }, - management: { - insightsAndAlerting: ['triggersActions'], - }, - ui: ['show'], - }, - }, -}); - -/** - * Returns the list of Security SubFeature IDs that should be loaded and available in - * kibana regardless of PLI or License level. - * @param _ - */ -export const getSecurityBaseKibanaSubFeatureIds = ( - _: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use -): SecuritySubFeatureId[] => [SecuritySubFeatureId.hostIsolation]; - -/** - * Maps the AppFeatures keys to Kibana privileges that will be merged - * into the base privileges config for the Security app. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Security feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. - */ -export const getSecurityAppFeaturesConfig = ( - _: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use -): AppFeaturesSecurityConfig => { - return { - [AppFeatureSecurityKey.advancedInsights]: { - privileges: { - all: { - ui: ['entity-analytics'], - api: [`${APP_ID}-entity-analytics`], - }, - read: { - ui: ['entity-analytics'], - api: [`${APP_ID}-entity-analytics`], - }, - }, - }, - [AppFeatureSecurityKey.investigationGuide]: { - privileges: { - all: { - ui: ['investigation-guide'], - }, - read: { - ui: ['investigation-guide'], - }, - }, - }, - - [AppFeatureSecurityKey.threatIntelligence]: { - privileges: { - all: { - ui: ['threat-intelligence'], - api: [`${APP_ID}-threat-intelligence`], - }, - read: { - ui: ['threat-intelligence'], - api: [`${APP_ID}-threat-intelligence`], - }, - }, - }, - - [AppFeatureSecurityKey.endpointHostManagement]: { - subFeatureIds: [SecuritySubFeatureId.endpointList], - }, - - [AppFeatureSecurityKey.endpointPolicyManagement]: { - subFeatureIds: [SecuritySubFeatureId.policyManagement], - }, - - // Adds no additional kibana feature controls - [AppFeatureSecurityKey.endpointPolicyProtections]: {}, - - [AppFeatureSecurityKey.endpointArtifactManagement]: { - subFeatureIds: [ - SecuritySubFeatureId.trustedApplications, - SecuritySubFeatureId.blocklist, - SecuritySubFeatureId.eventFilters, - ], - subFeaturesPrivileges: [ - { - id: 'host_isolation_exceptions_all', - api: [ - `${APP_ID}-accessHostIsolationExceptions`, - `${APP_ID}-writeHostIsolationExceptions`, - ], - ui: ['accessHostIsolationExceptions', 'writeHostIsolationExceptions'], - }, - { - id: 'host_isolation_exceptions_read', - api: [`${APP_ID}-accessHostIsolationExceptions`], - ui: ['accessHostIsolationExceptions'], - }, - ], - }, - - [AppFeatureSecurityKey.endpointResponseActions]: { - subFeatureIds: [ - SecuritySubFeatureId.hostIsolationExceptions, - - SecuritySubFeatureId.responseActionsHistory, - SecuritySubFeatureId.processOperations, - SecuritySubFeatureId.fileOperations, - SecuritySubFeatureId.executeAction, - ], - subFeaturesPrivileges: [ - // Adds the privilege to Isolate hosts to the already loaded `host_isolation_all` - // sub-feature (always loaded), which included the `release` privilege already - { - id: 'host_isolation_all', - api: [`${APP_ID}-writeHostIsolation`], - ui: ['writeHostIsolation'], - }, - ], - }, - - [AppFeatureSecurityKey.osqueryAutomatedResponseActions]: {}, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/app_features/types.ts b/x-pack/plugins/security_solution/server/lib/app_features/types.ts deleted file mode 100644 index e6a4fd8db0304..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/app_features/types.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 type { KibanaFeatureConfig, SubFeaturePrivilegeConfig } from '@kbn/features-plugin/common'; -import type { AppFeatureKey } from '../../../common'; -import type { - AppFeatureSecurityKey, - AppFeatureCasesKey, - AppFeatureAssistantKey, -} from '../../../common/types/app_features'; -import type { RecursivePartial } from '../../../common/utility_types'; - -export type BaseKibanaFeatureConfig = Omit; -export type SubFeaturesPrivileges = RecursivePartial; -export type AppFeatureKibanaConfig = - RecursivePartial & { - subFeatureIds?: T[]; - subFeaturesPrivileges?: SubFeaturesPrivileges[]; - }; -export type AppFeaturesConfig = Record< - AppFeatureKey, - AppFeatureKibanaConfig ->; -export type AppFeaturesSecurityConfig = Record< - AppFeatureSecurityKey, - AppFeatureKibanaConfig ->; -export type AppFeaturesCasesConfig = Record< - AppFeatureCasesKey, - AppFeatureKibanaConfig ->; -export type AppFeaturesAssistantConfig = Record< - AppFeatureAssistantKey, - AppFeatureKibanaConfig ->; diff --git a/x-pack/plugins/security_solution/server/lib/app_features_service/app_features.test.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features.test.ts new file mode 100644 index 0000000000000..8effe3837f76d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features.test.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PluginSetupContract } from '@kbn/features-plugin/server'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { AppFeatures } from './app_features'; +import type { + AppFeatureKeyType, + AppFeaturesConfig, + AppSubFeaturesMap, + BaseKibanaFeatureConfig, +} from '@kbn/security-solution-features'; + +const category = { + id: 'security', + label: 'Security app category', +}; + +const baseKibanaFeature: BaseKibanaFeatureConfig = { + id: 'FEATURE_ID', + name: 'Base Feature', + order: 1100, + app: ['FEATURE_ID', 'kibana'], + catalogue: ['APP_ID'], + privileges: { + all: { + api: ['api-read', 'api-write'], + app: ['FEATURE_ID', 'kibana'], + catalogue: ['APP_ID'], + savedObject: { + all: [], + read: [], + }, + ui: ['write', 'read'], + }, + read: { + api: ['api-read'], + app: ['FEATURE_ID', 'kibana'], + catalogue: ['APP_ID'], + savedObject: { + all: [], + read: [], + }, + ui: ['read'], + }, + }, + category, +}; + +const privileges = { + privileges: { + all: { + api: ['api-read', 'api-write', 'test-capability'], + app: ['FEATURE_ID', 'kibana'], + catalogue: ['APP_ID'], + savedObject: { + all: [], + read: [], + }, + ui: ['write', 'read', 'test-capability'], + }, + read: { + api: ['api-read', 'test-capability'], + app: ['FEATURE_ID', 'kibana'], + catalogue: ['APP_ID'], + savedObject: { + all: [], + read: [], + }, + ui: ['read', 'test-capability'], + }, + }, +}; + +const SECURITY_APP_FEATURE_CONFIG: AppFeaturesConfig = new Map(); +SECURITY_APP_FEATURE_CONFIG.set('test-base-feature' as AppFeatureKeyType, { + privileges: { + all: { + ui: ['test-capability'], + api: ['test-capability'], + }, + read: { + ui: ['test-capability'], + api: ['test-capability'], + }, + }, +}); + +const CASES_BASE_CONFIG = { + privileges: { + all: { + api: ['api-read', 'api-write', 'test-cases-capability'], + app: ['FEATURE_ID', 'kibana'], + catalogue: ['APP_ID'], + savedObject: { + all: [], + read: [], + }, + ui: ['write', 'read', 'test-cases-capability'], + }, + read: { + api: ['api-read', 'test-cases-capability'], + app: ['FEATURE_ID', 'kibana'], + catalogue: ['APP_ID'], + savedObject: { + all: [], + read: [], + }, + ui: ['read', 'test-cases-capability'], + }, + }, +}; + +const CASES_APP_FEATURE_CONFIG: AppFeaturesConfig = new Map(); +CASES_APP_FEATURE_CONFIG.set('test-cases-feature' as AppFeatureKeyType, { + privileges: { + all: { + ui: ['test-cases-capability'], + api: ['test-cases-capability'], + }, + read: { + ui: ['test-cases-capability'], + api: ['test-cases-capability'], + }, + }, +}); + +const securityKibanaSubFeatures = { + securitySubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]), +}; + +const securityCasesKibanaSubFeatures = { + casesSubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]), +}; + +describe('AppFeatures', () => { + it('should register enabled kibana features', () => { + const featuresSetup = { + registerKibanaFeature: jest.fn(), + getKibanaFeatures: jest.fn(), + } as unknown as PluginSetupContract; + + const appFeatures = new AppFeatures( + loggingSystemMock.create().get('mock'), + securityKibanaSubFeatures.securitySubFeaturesMap as unknown as AppSubFeaturesMap, + baseKibanaFeature, + ['subFeature1'] + ); + appFeatures.init(featuresSetup); + appFeatures.setConfig(SECURITY_APP_FEATURE_CONFIG); + + expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ + ...baseKibanaFeature, + ...SECURITY_APP_FEATURE_CONFIG.get('test-base-feature' as AppFeatureKeyType), + ...privileges, + subFeatures: [{ baz: 'baz' }], + }); + }); + + it('should register enabled cases features', () => { + const featuresSetup = { + registerKibanaFeature: jest.fn(), + } as unknown as PluginSetupContract; + + const appFeatures = new AppFeatures( + loggingSystemMock.create().get('mock'), + securityCasesKibanaSubFeatures.casesSubFeaturesMap as unknown as AppSubFeaturesMap, + baseKibanaFeature, + ['subFeature1'] + ); + appFeatures.init(featuresSetup); + appFeatures.setConfig(CASES_APP_FEATURE_CONFIG); + + expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ + ...baseKibanaFeature, + ...CASES_APP_FEATURE_CONFIG.get('test-cases-feature' as AppFeatureKeyType), + subFeatures: [{ baz: 'baz' }], + ...CASES_BASE_CONFIG, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/app_features_service/app_features.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features.ts new file mode 100644 index 0000000000000..7b264f49faa50 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features.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 { Logger } from '@kbn/core/server'; +import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import type { + AppFeatureKeyType, + AppFeaturesConfig, + AppSubFeaturesMap, + BaseKibanaFeatureConfig, +} from '@kbn/security-solution-features'; +import { AppFeaturesConfigMerger } from './app_features_config_merger'; + +export class AppFeatures { + private featureConfigMerger: AppFeaturesConfigMerger; + private appFeatures?: Set; + private featuresSetup?: FeaturesPluginSetup; + + constructor( + private readonly logger: Logger, + subFeaturesMap: AppSubFeaturesMap, + private readonly baseKibanaFeature: BaseKibanaFeatureConfig, + private readonly baseKibanaSubFeatureIds: T[] + ) { + this.featureConfigMerger = new AppFeaturesConfigMerger(this.logger, subFeaturesMap); + } + + public init(featuresSetup: FeaturesPluginSetup) { + this.featuresSetup = featuresSetup; + } + + public setConfig(config: AppFeaturesConfig) { + if (this.appFeatures) { + throw new Error('AppFeatures has already been registered'); + } + this.registerEnabledKibanaFeatures(config); + } + + private registerEnabledKibanaFeatures(appFeatureConfig: AppFeaturesConfig) { + if (this.featuresSetup == null) { + throw new Error( + 'Cannot sync kibana features as featuresSetup is not present. Did you call init?' + ); + } + + const completeAppFeatureConfig = this.featureConfigMerger.mergeAppFeatureConfigs( + this.baseKibanaFeature, + this.baseKibanaSubFeatureIds, + Array.from(appFeatureConfig.values()) + ); + + this.logger.debug(JSON.stringify(completeAppFeatureConfig)); + + this.featuresSetup.registerKibanaFeature(completeAppFeatureConfig); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features_config_merger.test.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features_config_merger.test.ts similarity index 99% rename from x-pack/plugins/security_solution/server/lib/app_features/app_features_config_merger.test.ts rename to x-pack/plugins/security_solution/server/lib/app_features_service/app_features_config_merger.test.ts index 49845c694ddb2..d0b95d50e8fb8 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/app_features_config_merger.test.ts +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features_config_merger.test.ts @@ -8,7 +8,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { AppFeaturesConfigMerger } from './app_features_config_merger'; import type { Logger } from '@kbn/core/server'; -import type { AppFeatureKibanaConfig } from './types'; +import type { AppFeatureKibanaConfig } from '@kbn/security-solution-features'; import type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common'; const category = { diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features_config_merger.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features_config_merger.ts similarity index 96% rename from x-pack/plugins/security_solution/server/lib/app_features/app_features_config_merger.ts rename to x-pack/plugins/security_solution/server/lib/app_features_service/app_features_config_merger.ts index a35273d34f3b1..524b6564eab26 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/app_features_config_merger.ts +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features_config_merger.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { cloneDeep, mergeWith, isArray, uniq } from 'lodash'; +import { cloneDeep, isArray, mergeWith, uniq } from 'lodash'; import type { Logger } from '@kbn/core/server'; import type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common'; import type { AppFeatureKibanaConfig, BaseKibanaFeatureConfig, SubFeaturesPrivileges, -} from './types'; +} from '@kbn/security-solution-features'; export class AppFeaturesConfigMerger { constructor( @@ -23,6 +23,7 @@ export class AppFeaturesConfigMerger { /** * Merges `appFeaturesConfigs` into `kibanaFeatureConfig`. * @param kibanaFeatureConfig the KibanaFeatureConfig to merge into + * @param kibanaSubFeatureIds * @param appFeaturesConfigs the AppFeatureKibanaConfig to merge * @returns mergedKibanaFeatureConfig the merged KibanaFeatureConfig * */ diff --git a/x-pack/plugins/security_solution/server/lib/app_features_service/app_features_service.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features_service.ts new file mode 100644 index 0000000000000..97fcf6cf67ed6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/app_features_service.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; +import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import type { AppFeatureKeyType } from '@kbn/security-solution-features'; +import { + getAssistantFeature, + getCasesFeature, + getSecurityFeature, +} from '@kbn/security-solution-features/app_features'; +import type { ExperimentalFeatures } from '../../../common'; +import { AppFeatures } from './app_features'; +import type { AppFeaturesConfigurator } from './types'; +import { securityDefaultSavedObjects } from './security_saved_objects'; +import { casesApiTags, casesUiCapabilities } from './cases_privileges'; + +export class AppFeaturesService { + private securityAppFeatures: AppFeatures; + private casesAppFeatures: AppFeatures; + private securityAssistantAppFeatures: AppFeatures; + private appFeatures?: Set; + + constructor( + private readonly logger: Logger, + private readonly experimentalFeatures: ExperimentalFeatures + ) { + const securityFeature = getSecurityFeature({ + savedObjects: securityDefaultSavedObjects, + experimentalFeatures: this.experimentalFeatures, + }); + this.securityAppFeatures = new AppFeatures( + this.logger, + securityFeature.subFeaturesMap, + securityFeature.baseKibanaFeature, + securityFeature.baseKibanaSubFeatureIds + ); + + const casesFeature = getCasesFeature({ + uiCapabilities: casesUiCapabilities, + apiTags: casesApiTags, + savedObjects: { files: filesSavedObjectTypes }, + }); + this.casesAppFeatures = new AppFeatures( + this.logger, + casesFeature.subFeaturesMap, + casesFeature.baseKibanaFeature, + casesFeature.baseKibanaSubFeatureIds + ); + + const assistantFeature = getAssistantFeature(); + this.securityAssistantAppFeatures = new AppFeatures( + this.logger, + assistantFeature.subFeaturesMap, + assistantFeature.baseKibanaFeature, + assistantFeature.baseKibanaSubFeatureIds + ); + } + + public init(featuresSetup: FeaturesPluginSetup) { + this.securityAppFeatures.init(featuresSetup); + this.casesAppFeatures.init(featuresSetup); + this.securityAssistantAppFeatures.init(featuresSetup); + } + + public setAppFeaturesConfigurator(configurator: AppFeaturesConfigurator) { + const securityAppFeaturesConfig = configurator.security(this.experimentalFeatures); + this.securityAppFeatures.setConfig(securityAppFeaturesConfig); + + const casesAppFeaturesConfig = configurator.cases(); + this.casesAppFeatures.setConfig(casesAppFeaturesConfig); + + const securityAssistantAppFeaturesConfig = configurator.securityAssistant(); + this.securityAssistantAppFeatures.setConfig(securityAssistantAppFeaturesConfig); + + this.appFeatures = new Set( + Object.freeze([ + ...securityAppFeaturesConfig.keys(), + ...casesAppFeaturesConfig.keys(), + ...securityAssistantAppFeaturesConfig.keys(), + ]) as readonly AppFeatureKeyType[] + ); + } + + public isEnabled(appFeatureKey: AppFeatureKeyType): boolean { + if (!this.appFeatures) { + throw new Error('AppFeatures has not yet been configured'); + } + return this.appFeatures.has(appFeatureKey); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/app_features_service/cases_privileges.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/cases_privileges.ts new file mode 100644 index 0000000000000..9e863385271b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/cases_privileges.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + createUICapabilities as createCasesUICapabilities, + getApiTags as getCasesApiTags, +} from '@kbn/cases-plugin/common'; +import { + CASES_CONNECTORS_CAPABILITY, + GET_CONNECTORS_CONFIGURE_API_TAG, +} from '@kbn/cases-plugin/common/constants'; + +import { APP_ID } from '../../../common/constants'; + +const originalCasesUiCapabilities = createCasesUICapabilities(); +const originalCasesApiTags = getCasesApiTags(APP_ID); + +export const casesUiCapabilities = { + ...originalCasesUiCapabilities, + all: originalCasesUiCapabilities.all.filter( + (capability) => capability !== CASES_CONNECTORS_CAPABILITY + ), + read: originalCasesUiCapabilities.read.filter( + (capability) => capability !== CASES_CONNECTORS_CAPABILITY + ), +}; + +export const casesApiTags = { + ...originalCasesApiTags, + all: originalCasesApiTags.all.filter( + (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG + ), + read: originalCasesApiTags.read.filter( + (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG + ), +}; diff --git a/x-pack/plugins/security_solution/server/lib/app_features_service/index.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/index.ts new file mode 100644 index 0000000000000..5cec9493aca89 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { AppFeaturesService } from './app_features_service'; diff --git a/x-pack/plugins/security_solution/server/lib/app_features_service/mocks.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/mocks.ts new file mode 100644 index 0000000000000..4f47befce92b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/mocks.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; + +import type { AppFeatureKeys } from '@kbn/security-solution-features'; +import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; +import { allowedExperimentalValues, type ExperimentalFeatures } from '../../../common'; +import { AppFeaturesService } from './app_features_service'; + +const SECURITY_BASE_CONFIG = { + foo: 'foo', +}; + +const CASES_BASE_CONFIG = { + bar: 'bar', +}; + +const ASSISTANT_BASE_CONFIG = { + bar: 'bar', +}; + +jest.mock('@kbn/security-solution-features/app_features', () => ({ + getSecurityFeature: jest.fn(() => ({ + baseKibanaFeature: SECURITY_BASE_CONFIG, + baseKibanaSubFeatureIds: ['subFeature1'], + subFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]), + })), + getCasesFeature: jest.fn(() => ({ + baseKibanaFeature: CASES_BASE_CONFIG, + baseKibanaSubFeatureIds: ['subFeature1'], + subFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]), + })), + getAssistantFeature: jest.fn(() => ({ + baseKibanaFeature: ASSISTANT_BASE_CONFIG, + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map([]), + })), +})); + +export const createAppFeaturesServiceMock = ( + /** What features keys should be enabled. Default is all */ + enabledFeatureKeys: AppFeatureKeys = [...ALL_APP_FEATURE_KEYS], + experimentalFeatures: ExperimentalFeatures = { ...allowedExperimentalValues }, + featuresPluginSetupContract: FeaturesPluginSetup = featuresPluginMock.createSetup(), + logger: Logger = loggingSystemMock.create().get('appFeatureMock') +) => { + const appFeaturesService = new AppFeaturesService(logger, experimentalFeatures); + + appFeaturesService.init(featuresPluginSetupContract); + + if (enabledFeatureKeys) { + appFeaturesService.setAppFeaturesConfigurator({ + security: jest.fn().mockReturnValue( + new Map( + enabledFeatureKeys.map((key) => [ + key, + { + privileges: { + all: { + ui: ['entity-analytics'], + api: [`test-entity-analytics`], + }, + read: { + ui: ['entity-analytics'], + api: [`test-entity-analytics`], + }, + }, + }, + ]) + ) + ), + cases: jest.fn().mockReturnValue( + new Map( + enabledFeatureKeys.map((key) => [ + key, + { + privileges: { + all: { + ui: ['entity-analytics'], + api: [`test-entity-analytics`], + }, + read: { + ui: ['entity-analytics'], + api: [`test-entity-analytics`], + }, + }, + }, + ]) + ) + ), + securityAssistant: jest.fn().mockReturnValue( + new Map( + enabledFeatureKeys.map((key) => [ + key, + { + privileges: { + all: { + ui: ['entity-analytics'], + api: [`test-entity-analytics`], + }, + read: { + ui: ['entity-analytics'], + api: [`test-entity-analytics`], + }, + }, + }, + ]) + ) + ), + }); + } + + return appFeaturesService; +}; diff --git a/x-pack/plugins/security_solution/server/lib/app_features_service/security_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/security_saved_objects.ts new file mode 100644 index 0000000000000..e34009121afb0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/security_saved_objects.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 { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; +import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; +import { savedObjectTypes } from '../../saved_objects'; + +// Same as the saved-object type for rules defined by Cloud Security Posture +const CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE = 'csp_rule'; + +export const securityDefaultSavedObjects = [ + 'exception-list', + EXCEPTION_LIST_NAMESPACE_AGNOSTIC, + DATA_VIEW_SAVED_OBJECT_TYPE, + ...savedObjectTypes, + CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE, +]; diff --git a/x-pack/plugins/security_solution/server/lib/app_features_service/types.ts b/x-pack/plugins/security_solution/server/lib/app_features_service/types.ts new file mode 100644 index 0000000000000..b2d1054985c4c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features_service/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 { AppFeaturesConfig } from '@kbn/security-solution-features'; +import type { + SecuritySubFeatureId, + CasesSubFeatureId, + AssistantSubFeatureId, +} from '@kbn/security-solution-features/keys'; +import type { ExperimentalFeatures } from '../../../common'; + +export interface AppFeaturesConfigurator { + security: (experimentalFlags: ExperimentalFeatures) => AppFeaturesConfig; + cases: () => AppFeaturesConfig; + securityAssistant: () => AppFeaturesConfig; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index a71585308c397..e68e84acf6029 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -16,7 +16,7 @@ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; // See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core. // We cannot import from "../../../../../../actions/server" directly here or we have a really bad memory issue. We cannot add this to the existing mocks we created, this fix must be here. -import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client.mock'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import { listMock } from '@kbn/lists-plugin/server/mocks'; import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index b4a493d7f1312..6c9aad0b202b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -101,33 +101,16 @@ describe('set signal status', () => { ); }); - test('calls "esClient.bulk" with signalIds when ids are defined', async () => { + test('calls "esClient.updateByQuery" with signalIds when ids are defined', async () => { await server.inject( getSetSignalStatusByIdsRequest(), requestContextMock.convertContext(context) ); - expect(context.core.elasticsearch.client.asCurrentUser.bulk).toHaveBeenCalledWith( + expect(context.core.elasticsearch.client.asCurrentUser.updateByQuery).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.arrayContaining([ - { - update: { - _id: 'somefakeid1', - _index: '.alerts-security.alerts-default', - }, - }, - { - script: expect.anything(), - }, - { - update: { - _id: 'somefakeid2', - _index: '.alerts-security.alerts-default', - }, - }, - { - script: expect.anything(), - }, - ]), + body: expect.objectContaining({ + query: { bool: { filter: { terms: { _id: ['somefakeid1', 'somefakeid2'] } } } }, + }), }) ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index 5ceeddad22514..42d4b81ffa703 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -121,17 +121,18 @@ const updateSignalsStatusByIds = async ( spaceId: string, esClient: ElasticsearchClient ) => - esClient.bulk({ + esClient.updateByQuery({ index: `${DEFAULT_ALERTS_INDEX}-${spaceId}`, - refresh: 'wait_for', - body: signalsId.flatMap((signalId) => [ - { - update: { _id: signalId, _index: `${DEFAULT_ALERTS_INDEX}-${spaceId}` }, - }, - { - script: getUpdateSignalStatusScript(status), + refresh: false, + body: { + script: getUpdateSignalStatusScript(status), + query: { + bool: { + filter: { terms: { _id: signalsId } }, + }, }, - ]), + }, + ignore_unavailable: true, }); /** @@ -149,10 +150,7 @@ const updateSignalsStatusByQuery = async ( esClient.updateByQuery({ index: `${DEFAULT_ALERTS_INDEX}-${spaceId}`, conflicts: options.conflicts, - // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html#_refreshing_shards_2 - // Note: Before we tried to use "refresh: wait_for" but I do not think that was available and instead it defaulted to "refresh: true" - // but the tests do not pass with "refresh: false". If at some point a "refresh: wait_for" is implemented, we should use that instead. - refresh: true, + refresh: false, body: { script: getUpdateSignalStatusScript(status), query: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/set_alert_tags_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/set_alert_tags_route.test.ts index 445d81526e85b..ab9d79c53fc3f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/set_alert_tags_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/set_alert_tags_route.test.ts @@ -100,7 +100,7 @@ describe('setAlertTagsRoute', () => { body: getSetAlertTagsRequestMock(['tag-1'], ['tag-2'], ['test-id']), }); - context.core.elasticsearch.client.asCurrentUser.bulk.mockRejectedValue( + context.core.elasticsearch.client.asCurrentUser.updateByQuery.mockRejectedValue( new Error('Test error') ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/set_alert_tags_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/set_alert_tags_route.ts index 02fe1ff73f710..1fc13037e1f9a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/set_alert_tags_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/set_alert_tags_route.ts @@ -54,12 +54,12 @@ export const setAlertTagsRoute = (router: SecuritySolutionPluginRouter) => { const painlessScript = { params: { tagsToAdd, tagsToRemove }, - source: `List newTagsArray = []; + source: `List newTagsArray = []; if (ctx._source["kibana.alert.workflow_tags"] != null) { for (tag in ctx._source["kibana.alert.workflow_tags"]) { if (!params.tagsToRemove.contains(tag)) { newTagsArray.add(tag); - } + } } for (tag in params.tagsToAdd) { if (!newTagsArray.contains(tag)) { @@ -90,9 +90,17 @@ export const setAlertTagsRoute = (router: SecuritySolutionPluginRouter) => { } try { - const body = await esClient.bulk({ - refresh: 'wait_for', - body: bulkUpdateRequest, + const body = await esClient.updateByQuery({ + index: `${DEFAULT_ALERTS_INDEX}-${spaceId}`, + refresh: false, + body: { + script: painlessScript, + query: { + bool: { + filter: { terms: { _id: ids } }, + }, + }, + }, }); return response.ok({ body }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 6897fdaf0b92c..16167acdf51ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -27,7 +27,7 @@ import { requestContextMock } from '../../../routes/__mocks__/request_context'; import { savedObjectsExporterMock } from '@kbn/core-saved-objects-import-export-server-mocks'; import { mockRouter } from '@kbn/core-http-router-server-mocks'; import { Readable } from 'stream'; -import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client.mock'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; const exceptionsClient = getExceptionListClientMock(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index c2fe91251d75d..c9d7f82588c52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -28,7 +28,7 @@ import { mockRouter } from '@kbn/core-http-router-server-mocks'; const exceptionsClient = getExceptionListClientMock(); import type { loggingSystemMock } from '@kbn/core/server/mocks'; import { requestContextMock } from '../../../routes/__mocks__/request_context'; -import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client.mock'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; const connectors = [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts index f6e0e34904c5a..7b161f1ab27f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client.mock'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; import { getImportRulesSchemaMock, webHookConnector, diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.test.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.test.ts index 9a723a5bc0e8b..889b01b1ea6cd 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.test.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.test.ts @@ -9,6 +9,7 @@ import { createOrUpdateComponentTemplate, createOrUpdateIlmPolicy, createOrUpdateIndexTemplate, + getDataStreamAdapter, } from '@kbn/alerting-plugin/server'; import { loggingSystemMock, @@ -63,6 +64,7 @@ jest.mock('@kbn/alerting-plugin/server', () => ({ createOrUpdateComponentTemplate: jest.fn(), createOrUpdateIlmPolicy: jest.fn(), createOrUpdateIndexTemplate: jest.fn(), + getDataStreamAdapter: jest.fn(), })); jest.mock('./utils/create_datastream', () => ({ @@ -81,237 +83,253 @@ jest.spyOn(transforms, 'createTransform').mockResolvedValue(Promise.resolve()); jest.spyOn(transforms, 'startTransform').mockResolvedValue(Promise.resolve()); describe('RiskEngineDataClient', () => { - let riskEngineDataClient: RiskEngineDataClient; - let mockSavedObjectClient: ReturnType; - let logger: ReturnType; - const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; - const totalFieldsLimit = 1000; - - beforeEach(() => { - logger = loggingSystemMock.createLogger(); - mockSavedObjectClient = savedObjectsClientMock.create(); - const options = { - logger, - kibanaVersion: '8.9.0', - esClient, - soClient: mockSavedObjectClient, - namespace: 'default', - }; - riskEngineDataClient = new RiskEngineDataClient(options); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('getWriter', () => { - it('should return a writer object', async () => { - const writer = await riskEngineDataClient.getWriter({ namespace: 'default' }); - expect(writer).toBeDefined(); - expect(typeof writer?.bulk).toBe('function'); - }); - - it('should cache and return the same writer for the same namespace', async () => { - const writer1 = await riskEngineDataClient.getWriter({ namespace: 'default' }); - const writer2 = await riskEngineDataClient.getWriter({ namespace: 'default' }); - const writer3 = await riskEngineDataClient.getWriter({ namespace: 'space-1' }); + for (const useDataStreamForAlerts of [false, true]) { + const label = useDataStreamForAlerts ? 'data streams' : 'aliases'; - expect(writer1).toEqual(writer2); - expect(writer2).not.toEqual(writer3); - }); - }); - - describe('initializeResources success', () => { - it('should initialize risk engine resources', async () => { - await riskEngineDataClient.initializeResources({ namespace: 'default' }); - - expect(createOrUpdateIlmPolicy).toHaveBeenCalledWith({ - logger, - esClient, - name: '.risk-score-ilm-policy', - policy: { - _meta: { - managed: true, - }, - phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_primary_shard_size: '50gb', - }, - }, - }, - }, - }, - }); + describe(`using ${label} for alert indices`, () => { + let riskEngineDataClient: RiskEngineDataClient; + let mockSavedObjectClient: ReturnType; + let logger: ReturnType; + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + const totalFieldsLimit = 1000; - expect(createOrUpdateComponentTemplate).toHaveBeenCalledWith( - expect.objectContaining({ + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + mockSavedObjectClient = savedObjectsClientMock.create(); + const options = { logger, + kibanaVersion: '8.9.0', esClient, - template: expect.objectContaining({ - name: '.risk-score-mappings', - _meta: { - managed: true, - }, - }), - totalFieldsLimit: 1000, - }) - ); - expect((createOrUpdateComponentTemplate as jest.Mock).mock.lastCall[0].template.template) - .toMatchInlineSnapshot(` - Object { - "mappings": Object { - "dynamic": "strict", - "properties": Object { - "@timestamp": Object { - "type": "date", + soClient: mockSavedObjectClient, + namespace: 'default', + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts }), + }; + riskEngineDataClient = new RiskEngineDataClient(options); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getWriter', () => { + it('should return a writer object', async () => { + const writer = await riskEngineDataClient.getWriter({ namespace: 'default' }); + expect(writer).toBeDefined(); + expect(typeof writer?.bulk).toBe('function'); + }); + + it('should cache and return the same writer for the same namespace', async () => { + const writer1 = await riskEngineDataClient.getWriter({ namespace: 'default' }); + const writer2 = await riskEngineDataClient.getWriter({ namespace: 'default' }); + const writer3 = await riskEngineDataClient.getWriter({ namespace: 'space-1' }); + + expect(writer1).toEqual(writer2); + expect(writer2).not.toEqual(writer3); + }); + }); + + describe('initializeResources success', () => { + it('should initialize risk engine resources', async () => { + await riskEngineDataClient.initializeResources({ namespace: 'default' }); + + expect(getDataStreamAdapter).toHaveBeenCalledWith({ useDataStreamForAlerts }); + + expect(createOrUpdateIlmPolicy).toHaveBeenCalledWith({ + logger, + esClient, + name: '.risk-score-ilm-policy', + policy: { + _meta: { + managed: true, }, - "host": Object { + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_primary_shard_size: '50gb', + }, + }, + }, + }, + }, + }); + + expect(createOrUpdateComponentTemplate).toHaveBeenCalledWith( + expect.objectContaining({ + logger, + esClient, + template: expect.objectContaining({ + name: '.risk-score-mappings', + _meta: { + managed: true, + }, + }), + totalFieldsLimit: 1000, + }) + ); + expect((createOrUpdateComponentTemplate as jest.Mock).mock.lastCall[0].template.template) + .toMatchInlineSnapshot(` + Object { + "mappings": Object { + "dynamic": "strict", "properties": Object { - "name": Object { - "type": "keyword", + "@timestamp": Object { + "type": "date", }, - "risk": Object { + "host": Object { "properties": Object { - "calculated_level": Object { + "name": Object { "type": "keyword", }, - "calculated_score": Object { - "type": "float", - }, - "calculated_score_norm": Object { - "type": "float", - }, - "category_1_count": Object { - "type": "long", - }, - "category_1_score": Object { - "type": "float", - }, - "id_field": Object { - "type": "keyword", - }, - "id_value": Object { - "type": "keyword", - }, - "inputs": Object { + "risk": Object { "properties": Object { - "category": Object { + "calculated_level": Object { "type": "keyword", }, - "description": Object { - "type": "keyword", + "calculated_score": Object { + "type": "float", + }, + "calculated_score_norm": Object { + "type": "float", + }, + "category_1_count": Object { + "type": "long", + }, + "category_1_score": Object { + "type": "float", }, - "id": Object { + "id_field": Object { "type": "keyword", }, - "index": Object { + "id_value": Object { "type": "keyword", }, - "risk_score": Object { - "type": "float", + "inputs": Object { + "properties": Object { + "category": Object { + "type": "keyword", + }, + "description": Object { + "type": "keyword", + }, + "id": Object { + "type": "keyword", + }, + "index": Object { + "type": "keyword", + }, + "risk_score": Object { + "type": "float", + }, + "timestamp": Object { + "type": "date", + }, + }, + "type": "object", }, - "timestamp": Object { - "type": "date", + "notes": Object { + "type": "keyword", }, }, "type": "object", }, - "notes": Object { - "type": "keyword", - }, }, - "type": "object", - }, - }, - }, - "user": Object { - "properties": Object { - "name": Object { - "type": "keyword", }, - "risk": Object { + "user": Object { "properties": Object { - "calculated_level": Object { - "type": "keyword", - }, - "calculated_score": Object { - "type": "float", - }, - "calculated_score_norm": Object { - "type": "float", - }, - "category_1_count": Object { - "type": "long", - }, - "category_1_score": Object { - "type": "float", - }, - "id_field": Object { - "type": "keyword", - }, - "id_value": Object { + "name": Object { "type": "keyword", }, - "inputs": Object { + "risk": Object { "properties": Object { - "category": Object { + "calculated_level": Object { "type": "keyword", }, - "description": Object { - "type": "keyword", + "calculated_score": Object { + "type": "float", + }, + "calculated_score_norm": Object { + "type": "float", + }, + "category_1_count": Object { + "type": "long", + }, + "category_1_score": Object { + "type": "float", }, - "id": Object { + "id_field": Object { "type": "keyword", }, - "index": Object { + "id_value": Object { "type": "keyword", }, - "risk_score": Object { - "type": "float", + "inputs": Object { + "properties": Object { + "category": Object { + "type": "keyword", + }, + "description": Object { + "type": "keyword", + }, + "id": Object { + "type": "keyword", + }, + "index": Object { + "type": "keyword", + }, + "risk_score": Object { + "type": "float", + }, + "timestamp": Object { + "type": "date", + }, + }, + "type": "object", }, - "timestamp": Object { - "type": "date", + "notes": Object { + "type": "keyword", }, }, "type": "object", }, - "notes": Object { - "type": "keyword", - }, }, - "type": "object", }, }, }, - }, - }, - "settings": Object {}, - } - `); - - expect(createOrUpdateIndexTemplate).toHaveBeenCalledWith({ - logger, - esClient, - template: { - name: '.risk-score.risk-score-default-index-template', - body: { - data_stream: { hidden: true }, - index_patterns: ['risk-score.risk-score-default'], - composed_of: ['.risk-score-mappings'], + "settings": Object {}, + } + `); + + expect(createOrUpdateIndexTemplate).toHaveBeenCalledWith({ + logger, + esClient, template: { - settings: { - auto_expand_replicas: '0-1', - hidden: true, - 'index.lifecycle': { - name: '.risk-score-ilm-policy', + name: '.risk-score.risk-score-default-index-template', + body: { + data_stream: { hidden: true }, + index_patterns: ['risk-score.risk-score-default'], + composed_of: ['.risk-score-mappings'], + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.risk-score-ilm-policy', + }, + 'index.mapping.total_fields.limit': totalFieldsLimit, + }, + mappings: { + dynamic: false, + _meta: { + kibana: { + version: '8.9.0', + }, + managed: true, + namespace: 'default', + }, + }, }, - 'index.mapping.total_fields.limit': totalFieldsLimit, - }, - mappings: { - dynamic: false, _meta: { kibana: { version: '8.9.0', @@ -321,552 +339,547 @@ describe('RiskEngineDataClient', () => { }, }, }, - _meta: { - kibana: { - version: '8.9.0', - }, - managed: true, - namespace: 'default', + }); + + expect(createDataStream).toHaveBeenCalledWith({ + logger, + esClient, + totalFieldsLimit, + indexPatterns: { + template: `.risk-score.risk-score-default-index-template`, + alias: `risk-score.risk-score-default`, }, - }, - }, - }); + }); - expect(createDataStream).toHaveBeenCalledWith({ - logger, - esClient, - totalFieldsLimit, - indexPatterns: { - template: `.risk-score.risk-score-default-index-template`, - alias: `risk-score.risk-score-default`, - }, - }); - - expect(createIndex).toHaveBeenCalledWith({ - logger, - esClient, - options: { - index: `risk-score.risk-score-latest-default`, - mappings: { - dynamic: 'strict', - properties: { - '@timestamp': { - type: 'date', - }, - host: { + expect(createIndex).toHaveBeenCalledWith({ + logger, + esClient, + options: { + index: `risk-score.risk-score-latest-default`, + mappings: { + dynamic: 'strict', properties: { - name: { - type: 'keyword', + '@timestamp': { + type: 'date', }, - risk: { + host: { properties: { - calculated_level: { - type: 'keyword', - }, - calculated_score: { - type: 'float', - }, - calculated_score_norm: { - type: 'float', - }, - category_1_count: { - type: 'long', - }, - category_1_score: { - type: 'float', - }, - id_field: { - type: 'keyword', - }, - id_value: { + name: { type: 'keyword', }, - inputs: { + risk: { properties: { - category: { + calculated_level: { type: 'keyword', }, - description: { - type: 'keyword', + calculated_score: { + type: 'float', }, - id: { + calculated_score_norm: { + type: 'float', + }, + category_1_count: { + type: 'long', + }, + category_1_score: { + type: 'float', + }, + id_field: { type: 'keyword', }, - index: { + id_value: { type: 'keyword', }, - risk_score: { - type: 'float', + inputs: { + properties: { + category: { + type: 'keyword', + }, + description: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + index: { + type: 'keyword', + }, + risk_score: { + type: 'float', + }, + timestamp: { + type: 'date', + }, + }, + type: 'object', }, - timestamp: { - type: 'date', + notes: { + type: 'keyword', }, }, type: 'object', }, - notes: { - type: 'keyword', - }, }, - type: 'object', - }, - }, - }, - user: { - properties: { - name: { - type: 'keyword', }, - risk: { + user: { properties: { - calculated_level: { + name: { type: 'keyword', }, - calculated_score: { - type: 'float', - }, - calculated_score_norm: { - type: 'float', - }, - category_1_count: { - type: 'long', - }, - category_1_score: { - type: 'float', - }, - id_field: { - type: 'keyword', - }, - id_value: { - type: 'keyword', - }, - inputs: { + risk: { properties: { - category: { + calculated_level: { type: 'keyword', }, - description: { - type: 'keyword', + calculated_score: { + type: 'float', }, - id: { + calculated_score_norm: { + type: 'float', + }, + category_1_count: { + type: 'long', + }, + category_1_score: { + type: 'float', + }, + id_field: { type: 'keyword', }, - index: { + id_value: { type: 'keyword', }, - risk_score: { - type: 'float', + inputs: { + properties: { + category: { + type: 'keyword', + }, + description: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + index: { + type: 'keyword', + }, + risk_score: { + type: 'float', + }, + timestamp: { + type: 'date', + }, + }, + type: 'object', }, - timestamp: { - type: 'date', + notes: { + type: 'keyword', }, }, type: 'object', }, - notes: { - type: 'keyword', - }, }, - type: 'object', }, }, }, }, - }, - }, - }); - - expect(transforms.createTransform).toHaveBeenCalledWith({ - logger, - esClient, - transform: { - dest: { - index: 'risk-score.risk-score-latest-default', - }, - frequency: '1h', - latest: { - sort: '@timestamp', - unique_key: ['host.name', 'user.name'], - }, - source: { - index: ['risk-score.risk-score-default'], - }, - sync: { - time: { - delay: '2s', - field: '@timestamp', + }); + + expect(transforms.createTransform).toHaveBeenCalledWith({ + logger, + esClient, + transform: { + dest: { + index: 'risk-score.risk-score-latest-default', + }, + frequency: '1h', + latest: { + sort: '@timestamp', + unique_key: ['host.name', 'user.name'], + }, + source: { + index: ['risk-score.risk-score-default'], + }, + sync: { + time: { + delay: '2s', + field: '@timestamp', + }, + }, + transform_id: 'risk_score_latest_transform_default', }, - }, - transform_id: 'risk_score_latest_transform_default', - }, - }); - - expect(transforms.startTransform).toHaveBeenCalledWith({ - esClient, - transformId: 'risk_score_latest_transform_default', - }); - }); - }); - - describe('initializeResources error', () => { - it('should handle errors during initialization', async () => { - const error = new Error('There error'); - (createOrUpdateIlmPolicy as jest.Mock).mockRejectedValue(error); - - try { - await riskEngineDataClient.initializeResources({ namespace: 'default' }); - } catch (e) { - expect(logger.error).toHaveBeenCalledWith( - `Error initializing risk engine resources: ${error.message}` - ); - } - }); - }); - - describe('getStatus', () => { - it('should return initial status', async () => { - const status = await riskEngineDataClient.getStatus({ - namespace: 'default', - }); - expect(status).toEqual({ - isMaxAmountOfRiskEnginesReached: false, - riskEngineStatus: 'NOT_INSTALLED', - legacyRiskEngineStatus: 'NOT_INSTALLED', - }); - }); - - describe('saved object exists and transforms not', () => { - beforeEach(() => { - mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration()); - }); - - it('should return status with enabled true', async () => { - mockSavedObjectClient.find.mockResolvedValue( - getSavedObjectConfiguration({ - enabled: true, - }) - ); + }); - const status = await riskEngineDataClient.getStatus({ - namespace: 'default', - }); - expect(status).toEqual({ - isMaxAmountOfRiskEnginesReached: false, - riskEngineStatus: 'ENABLED', - legacyRiskEngineStatus: 'NOT_INSTALLED', + expect(transforms.startTransform).toHaveBeenCalledWith({ + esClient, + transformId: 'risk_score_latest_transform_default', + }); }); }); - it('should return status with enabled false', async () => { - mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration()); + describe('initializeResources error', () => { + it('should handle errors during initialization', async () => { + const error = new Error('There error'); + (createOrUpdateIlmPolicy as jest.Mock).mockRejectedValueOnce(error); - const status = await riskEngineDataClient.getStatus({ - namespace: 'default', - }); - expect(status).toEqual({ - isMaxAmountOfRiskEnginesReached: false, - riskEngineStatus: 'DISABLED', - legacyRiskEngineStatus: 'NOT_INSTALLED', + try { + await riskEngineDataClient.initializeResources({ namespace: 'default' }); + } catch (e) { + expect(logger.error).toHaveBeenCalledWith( + `Error initializing risk engine resources: ${error.message}` + ); + } }); }); - }); - describe('legacy transforms', () => { - it('should fetch transforms', async () => { - await riskEngineDataClient.getStatus({ - namespace: 'default', + describe('getStatus', () => { + it('should return initial status', async () => { + const status = await riskEngineDataClient.getStatus({ + namespace: 'default', + }); + expect(status).toEqual({ + isMaxAmountOfRiskEnginesReached: false, + riskEngineStatus: 'NOT_INSTALLED', + legacyRiskEngineStatus: 'NOT_INSTALLED', + }); }); - expect(esClient.transform.getTransform).toHaveBeenCalledTimes(4); - expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(1, { - transform_id: 'ml_hostriskscore_pivot_transform_default', - }); - expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(2, { - transform_id: 'ml_hostriskscore_latest_transform_default', - }); - expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(3, { - transform_id: 'ml_userriskscore_pivot_transform_default', - }); - expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(4, { - transform_id: 'ml_userriskscore_latest_transform_default', - }); - }); - - it('should return that legacy transform enabled if at least on transform exist', async () => { - esClient.transform.getTransform.mockResolvedValueOnce(transformsMock); + describe('saved object exists and transforms not', () => { + beforeEach(() => { + mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration()); + }); - const status = await riskEngineDataClient.getStatus({ - namespace: 'default', - }); + it('should return status with enabled true', async () => { + mockSavedObjectClient.find.mockResolvedValue( + getSavedObjectConfiguration({ + enabled: true, + }) + ); - expect(status).toEqual({ - isMaxAmountOfRiskEnginesReached: false, - riskEngineStatus: 'NOT_INSTALLED', - legacyRiskEngineStatus: 'ENABLED', + const status = await riskEngineDataClient.getStatus({ + namespace: 'default', + }); + expect(status).toEqual({ + isMaxAmountOfRiskEnginesReached: false, + riskEngineStatus: 'ENABLED', + legacyRiskEngineStatus: 'NOT_INSTALLED', + }); + }); + + it('should return status with enabled false', async () => { + mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration()); + + const status = await riskEngineDataClient.getStatus({ + namespace: 'default', + }); + expect(status).toEqual({ + isMaxAmountOfRiskEnginesReached: false, + riskEngineStatus: 'DISABLED', + legacyRiskEngineStatus: 'NOT_INSTALLED', + }); + }); }); - esClient.transform.getTransformStats.mockReset(); - }); - }); - }); - - describe('#getConfiguration', () => { - it('retrieves configuration from the saved object', async () => { - mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration()); - - const configuration = await riskEngineDataClient.getConfiguration(); - - expect(mockSavedObjectClient.find).toHaveBeenCalledTimes(1); - - expect(configuration).toEqual({ - enabled: false, - }); - }); - }); + describe('legacy transforms', () => { + it('should fetch transforms', async () => { + await riskEngineDataClient.getStatus({ + namespace: 'default', + }); + + expect(esClient.transform.getTransform).toHaveBeenCalledTimes(4); + expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(1, { + transform_id: 'ml_hostriskscore_pivot_transform_default', + }); + expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(2, { + transform_id: 'ml_hostriskscore_latest_transform_default', + }); + expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(3, { + transform_id: 'ml_userriskscore_pivot_transform_default', + }); + expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(4, { + transform_id: 'ml_userriskscore_latest_transform_default', + }); + }); + + it('should return that legacy transform enabled if at least on transform exist', async () => { + esClient.transform.getTransform.mockResolvedValueOnce(transformsMock); + + const status = await riskEngineDataClient.getStatus({ + namespace: 'default', + }); - describe('enableRiskEngine', () => { - let mockTaskManagerStart: ReturnType; + expect(status).toEqual({ + isMaxAmountOfRiskEnginesReached: false, + riskEngineStatus: 'NOT_INSTALLED', + legacyRiskEngineStatus: 'ENABLED', + }); - beforeEach(() => { - mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration()); - mockTaskManagerStart = taskManagerMock.createStart(); - }); - - it('returns an error if saved object does not exist', async () => { - mockSavedObjectClient.find.mockResolvedValue({ - page: 1, - per_page: 20, - total: 0, - saved_objects: [], + esClient.transform.getTransformStats.mockReset(); + }); + }); }); - await expect( - riskEngineDataClient.enableRiskEngine({ taskManager: mockTaskManagerStart }) - ).rejects.toThrow('Risk engine configuration not found'); - }); - - it('should update saved object attribute', async () => { - await riskEngineDataClient.enableRiskEngine({ taskManager: mockTaskManagerStart }); - - expect(mockSavedObjectClient.update).toHaveBeenCalledWith( - 'risk-engine-configuration', - 'de8ca330-2d26-11ee-bc86-f95bf6192ee6', - { - enabled: true, - }, - { - refresh: 'wait_for', - } - ); - }); + describe('#getConfiguration', () => { + it('retrieves configuration from the saved object', async () => { + mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration()); - describe('if task manager throws an error', () => { - beforeEach(() => { - mockTaskManagerStart.ensureScheduled.mockRejectedValueOnce(new Error('Task Manager error')); - }); + const configuration = await riskEngineDataClient.getConfiguration(); - it('disables the risk engine and re-throws the error', async () => { - await expect( - riskEngineDataClient.enableRiskEngine({ taskManager: mockTaskManagerStart }) - ).rejects.toThrow('Task Manager error'); + expect(mockSavedObjectClient.find).toHaveBeenCalledTimes(1); - expect(mockSavedObjectClient.update).toHaveBeenCalledWith( - 'risk-engine-configuration', - 'de8ca330-2d26-11ee-bc86-f95bf6192ee6', - { + expect(configuration).toEqual({ enabled: false, - }, - { - refresh: 'wait_for', - } - ); + }); + }); }); - }); - }); - describe('disableRiskEngine', () => { - let mockTaskManagerStart: ReturnType; + describe('enableRiskEngine', () => { + let mockTaskManagerStart: ReturnType; - beforeEach(() => { - mockTaskManagerStart = taskManagerMock.createStart(); - }); + beforeEach(() => { + mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration()); + mockTaskManagerStart = taskManagerMock.createStart(); + }); - it('should return error if saved object not exist', async () => { - mockSavedObjectClient.find.mockResolvedValueOnce({ - page: 1, - per_page: 20, - total: 0, - saved_objects: [], - }); + it('returns an error if saved object does not exist', async () => { + mockSavedObjectClient.find.mockResolvedValue({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [], + }); + + await expect( + riskEngineDataClient.enableRiskEngine({ taskManager: mockTaskManagerStart }) + ).rejects.toThrow('Risk engine configuration not found'); + }); - expect.assertions(1); - try { - await riskEngineDataClient.disableRiskEngine({ taskManager: mockTaskManagerStart }); - } catch (e) { - expect(e.message).toEqual('Risk engine configuration not found'); - } - }); + it('should update saved object attribute', async () => { + await riskEngineDataClient.enableRiskEngine({ taskManager: mockTaskManagerStart }); - it('should update saved object attrubute', async () => { - mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration()); - - await riskEngineDataClient.disableRiskEngine({ taskManager: mockTaskManagerStart }); - - expect(mockSavedObjectClient.update).toHaveBeenCalledWith( - 'risk-engine-configuration', - 'de8ca330-2d26-11ee-bc86-f95bf6192ee6', - { - enabled: false, - }, - { - refresh: 'wait_for', - } - ); - }); - }); - - describe('init', () => { - let mockTaskManagerStart: ReturnType; - const initializeResourcesMock = jest.spyOn( - RiskEngineDataClient.prototype, - 'initializeResources' - ); - const enableRiskEngineMock = jest.spyOn(RiskEngineDataClient.prototype, 'enableRiskEngine'); - - const disableLegacyRiskEngineMock = jest.spyOn( - RiskEngineDataClient.prototype, - 'disableLegacyRiskEngine' - ); - beforeEach(() => { - mockTaskManagerStart = taskManagerMock.createStart(); - disableLegacyRiskEngineMock.mockImplementation(() => Promise.resolve(true)); - - initializeResourcesMock.mockImplementation(() => { - return Promise.resolve(); - }); + expect(mockSavedObjectClient.update).toHaveBeenCalledWith( + 'risk-engine-configuration', + 'de8ca330-2d26-11ee-bc86-f95bf6192ee6', + { + enabled: true, + }, + { + refresh: 'wait_for', + } + ); + }); - enableRiskEngineMock.mockImplementation(() => { - return Promise.resolve(getSavedObjectConfiguration().saved_objects[0]); + describe('if task manager throws an error', () => { + beforeEach(() => { + mockTaskManagerStart.ensureScheduled.mockRejectedValueOnce( + new Error('Task Manager error') + ); + }); + + it('disables the risk engine and re-throws the error', async () => { + await expect( + riskEngineDataClient.enableRiskEngine({ taskManager: mockTaskManagerStart }) + ).rejects.toThrow('Task Manager error'); + + expect(mockSavedObjectClient.update).toHaveBeenCalledWith( + 'risk-engine-configuration', + 'de8ca330-2d26-11ee-bc86-f95bf6192ee6', + { + enabled: false, + }, + { + refresh: 'wait_for', + } + ); + }); + }); }); - jest - .spyOn(savedObjectConfig, 'initSavedObjects') - .mockResolvedValue({} as unknown as SavedObject); - }); + describe('disableRiskEngine', () => { + let mockTaskManagerStart: ReturnType; - afterEach(() => { - initializeResourcesMock.mockReset(); - enableRiskEngineMock.mockReset(); - disableLegacyRiskEngineMock.mockReset(); - }); + beforeEach(() => { + mockTaskManagerStart = taskManagerMock.createStart(); + }); - it('success', async () => { - const initResult = await riskEngineDataClient.init({ - namespace: 'default', - taskManager: mockTaskManagerStart, - }); + it('should return error if saved object not exist', async () => { + mockSavedObjectClient.find.mockResolvedValueOnce({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [], + }); + + expect.assertions(1); + try { + await riskEngineDataClient.disableRiskEngine({ taskManager: mockTaskManagerStart }); + } catch (e) { + expect(e.message).toEqual('Risk engine configuration not found'); + } + }); - expect(initResult).toEqual({ - errors: [], - legacyRiskEngineDisabled: true, - riskEngineConfigurationCreated: true, - riskEngineEnabled: true, - riskEngineResourcesInstalled: true, - }); - }); + it('should update saved object attrubute', async () => { + mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration()); - it('should catch error for disableLegacyRiskEngine, but continue', async () => { - disableLegacyRiskEngineMock.mockImplementation(() => { - throw new Error('Error disableLegacyRiskEngineMock'); - }); - const initResult = await riskEngineDataClient.init({ - namespace: 'default', - taskManager: mockTaskManagerStart, - }); + await riskEngineDataClient.disableRiskEngine({ taskManager: mockTaskManagerStart }); - expect(initResult).toEqual({ - errors: ['Error disableLegacyRiskEngineMock'], - legacyRiskEngineDisabled: false, - riskEngineConfigurationCreated: true, - riskEngineEnabled: true, - riskEngineResourcesInstalled: true, + expect(mockSavedObjectClient.update).toHaveBeenCalledWith( + 'risk-engine-configuration', + 'de8ca330-2d26-11ee-bc86-f95bf6192ee6', + { + enabled: false, + }, + { + refresh: 'wait_for', + } + ); + }); }); - }); - it('should catch error for resource init', async () => { - disableLegacyRiskEngineMock.mockImplementationOnce(() => { - throw new Error('Error disableLegacyRiskEngineMock'); - }); + describe('init', () => { + let mockTaskManagerStart: ReturnType; + const initializeResourcesMock = jest.spyOn( + RiskEngineDataClient.prototype, + 'initializeResources' + ); + const enableRiskEngineMock = jest.spyOn(RiskEngineDataClient.prototype, 'enableRiskEngine'); - const initResult = await riskEngineDataClient.init({ - namespace: 'default', - taskManager: mockTaskManagerStart, - }); + const disableLegacyRiskEngineMock = jest.spyOn( + RiskEngineDataClient.prototype, + 'disableLegacyRiskEngine' + ); + beforeEach(() => { + mockTaskManagerStart = taskManagerMock.createStart(); + disableLegacyRiskEngineMock.mockImplementation(() => Promise.resolve(true)); - expect(initResult).toEqual({ - errors: ['Error disableLegacyRiskEngineMock'], - legacyRiskEngineDisabled: false, - riskEngineConfigurationCreated: true, - riskEngineEnabled: true, - riskEngineResourcesInstalled: true, - }); - }); + initializeResourcesMock.mockImplementation(() => { + return Promise.resolve(); + }); - it('should catch error for initializeResources and stop', async () => { - initializeResourcesMock.mockImplementationOnce(() => { - throw new Error('Error initializeResourcesMock'); - }); + enableRiskEngineMock.mockImplementation(() => { + return Promise.resolve(getSavedObjectConfiguration().saved_objects[0]); + }); - const initResult = await riskEngineDataClient.init({ - namespace: 'default', - taskManager: mockTaskManagerStart, - }); + jest + .spyOn(savedObjectConfig, 'initSavedObjects') + .mockResolvedValue({} as unknown as SavedObject); + }); - expect(initResult).toEqual({ - errors: ['Error initializeResourcesMock'], - legacyRiskEngineDisabled: true, - riskEngineConfigurationCreated: false, - riskEngineEnabled: false, - riskEngineResourcesInstalled: false, - }); - }); + afterEach(() => { + initializeResourcesMock.mockReset(); + enableRiskEngineMock.mockReset(); + disableLegacyRiskEngineMock.mockReset(); + }); - it('should catch error for initSavedObjects and stop', async () => { - jest.spyOn(savedObjectConfig, 'initSavedObjects').mockImplementationOnce(() => { - throw new Error('Error initSavedObjects'); - }); + it('success', async () => { + const initResult = await riskEngineDataClient.init({ + namespace: 'default', + taskManager: mockTaskManagerStart, + }); + + expect(initResult).toEqual({ + errors: [], + legacyRiskEngineDisabled: true, + riskEngineConfigurationCreated: true, + riskEngineEnabled: true, + riskEngineResourcesInstalled: true, + }); + }); - const initResult = await riskEngineDataClient.init({ - namespace: 'default', - taskManager: mockTaskManagerStart, - }); + it('should catch error for disableLegacyRiskEngine, but continue', async () => { + disableLegacyRiskEngineMock.mockImplementation(() => { + throw new Error('Error disableLegacyRiskEngineMock'); + }); + const initResult = await riskEngineDataClient.init({ + namespace: 'default', + taskManager: mockTaskManagerStart, + }); + + expect(initResult).toEqual({ + errors: ['Error disableLegacyRiskEngineMock'], + legacyRiskEngineDisabled: false, + riskEngineConfigurationCreated: true, + riskEngineEnabled: true, + riskEngineResourcesInstalled: true, + }); + }); - expect(initResult).toEqual({ - errors: ['Error initSavedObjects'], - legacyRiskEngineDisabled: true, - riskEngineConfigurationCreated: false, - riskEngineEnabled: false, - riskEngineResourcesInstalled: true, - }); - }); + it('should catch error for resource init', async () => { + disableLegacyRiskEngineMock.mockImplementationOnce(() => { + throw new Error('Error disableLegacyRiskEngineMock'); + }); + + const initResult = await riskEngineDataClient.init({ + namespace: 'default', + taskManager: mockTaskManagerStart, + }); + + expect(initResult).toEqual({ + errors: ['Error disableLegacyRiskEngineMock'], + legacyRiskEngineDisabled: false, + riskEngineConfigurationCreated: true, + riskEngineEnabled: true, + riskEngineResourcesInstalled: true, + }); + }); - it('should catch error for enableRiskEngineMock and stop', async () => { - enableRiskEngineMock.mockImplementationOnce(() => { - throw new Error('Error enableRiskEngineMock'); - }); + it('should catch error for initializeResources and stop', async () => { + initializeResourcesMock.mockImplementationOnce(() => { + throw new Error('Error initializeResourcesMock'); + }); + + const initResult = await riskEngineDataClient.init({ + namespace: 'default', + taskManager: mockTaskManagerStart, + }); + + expect(initResult).toEqual({ + errors: ['Error initializeResourcesMock'], + legacyRiskEngineDisabled: true, + riskEngineConfigurationCreated: false, + riskEngineEnabled: false, + riskEngineResourcesInstalled: false, + }); + }); - const initResult = await riskEngineDataClient.init({ - namespace: 'default', - taskManager: mockTaskManagerStart, - }); + it('should catch error for initSavedObjects and stop', async () => { + jest.spyOn(savedObjectConfig, 'initSavedObjects').mockImplementationOnce(() => { + throw new Error('Error initSavedObjects'); + }); + + const initResult = await riskEngineDataClient.init({ + namespace: 'default', + taskManager: mockTaskManagerStart, + }); + + expect(initResult).toEqual({ + errors: ['Error initSavedObjects'], + legacyRiskEngineDisabled: true, + riskEngineConfigurationCreated: false, + riskEngineEnabled: false, + riskEngineResourcesInstalled: true, + }); + }); - expect(initResult).toEqual({ - errors: ['Error enableRiskEngineMock'], - legacyRiskEngineDisabled: true, - riskEngineConfigurationCreated: true, - riskEngineEnabled: false, - riskEngineResourcesInstalled: true, + it('should catch error for enableRiskEngineMock and stop', async () => { + enableRiskEngineMock.mockImplementationOnce(() => { + throw new Error('Error enableRiskEngineMock'); + }); + + const initResult = await riskEngineDataClient.init({ + namespace: 'default', + taskManager: mockTaskManagerStart, + }); + + expect(initResult).toEqual({ + errors: ['Error enableRiskEngineMock'], + legacyRiskEngineDisabled: true, + riskEngineConfigurationCreated: true, + riskEngineEnabled: false, + riskEngineResourcesInstalled: true, + }); + }); }); }); - }); + } }); diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.ts index b66ec12b08d69..a1e463861d4fb 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_engine_data_client.ts @@ -11,6 +11,7 @@ import { createOrUpdateComponentTemplate, createOrUpdateIlmPolicy, createOrUpdateIndexTemplate, + type DataStreamAdapter, } from '@kbn/alerting-plugin/server'; import { mappingFromFieldMap } from '@kbn/alerting-plugin/common'; import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; @@ -69,6 +70,7 @@ interface RiskEngineDataClientOpts { esClient: ElasticsearchClient; namespace: string; soClient: SavedObjectsClientContract; + dataStreamAdapter: DataStreamAdapter; } export class RiskEngineDataClient { @@ -285,6 +287,7 @@ export class RiskEngineDataClient { esClient, name: ilmPolicyName, policy: ilmPolicy, + dataStreamAdapter: this.options.dataStreamAdapter, }), createOrUpdateComponentTemplate({ logger: this.options.logger, diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/tasks/risk_scoring_task.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/tasks/risk_scoring_task.ts index 075f01ac66ea9..b8a9f3325f81d 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/tasks/risk_scoring_task.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/tasks/risk_scoring_task.ts @@ -17,6 +17,7 @@ import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; +import { getDataStreamAdapter } from '@kbn/alerting-plugin/server'; import type { AfterKeys, IdentifierType } from '../../../../common/risk_engine'; import type { StartPlugins } from '../../../plugin'; @@ -63,12 +64,18 @@ export const registerRiskScoringTask = ({ getStartServices().then(([coreStart, _]) => { const esClient = coreStart.elasticsearch.client.asInternalUser; const soClient = buildScopedInternalSavedObjectsClientUnsafe({ coreStart, namespace }); + // the risk engine seems to be using alerts-as-data innards for it's + // own purposes. It appears the client is using ILM, and this won't work + // on serverless, so we hardcode "not using datastreams" here, since that + // code will have to change someday ... + const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: false }); const riskEngineDataClient = new RiskEngineDataClient({ logger, kibanaVersion, esClient, namespace, soClient, + dataStreamAdapter, }); return riskScoreServiceFactory({ diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 35719e21a20e6..65d66e4fde644 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -20,7 +20,7 @@ import type { ILicense } from '@kbn/licensing-plugin/server'; import { turnOffPolicyProtectionsIfNotSupported } from './endpoint/migrations/turn_off_policy_protections'; import { endpointSearchStrategyProvider } from './search_strategy/endpoint'; import { getScheduleNotificationResponseActionsService } from './lib/detection_engine/rule_response_actions/schedule_notification_response_actions'; -import { siemGuideId, siemGuideConfig } from '../common/guided_onboarding/siem_guide_config'; +import { siemGuideConfig, siemGuideId } from '../common/guided_onboarding/siem_guide_config'; import { createEqlAlertType, createIndicatorMatchAlertType, @@ -38,7 +38,7 @@ import { AppClientFactory } from './client'; import type { ConfigType } from './config'; import { createConfig } from './config'; import { initUiSettings } from './ui_settings'; -import { APP_ID, SERVER_APP_ID, DEFAULT_ALERTS_INDEX } from '../common/constants'; +import { APP_ID, DEFAULT_ALERTS_INDEX, SERVER_APP_ID } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerPolicyRoutes } from './endpoint/routes/policy'; import { registerActionRoutes } from './endpoint/routes/actions'; @@ -60,13 +60,13 @@ import type { IRuleMonitoringService } from './lib/detection_engine/rule_monitor import { createRuleMonitoringService } from './lib/detection_engine/rule_monitoring'; import { EndpointMetadataService } from './endpoint/services/metadata'; import type { - CreateRuleOptions, CreateQueryRuleAdditionalOptions, + CreateRuleOptions, } from './lib/detection_engine/rule_types/types'; // eslint-disable-next-line no-restricted-imports import { - legacyRulesNotificationAlertType, legacyIsNotificationAlertExecutor, + legacyRulesNotificationAlertType, } from './lib/detection_engine/rule_actions_legacy'; import { createSecurityRuleTypeWrapper, @@ -77,13 +77,13 @@ import { RequestContextFactory } from './request_context_factory'; import type { ISecuritySolutionPlugin, - SecuritySolutionPluginSetupDependencies, - SecuritySolutionPluginStartDependencies, + PluginInitializerContext, SecuritySolutionPluginCoreSetupDependencies, SecuritySolutionPluginCoreStartDependencies, SecuritySolutionPluginSetup, + SecuritySolutionPluginSetupDependencies, SecuritySolutionPluginStart, - PluginInitializerContext, + SecuritySolutionPluginStartDependencies, } from './plugin_contract'; import { EndpointFleetServicesFactory } from './endpoint/services/fleet'; import { featureUsageService } from './endpoint/services/feature_usage'; @@ -96,7 +96,7 @@ import { ENDPOINT_SEARCH_STRATEGY, } from '../common/endpoint/constants'; -import { AppFeatures } from './lib/app_features'; +import { AppFeaturesService } from './lib/app_features_service/app_features_service'; import { registerRiskScoringTask } from './lib/risk_engine/tasks/risk_scoring_task'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -106,7 +106,7 @@ export class Plugin implements ISecuritySolutionPlugin { private readonly config: ConfigType; private readonly logger: Logger; private readonly appClientFactory: AppClientFactory; - private readonly appFeatures: AppFeatures; + private readonly appFeaturesService: AppFeaturesService; private readonly ruleMonitoringService: IRuleMonitoringService; private readonly endpointAppContextService = new EndpointAppContextService(); @@ -129,7 +129,7 @@ export class Plugin implements ISecuritySolutionPlugin { this.config = serverConfig; this.logger = context.logger.get(); this.appClientFactory = new AppClientFactory(); - this.appFeatures = new AppFeatures(this.logger, this.config.experimentalFeatures); + this.appFeaturesService = new AppFeaturesService(this.logger, this.config.experimentalFeatures); this.ruleMonitoringService = createRuleMonitoringService(this.config, this.logger); this.telemetryEventsSender = new TelemetryEventsSender(this.logger); @@ -153,12 +153,12 @@ export class Plugin implements ISecuritySolutionPlugin { ): SecuritySolutionPluginSetup { this.logger.debug('plugin setup'); - const { appClientFactory, appFeatures, pluginContext, config, logger } = this; + const { appClientFactory, appFeaturesService, pluginContext, config, logger } = this; const experimentalFeatures = config.experimentalFeatures; initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings, experimentalFeatures); - appFeatures.init(plugins.features); + appFeaturesService.init(plugins.features); this.ruleMonitoringService.setup(core, plugins); @@ -190,6 +190,7 @@ export class Plugin implements ISecuritySolutionPlugin { this.endpointAppContextService.setup({ securitySolutionRequestContextFactory: requestContextFactory, + cloud: plugins.cloud, }); initUsageCollectors({ @@ -403,7 +404,8 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.guidedOnboarding.registerGuideConfig(siemGuideId, siemGuideConfig); return { - setAppFeatures: this.appFeatures.set.bind(this.appFeatures), + setAppFeaturesConfigurator: + appFeaturesService.setAppFeaturesConfigurator.bind(appFeaturesService), }; } @@ -411,7 +413,7 @@ export class Plugin implements ISecuritySolutionPlugin { core: SecuritySolutionPluginCoreStartDependencies, plugins: SecuritySolutionPluginStartDependencies ): SecuritySolutionPluginStart { - const { config, logger } = this; + const { config, logger, appFeaturesService } = this; this.ruleMonitoringService.start(core, plugins); @@ -467,7 +469,7 @@ export class Plugin implements ISecuritySolutionPlugin { experimentalFeatures: config.experimentalFeatures, packagerTaskPackagePolicyUpdateBatchSize: config.packagerTaskPackagePolicyUpdateBatchSize, esClient: core.elasticsearch.client.asInternalUser, - appFeatures: this.appFeatures, + appFeaturesService, }); // Migrate artifacts to fleet and then start the manifest task after that is done @@ -484,7 +486,7 @@ export class Plugin implements ISecuritySolutionPlugin { turnOffPolicyProtectionsIfNotSupported( core.elasticsearch.client.asInternalUser, endpointFleetServicesFactory.asInternalUser(), - this.appFeatures, + appFeaturesService, logger ); }); @@ -513,13 +515,12 @@ export class Plugin implements ISecuritySolutionPlugin { endpointFleetServicesFactory, security: plugins.security, alerting: plugins.alerting, - config: this.config, + config, cases: plugins.cases, logger, manifestManager, registerIngestCallback, licenseService, - cloud: plugins.cloud, exceptionListsClient: exceptionListClient, registerListsServerExtension: this.lists?.registerExtension, featureUsageService, @@ -531,7 +532,7 @@ export class Plugin implements ISecuritySolutionPlugin { ), createFleetActionsClient, esClient: core.elasticsearch.client.asInternalUser, - appFeatures: this.appFeatures, + appFeaturesService, }); this.telemetryReceiver.start( diff --git a/x-pack/plugins/security_solution/server/plugin_contract.ts b/x-pack/plugins/security_solution/server/plugin_contract.ts index 26dee3fbbb016..0c34bede016f1 100644 --- a/x-pack/plugins/security_solution/server/plugin_contract.ts +++ b/x-pack/plugins/security_solution/server/plugin_contract.ts @@ -41,7 +41,7 @@ import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/ import type { SharePluginStart } from '@kbn/share-plugin/server'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server'; -import type { AppFeatures } from './lib/app_features/app_features'; +import type { AppFeaturesService } from './lib/app_features_service/app_features_service'; export interface SecuritySolutionPluginSetupDependencies { alerting: AlertingPluginSetup; @@ -84,9 +84,9 @@ export interface SecuritySolutionPluginStartDependencies { export interface SecuritySolutionPluginSetup { /** - * Sets the app features that are available to the Security Solution + * Sets the configurations for app features that are available to the Security Solution */ - setAppFeatures: AppFeatures['set']; + setAppFeaturesConfigurator: AppFeaturesService['setAppFeaturesConfigurator']; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index be810fd5ae41e..76a4149e11519 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -139,6 +139,7 @@ export class RequestContextFactory implements IRequestContextFactory { esClient: coreContext.elasticsearch.client.asCurrentUser, soClient: coreContext.savedObjects.client, namespace: getSpaceId(), + dataStreamAdapter: plugins.alerting.getDataStreamAdapter(), }) ), }; diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 9604dae1909ce..5452acb5e6f0c 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -149,7 +149,6 @@ "@kbn/expandable-flyout", "@kbn/securitysolution-grouping", "@kbn/securitysolution-data-table", - "@kbn/saved-objects-management-plugin", "@kbn/core-analytics-server", "@kbn/analytics-client", "@kbn/security-solution-side-nav", @@ -171,6 +170,8 @@ "@kbn/navigation-plugin", "@kbn/core-logging-server-mocks", "@kbn/core-lifecycle-browser", - "@kbn/handlebars" + "@kbn/security-solution-features", + "@kbn/handlebars", + "@kbn/content-management-plugin" ] } diff --git a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx index 930f8443369f8..05af48c280395 100644 --- a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx @@ -6,14 +6,14 @@ */ import { SecurityPageName } from '@kbn/security-solution-plugin/common'; -import type { UpsellingService } from '@kbn/security-solution-plugin/public'; import type { MessageUpsellings, PageUpsellings, SectionUpsellings, UpsellingMessageId, UpsellingSectionId, -} from '@kbn/security-solution-upselling/service/types'; + UpsellingService, +} from '@kbn/security-solution-upselling/service'; import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public'; import React, { lazy } from 'react'; import { UPGRADE_INVESTIGATION_GUIDE } from '@kbn/security-solution-upselling/messages'; diff --git a/x-pack/plugins/security_solution_ess/server/app_features/cases_app_features_config.ts b/x-pack/plugins/security_solution_ess/server/app_features/cases_app_features_config.ts new file mode 100644 index 0000000000000..58dcaee8f94ff --- /dev/null +++ b/x-pack/plugins/security_solution_ess/server/app_features/cases_app_features_config.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + AppFeatureKibanaConfig, + AppFeaturesCasesConfig, + AppFeatureKeys, +} from '@kbn/security-solution-features'; +import type { AppFeatureCasesKey, CasesSubFeatureId } from '@kbn/security-solution-features/keys'; +import { + getCasesDefaultAppFeaturesConfig, + createEnabledAppFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; + +import { + CASES_CONNECTORS_CAPABILITY, + GET_CONNECTORS_CONFIGURE_API_TAG, +} from '@kbn/cases-plugin/common/constants'; + +export const getCasesAppFeaturesConfigurator = + (enabledAppFeatureKeys: AppFeatureKeys) => (): AppFeaturesCasesConfig => { + return createEnabledAppFeaturesConfigMap(casesAppFeaturesConfig, enabledAppFeatureKeys); + }; + +/** + * Maps the AppFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security Cases app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security Cases feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Cases subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Cases subFeature with the privilege `id` specified. + */ +const casesAppFeaturesConfig: Record< + AppFeatureCasesKey, + AppFeatureKibanaConfig +> = { + ...getCasesDefaultAppFeaturesConfig({ + apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, + uiCapabilities: { connectors: CASES_CONNECTORS_CAPABILITY }, + }), + // ess-specific app features configs here +}; diff --git a/x-pack/plugins/security_solution_ess/server/app_features/index.ts b/x-pack/plugins/security_solution_ess/server/app_features/index.ts new file mode 100644 index 0000000000000..95c5bea8f05df --- /dev/null +++ b/x-pack/plugins/security_solution_ess/server/app_features/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AppFeatureKeys } from '@kbn/security-solution-features'; +import type { AppFeaturesConfigurator } from '@kbn/security-solution-plugin/server/lib/app_features_service/types'; +import { getCasesAppFeaturesConfigurator } from './cases_app_features_config'; +import { getSecurityAppFeaturesConfigurator } from './security_app_features_config'; +import { getSecurityAssistantAppFeaturesConfigurator } from './security_assistant_app_features_config'; + +export const getProductAppFeaturesConfigurator = ( + enabledAppFeatureKeys: AppFeatureKeys +): AppFeaturesConfigurator => { + return { + security: getSecurityAppFeaturesConfigurator(enabledAppFeatureKeys), + cases: getCasesAppFeaturesConfigurator(enabledAppFeatureKeys), + securityAssistant: getSecurityAssistantAppFeaturesConfigurator(enabledAppFeatureKeys), + }; +}; diff --git a/x-pack/plugins/security_solution_ess/server/app_features/security_app_features_config.ts b/x-pack/plugins/security_solution_ess/server/app_features/security_app_features_config.ts new file mode 100644 index 0000000000000..6badb63d30ed1 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/server/app_features/security_app_features_config.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { ExperimentalFeatures } from '@kbn/security-solution-plugin/common'; +import type { + AppFeatureKeys, + AppFeatureKibanaConfig, + AppFeaturesSecurityConfig, +} from '@kbn/security-solution-features'; +import { + AppFeatureSecurityKey, + type SecuritySubFeatureId, +} from '@kbn/security-solution-features/keys'; +import { + securityDefaultAppFeaturesConfig, + createEnabledAppFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import { + AppFeaturesPrivilegeId, + AppFeaturesPrivileges, +} from '@kbn/security-solution-features/privileges'; + +export const getSecurityAppFeaturesConfigurator = + (enabledAppFeatureKeys: AppFeatureKeys) => + ( + _: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use + ): AppFeaturesSecurityConfig => { + return createEnabledAppFeaturesConfigMap(securityAppFeaturesConfig, enabledAppFeatureKeys); + }; + +/** + * Maps the AppFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. + */ +const securityAppFeaturesConfig: Record< + AppFeatureSecurityKey, + AppFeatureKibanaConfig +> = { + ...securityDefaultAppFeaturesConfig, + [AppFeatureSecurityKey.endpointExceptions]: { + privileges: AppFeaturesPrivileges[AppFeaturesPrivilegeId.endpointExceptions], + }, +}; diff --git a/x-pack/plugins/security_solution_ess/server/app_features/security_assistant_app_features_config.ts b/x-pack/plugins/security_solution_ess/server/app_features/security_assistant_app_features_config.ts new file mode 100644 index 0000000000000..b17ebf859a29a --- /dev/null +++ b/x-pack/plugins/security_solution_ess/server/app_features/security_assistant_app_features_config.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + AppFeatureKeys, + AppFeatureKibanaConfig, + AppFeaturesAssistantConfig, +} from '@kbn/security-solution-features'; +import { + assistantDefaultAppFeaturesConfig, + createEnabledAppFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { + AppFeatureAssistantKey, + AssistantSubFeatureId, +} from '@kbn/security-solution-features/keys'; + +export const getSecurityAssistantAppFeaturesConfigurator = + (enabledAppFeatureKeys: AppFeatureKeys) => (): AppFeaturesAssistantConfig => { + return createEnabledAppFeaturesConfigMap(assistantAppFeaturesConfig, enabledAppFeatureKeys); + }; + +/** + * Maps the AppFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security Assistant app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security Assistant feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Assistant subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Assistant subFeature with the privilege `id` specified. + */ +const assistantAppFeaturesConfig: Record< + AppFeatureAssistantKey, + AppFeatureKibanaConfig +> = { + ...assistantDefaultAppFeaturesConfig, + // ess-specific app features configs here +}; diff --git a/x-pack/plugins/security_solution_ess/server/constants.ts b/x-pack/plugins/security_solution_ess/server/constants.ts index f86558aa1f0bb..4c16f0d14b951 100644 --- a/x-pack/plugins/security_solution_ess/server/constants.ts +++ b/x-pack/plugins/security_solution_ess/server/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-plugin/common'; +import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; // Just copying all feature keys for now. // We may need a different set of keys in the future if we create serverless-specific appFeatures diff --git a/x-pack/plugins/security_solution_ess/server/plugin.ts b/x-pack/plugins/security_solution_ess/server/plugin.ts index 0083fe1314cfd..784df48988774 100644 --- a/x-pack/plugins/security_solution_ess/server/plugin.ts +++ b/x-pack/plugins/security_solution_ess/server/plugin.ts @@ -6,6 +6,7 @@ */ import type { Plugin, CoreSetup } from '@kbn/core/server'; +import { getProductAppFeaturesConfigurator } from './app_features'; import { DEFAULT_APP_FEATURES } from './constants'; import type { @@ -25,7 +26,8 @@ export class SecuritySolutionEssPlugin > { public setup(_coreSetup: CoreSetup, pluginsSetup: SecuritySolutionEssPluginSetupDeps) { - pluginsSetup.securitySolution.setAppFeatures(DEFAULT_APP_FEATURES); + const appFeaturesConfigurator = getProductAppFeaturesConfigurator(DEFAULT_APP_FEATURES); + pluginsSetup.securitySolution.setAppFeaturesConfigurator(appFeaturesConfigurator); return {}; } diff --git a/x-pack/plugins/security_solution_ess/tsconfig.json b/x-pack/plugins/security_solution_ess/tsconfig.json index 57c520bf896c1..0d18f6a324785 100644 --- a/x-pack/plugins/security_solution_ess/tsconfig.json +++ b/x-pack/plugins/security_solution_ess/tsconfig.json @@ -19,6 +19,8 @@ "@kbn/i18n", "@kbn/cloud-experiments-plugin", "@kbn/kibana-react-plugin", + "@kbn/security-solution-features", + "@kbn/cases-plugin", "@kbn/security-solution-navigation", "@kbn/licensing-plugin", "@kbn/security-solution-upselling", diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts index 6122c65aa5de7..1ef3da121b910 100644 --- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { AppFeatureKey, type AppFeatureKeys } from '@kbn/security-solution-plugin/common'; +import type { AppFeatureKeys } from '@kbn/security-solution-features'; +import { AppFeatureKey } from '@kbn/security-solution-features/keys'; import type { SecurityProductLine, SecurityProductTier } from '../config'; type PliAppFeatures = Readonly< @@ -24,7 +25,11 @@ export const PLI_APP_FEATURES: PliAppFeatures = { ], }, endpoint: { - essentials: [AppFeatureKey.endpointPolicyProtections, AppFeatureKey.endpointArtifactManagement], + essentials: [ + AppFeatureKey.endpointPolicyProtections, + AppFeatureKey.endpointArtifactManagement, + AppFeatureKey.endpointExceptions, + ], complete: [ AppFeatureKey.endpointResponseActions, AppFeatureKey.osqueryAutomatedResponseActions, diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_features.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_features.ts index 5db2945f512e8..cd20bc6681b3f 100644 --- a/x-pack/plugins/security_solution_serverless/common/pli/pli_features.ts +++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_features.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { AppFeatureKeys } from '@kbn/security-solution-plugin/common'; +import type { AppFeatureKeys } from '@kbn/security-solution-features/src/types'; import type { SecurityProductTypes } from '../config'; import { ProductTier } from '../product'; import { PLI_APP_FEATURES } from './pli_config'; diff --git a/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/dashboard_landing_callout.tsx b/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/dashboard_landing_callout.tsx new file mode 100644 index 0000000000000..43102f0e50ae3 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/dashboard_landing_callout.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 { EuiCallOut } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { LinkAnchor } from '@kbn/security-solution-navigation/links'; +import { css } from '@emotion/react'; +import { ExternalPageName } from '../../navigation/links/constants'; + +const linkAnchorCss = css` + text-decoration: underline; +`; + +export const DashboardsLandingCalloutComponent: React.FC = () => { + return ( + + + + + ), + visualizationLibrary: ( + + + + ), + }} + /> + + } + /> + ); +}; + +DashboardsLandingCalloutComponent.displayName = 'DashboardsLandingCalloutComponent'; +export const DashboardsLandingCallout = React.memo(DashboardsLandingCalloutComponent); + +// eslint-disable-next-line import/no-default-export +export default DashboardsLandingCallout; diff --git a/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/index.tsx b/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/index.tsx new file mode 100644 index 0000000000000..207a6f122c98e --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Services } from '../../common/services'; +import { ServicesProvider } from '../../common/services'; + +import { DashboardsLandingCallout } from './lazy'; + +export const getDashboardsLandingCallout = (services: Services): React.ComponentType => + function DashboardsLandingCalloutComponent() { + return ( + + + + ); + }; diff --git a/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/lazy.tsx b/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/lazy.tsx new file mode 100644 index 0000000000000..80ac8eacd281e --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/components/dashboards_landing_callout/lazy.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +const DashboardsLandingCalloutLazy = lazy(() => import('./dashboard_landing_callout')); + +export const DashboardsLandingCallout = () => ( + }> + + +); diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/constants.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/constants.ts index 0d0b606976bfe..9c6073c028731 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/constants.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/constants.ts @@ -32,6 +32,9 @@ export const SecurityPagePath = { export enum ExternalPageName { // Osquery osquery = 'osquery:', + // Analytics + maps = 'maps:', + visualize = 'visualize:', // Machine Learning // Ref: packages/default-nav/ml/default_navigation.ts mlOverview = 'ml:overview', diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts index 039ffcfda7dd2..643ec2e905298 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts @@ -53,10 +53,17 @@ export const projectSettingsNavCategories: ProjectLinkCategory[] = [ linkIds: [ ExternalPageName.cloudUsersAndRoles, ExternalPageName.cloudBilling, - ExternalPageName.integrationsSecurity, SecurityPageName.entityAnalyticsManagement, ], }, + { + type: LinkCategoryType.separator, + linkIds: [ + ExternalPageName.integrationsSecurity, + ExternalPageName.maps, + ExternalPageName.visualize, + ], + }, { type: LinkCategoryType.accordion, label: i18n.MANAGEMENT_CATEGORY_TITLE, @@ -181,4 +188,16 @@ export const projectSettingsNavLinks: ProjectNavigationLink[] = [ id: ExternalPageName.managementSettings, title: i18n.MANAGEMENT_SETTINGS_TITLE, }, + { + id: ExternalPageName.maps, + title: i18n.MAPS_TITLE, + description: i18n.MAPS_DESCRIPTION, + landingIcon: IconGraphLazy, + }, + { + id: ExternalPageName.visualize, + title: i18n.VISUALIZE_TITLE, + description: i18n.VISUALIZE_DESCRIPTION, + landingIcon: IconGraphLazy, + }, ]; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_translations.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_translations.ts index 036c3a350a74f..56ef414457dfa 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_translations.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_translations.ts @@ -174,3 +174,28 @@ export const OTHER_CATEGORY_TITLE = i18n.translate( defaultMessage: 'OTHER', } ); +export const MAPS_TITLE = i18n.translate( + 'xpack.securitySolutionServerless.navLinks.projectSettings.maps.title', + { + defaultMessage: 'Maps', + } +); +export const MAPS_DESCRIPTION = i18n.translate( + 'xpack.securitySolutionServerless.navLinks.projectSettings.maps.description', + { + defaultMessage: 'Plot geographic data', + } +); +export const VISUALIZE_TITLE = i18n.translate( + 'xpack.securitySolutionServerless.navLinks.projectSettings.visualize.title', + { + defaultMessage: 'Visualize library', + } +); + +export const VISUALIZE_DESCRIPTION = i18n.translate( + 'xpack.securitySolutionServerless.navLinks.projectSettings.visualize.description', + { + defaultMessage: 'Visualize library page', + } +); diff --git a/x-pack/plugins/security_solution_serverless/public/pages/project_settings.tsx b/x-pack/plugins/security_solution_serverless/public/pages/project_settings.tsx index 9c3f8e5012a67..71809249b266e 100644 --- a/x-pack/plugins/security_solution_serverless/public/pages/project_settings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/pages/project_settings.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { EuiHorizontalRule, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { - LandingLinksIcons, + LandingLinksIconsCategories, LandingLinksIconsCategoriesGroups, } from '@kbn/security-solution-navigation/landing_links'; -import type { AccordionLinkCategory } from '@kbn/security-solution-navigation'; +import type { AccordionLinkCategory, NavigationLink } from '@kbn/security-solution-navigation'; import { isAccordionLinkCategory, isSeparatorLinkCategory, @@ -25,13 +25,21 @@ export const ProjectSettingsRoute: React.FC = () => { const projectSettingsLink = useNavLink(SecurityPageName.projectSettings); const { links = [], categories = [], title } = projectSettingsLink ?? {}; - const iconLinkIds = - categories.find((category) => isSeparatorLinkCategory(category))?.linkIds ?? []; - const iconLinks = links.filter(({ id }) => iconLinkIds.includes(id)); + const iconLinks = categories.reduce((acc, category) => { + if (isSeparatorLinkCategory(category)) { + const categoryLinks = links.filter(({ id }) => category.linkIds.includes(id)); + + acc.push(...categoryLinks); + } + return acc; + }, []); const accordionCategories = (categories.filter((category) => isAccordionLinkCategory(category)) ?? []) as AccordionLinkCategory[]; + const separatorCategories = (categories.filter((category) => isSeparatorLinkCategory(category)) ?? + []) as AccordionLinkCategory[]; + return ( @@ -39,7 +47,7 @@ export const ProjectSettingsRoute: React.FC = () => { - + diff --git a/x-pack/plugins/security_solution_serverless/public/plugin.ts b/x-pack/plugins/security_solution_serverless/public/plugin.ts index 05ea3185ce43c..b562e16293c66 100644 --- a/x-pack/plugins/security_solution_serverless/public/plugin.ts +++ b/x-pack/plugins/security_solution_serverless/public/plugin.ts @@ -8,6 +8,7 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { getSecurityGetStartedComponent } from './get_started'; +import { getDashboardsLandingCallout } from './components/dashboards_landing_callout'; import type { SecuritySolutionServerlessPluginSetup, SecuritySolutionServerlessPluginStart, @@ -57,6 +58,7 @@ export class SecuritySolutionServerlessPlugin registerUpsellings(securitySolution.getUpselling(), this.config.productTypes, services); securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes)); + securitySolution.setDashboardsLandingCallout(getDashboardsLandingCallout(services)); securitySolution.setIsILMAvailable(false); configureNavigation(services, this.config); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx index 6c886c681d0aa..cc045caf3cc21 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx @@ -5,8 +5,6 @@ * 2.0. */ -import type { UpsellingService } from '@kbn/security-solution-plugin/public'; -import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-plugin/common'; import { registerUpsellings, upsellingMessages, @@ -15,6 +13,9 @@ import { } from './register_upsellings'; import { ProductLine, ProductTier } from '../../common/product'; import type { SecurityProductTypes } from '../../common/config'; +import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; +import type { UpsellingService } from '@kbn/security-solution-upselling/service'; +import { mockServices } from '../common/services/__mocks__/services.mock'; const mockGetProductAppFeatures = jest.fn(); jest.mock('../../common/pli/pli_features', () => ({ @@ -40,7 +41,7 @@ describe('registerUpsellings', () => { setMessages, } as unknown as UpsellingService; - registerUpsellings(upselling, allProductTypes); + registerUpsellings(upselling, allProductTypes, mockServices); expect(setPages).toHaveBeenCalledTimes(1); expect(setPages).toHaveBeenCalledWith({}); @@ -65,7 +66,7 @@ describe('registerUpsellings', () => { setMessages, } as unknown as UpsellingService; - registerUpsellings(upselling, allProductTypes); + registerUpsellings(upselling, allProductTypes, mockServices); const expectedPagesObject = Object.fromEntries( upsellingPages.map(({ pageName }) => [pageName, expect.anything()]) diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx index ee8427717ad01..f1b8da6b1557d 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx @@ -4,38 +4,38 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { SecurityPageName, AppFeatureKey } from '@kbn/security-solution-plugin/common'; +import { SecurityPageName } from '@kbn/security-solution-plugin/common'; import type { - UpsellingService, + MessageUpsellings, PageUpsellings, SectionUpsellings, - UpsellingSectionId, -} from '@kbn/security-solution-plugin/public'; -import type { - MessageUpsellings, UpsellingMessageId, + UpsellingSectionId, } from '@kbn/security-solution-upselling/service/types'; +import type { UpsellingService } from '@kbn/security-solution-upselling/service'; import React from 'react'; import { UPGRADE_INVESTIGATION_GUIDE } from '@kbn/security-solution-upselling/messages'; +import { AppFeatureKey } from '@kbn/security-solution-features/keys'; +import type { AppFeatureKeyType } from '@kbn/security-solution-features'; import { EndpointPolicyProtectionsLazy } from './sections/endpoint_management'; import type { SecurityProductTypes } from '../../common/config'; import { getProductAppFeatures } from '../../common/pli/pli_features'; import { + EntityAnalyticsUpsellingLazy, OsqueryResponseActionsUpsellingSectionLazy, ThreatIntelligencePaywallLazy, - EntityAnalyticsUpsellingLazy, } from './lazy_upselling'; import { getProductTypeByPLI } from './hooks/use_product_type_by_pli'; import type { Services } from '../common/services'; import { withServicesProvider } from '../common/services'; interface UpsellingsConfig { - pli: AppFeatureKey; + pli: AppFeatureKeyType; component: React.ComponentType; } interface UpsellingsMessageConfig { - pli: AppFeatureKey; + pli: AppFeatureKeyType; message: string; id: UpsellingMessageId; } diff --git a/x-pack/plugins/security_solution_serverless/server/app_features/cases_app_features_config.ts b/x-pack/plugins/security_solution_serverless/server/app_features/cases_app_features_config.ts new file mode 100644 index 0000000000000..7e62866816cc3 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/app_features/cases_app_features_config.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + AppFeatureKibanaConfig, + AppFeaturesCasesConfig, + AppFeatureKeys, +} from '@kbn/security-solution-features'; +import { + getCasesDefaultAppFeaturesConfig, + createEnabledAppFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { AppFeatureCasesKey, CasesSubFeatureId } from '@kbn/security-solution-features/keys'; +import { + CASES_CONNECTORS_CAPABILITY, + GET_CONNECTORS_CONFIGURE_API_TAG, +} from '@kbn/cases-plugin/common/constants'; + +export const getCasesAppFeaturesConfigurator = + (enabledAppFeatureKeys: AppFeatureKeys) => (): AppFeaturesCasesConfig => { + return createEnabledAppFeaturesConfigMap(casesAppFeaturesConfig, enabledAppFeatureKeys); + }; + +/** + * Maps the AppFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security Cases app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security Cases feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Cases subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Cases subFeature with the privilege `id` specified. + */ +const casesAppFeaturesConfig: Record< + AppFeatureCasesKey, + AppFeatureKibanaConfig +> = { + ...getCasesDefaultAppFeaturesConfig({ + apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, + uiCapabilities: { connectors: CASES_CONNECTORS_CAPABILITY }, + }), + // serverless-specific app features configs here +}; diff --git a/x-pack/plugins/security_solution_serverless/server/app_features/index.ts b/x-pack/plugins/security_solution_serverless/server/app_features/index.ts new file mode 100644 index 0000000000000..95c5bea8f05df --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/app_features/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AppFeatureKeys } from '@kbn/security-solution-features'; +import type { AppFeaturesConfigurator } from '@kbn/security-solution-plugin/server/lib/app_features_service/types'; +import { getCasesAppFeaturesConfigurator } from './cases_app_features_config'; +import { getSecurityAppFeaturesConfigurator } from './security_app_features_config'; +import { getSecurityAssistantAppFeaturesConfigurator } from './security_assistant_app_features_config'; + +export const getProductAppFeaturesConfigurator = ( + enabledAppFeatureKeys: AppFeatureKeys +): AppFeaturesConfigurator => { + return { + security: getSecurityAppFeaturesConfigurator(enabledAppFeatureKeys), + cases: getCasesAppFeaturesConfigurator(enabledAppFeatureKeys), + securityAssistant: getSecurityAssistantAppFeaturesConfigurator(enabledAppFeatureKeys), + }; +}; diff --git a/x-pack/plugins/security_solution_serverless/server/app_features/security_app_features_config.ts b/x-pack/plugins/security_solution_serverless/server/app_features/security_app_features_config.ts new file mode 100644 index 0000000000000..a18e9c39d2f5a --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/app_features/security_app_features_config.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { ExperimentalFeatures } from '@kbn/security-solution-plugin/common'; +import type { + AppFeatureKeys, + AppFeatureKibanaConfig, + AppFeaturesSecurityConfig, +} from '@kbn/security-solution-features'; +import { + securityDefaultAppFeaturesConfig, + createEnabledAppFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import { AppFeatureSecurityKey, SecuritySubFeatureId } from '@kbn/security-solution-features/keys'; + +export const getSecurityAppFeaturesConfigurator = + (enabledAppFeatureKeys: AppFeatureKeys) => + ( + _: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use + ): AppFeaturesSecurityConfig => { + return createEnabledAppFeaturesConfigMap(securityAppFeaturesConfig, enabledAppFeatureKeys); + }; + +/** + * Maps the AppFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. + */ +const securityAppFeaturesConfig: Record< + AppFeatureSecurityKey, + AppFeatureKibanaConfig +> = { + ...securityDefaultAppFeaturesConfig, + [AppFeatureSecurityKey.endpointExceptions]: { + subFeatureIds: [SecuritySubFeatureId.endpointExceptions], + }, +}; diff --git a/x-pack/plugins/security_solution_serverless/server/app_features/security_assistant_app_features_config.ts b/x-pack/plugins/security_solution_serverless/server/app_features/security_assistant_app_features_config.ts new file mode 100644 index 0000000000000..8c3971faee45e --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/app_features/security_assistant_app_features_config.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + AppFeatureKeys, + AppFeatureKibanaConfig, + AppFeaturesAssistantConfig, +} from '@kbn/security-solution-features'; +import { + assistantDefaultAppFeaturesConfig, + createEnabledAppFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { + AppFeatureAssistantKey, + AssistantSubFeatureId, +} from '@kbn/security-solution-features/keys'; + +export const getSecurityAssistantAppFeaturesConfigurator = + (enabledAppFeatureKeys: AppFeatureKeys) => (): AppFeaturesAssistantConfig => { + return createEnabledAppFeaturesConfigMap(assistantAppFeaturesConfig, enabledAppFeatureKeys); + }; + +/** + * Maps the AppFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security Assistant app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security Assistant feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Assistant subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Assistant subFeature with the privilege `id` specified. + */ +const assistantAppFeaturesConfig: Record< + AppFeatureAssistantKey, + AppFeatureKibanaConfig +> = { + ...assistantDefaultAppFeaturesConfig, + // serverless-specific app features configs here +}; diff --git a/x-pack/plugins/security_solution_serverless/server/plugin.ts b/x-pack/plugins/security_solution_serverless/server/plugin.ts index f83afd4593e4f..f4937ea4f0b32 100644 --- a/x-pack/plugins/security_solution_serverless/server/plugin.ts +++ b/x-pack/plugins/security_solution_serverless/server/plugin.ts @@ -24,6 +24,7 @@ import type { } from './types'; import { SecurityUsageReportingTask } from './task_manager/usage_reporting_task'; import { cloudSecurityMetringTaskProperties } from './cloud_security/cloud_security_metering_task_config'; +import { getProductAppFeaturesConfigurator } from './app_features'; import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering'; import { endpointMeteringService, @@ -57,7 +58,10 @@ export class SecuritySolutionServerlessPlugin if (shouldRegister) { const productTypesStr = JSON.stringify(this.config.productTypes, null, 2); this.logger.info(`Security Solution running with product types:\n${productTypesStr}`); - pluginsSetup.securitySolution.setAppFeatures(getProductAppFeatures(this.config.productTypes)); + const appFeaturesConfigurator = getProductAppFeaturesConfigurator( + getProductAppFeatures(this.config.productTypes) + ); + pluginsSetup.securitySolution.setAppFeaturesConfigurator(appFeaturesConfigurator); } pluginsSetup.ml.setFeaturesEnabled({ ad: true, dfa: true, nlp: false }); diff --git a/x-pack/plugins/security_solution_serverless/tsconfig.json b/x-pack/plugins/security_solution_serverless/tsconfig.json index 7e3bf59fad132..0f517caa077aa 100644 --- a/x-pack/plugins/security_solution_serverless/tsconfig.json +++ b/x-pack/plugins/security_solution_serverless/tsconfig.json @@ -10,8 +10,6 @@ "public/**/*.tsx", "server/**/*.ts", "../../../typings/**/*" - , - "../../packages/security-solution/upselling/sections/generic_upselling_section.tsx" ], "exclude": ["target/**/*"], "kbn_references": [ @@ -39,6 +37,8 @@ "@kbn/task-manager-plugin", "@kbn/cloud-plugin", "@kbn/cloud-security-posture-plugin", + "@kbn/security-solution-features", + "@kbn/cases-plugin", "@kbn/fleet-plugin", "@kbn/core-elasticsearch-server", "@kbn/usage-collection-plugin" diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx index 34b0e2652b5de..3808a64e3baff 100644 --- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx @@ -31,10 +31,10 @@ const navigationTree: NavigationTreeDefinition = { id: 'discover-dashboard-alerts-slos', children: [ { - title: i18n.translate('xpack.serverlessObservability.nav.discover', { - defaultMessage: 'Discover', + title: i18n.translate('xpack.serverlessObservability.nav.logExplorer', { + defaultMessage: 'Log Explorer', }), - link: 'discover:log-explorer', + link: 'observability-log-explorer', }, { title: i18n.translate('xpack.serverlessObservability.nav.dashboards', { diff --git a/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.scss b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.scss new file mode 100644 index 0000000000000..b74c06cd58030 --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.scss @@ -0,0 +1,3 @@ +.apiKeySuccessPanel { + background-color: transparentize($euiColorSuccess, .9); +} diff --git a/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx index 4351c0777fe80..07675f41e3f07 100644 --- a/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx @@ -14,10 +14,9 @@ import { EuiIcon, EuiPanel, EuiSpacer, - EuiSplitPanel, EuiStep, EuiText, - EuiThemeProvider, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -29,9 +28,10 @@ import { useKibanaServices } from '../../hooks/use_kibana'; import { MANAGEMENT_API_KEYS } from '../../routes'; import { CreateApiKeyFlyout } from './create_api_key_flyout'; import { CreateApiKeyResponse } from './types'; +import './api_key.scss'; export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: string) => void }) => { - const { cloud, http, userProfile } = useKibanaServices(); + const { http, userProfile } = useKibanaServices(); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); const { data } = useQuery({ queryKey: ['apiKey'], @@ -53,7 +53,7 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri /> )} {apiKey ? ( - + ) : ( - - - {i18n.translate('xpack.serverlessSearch.apiKey.stepOneDescription', { - defaultMessage: 'Unique identifier for authentication and authorization. ', + +

+ {i18n.translate('xpack.serverlessSearch.apiKey.panel.title', { + defaultMessage: 'Prepare an API Key', })} - - - - - - - - setIsFlyoutOpen(true)} - > - - {i18n.translate('xpack.serverlessSearch.apiKey.newButtonLabel', { - defaultMessage: 'New', - })} - - - - - - - - {i18n.translate('xpack.serverlessSearch.apiKey.manageLabel', { - defaultMessage: 'Manage', +

+
+ + {i18n.translate('xpack.serverlessSearch.apiKey.panel.description', { + defaultMessage: + 'An API key is a private, unique identifier for authentication and authorization.', + })} + + + + + + + + setIsFlyoutOpen(true)} + > + + {i18n.translate('xpack.serverlessSearch.apiKey.newButtonLabel', { + defaultMessage: 'New', })} - - +
+ + + + + + + {i18n.translate('xpack.serverlessSearch.apiKey.manageLabel', { + defaultMessage: 'Manage', + })} + + + + + + + {!!data?.apiKeys && ( + + + + + + + 0 ? 'success' : 'warning'}> + {data.apiKeys.length} + + ), + }} + /> + - - - {!!data?.apiKeys && ( - - - - - - - 0 ? 'success' : 'warning'}> - {data.apiKeys.length} - - ), - }} - /> - - - - )} - - -
+ )} + +
)} - - - - - - {i18n.translate('xpack.serverlessSearch.apiKey.stepTwoDescription', { - defaultMessage: 'Unique identifier for specific project. ', - })} - - - - - - - {cloud.cloudId} - - - - ); }; diff --git a/x-pack/plugins/serverless_search/public/application/components/overview.scss b/x-pack/plugins/serverless_search/public/application/components/overview.scss index bb7992b3ac51f..72948fb99c1c4 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview.scss +++ b/x-pack/plugins/serverless_search/public/application/components/overview.scss @@ -7,37 +7,6 @@ padding-top: 0; } -.serverlessSearchFooterCardsSection { - background-color: $euiColorPrimary; - padding-top: $euiSizeXL; - padding-bottom: $euiSizeXL; -} - -.serverlessSearchFooterCard { - &--wrapper { - position: relative; - } - - &--background { - z-index: 1; - } - - &--iconWrapper { - align-items: center; - display: inlineFlex; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - justify-content: center; - padding: 20px; - z-index: 2; - } -} - -.serverlessSearchFooter { - background-color: $euiColorSuccess; - padding-top: $euiSizeXL; - padding-bottom: $euiSizeXL; +.serverlessSearchOverviewFooterSection { + background-color: transparentize($euiColorPrimary, .9); } diff --git a/x-pack/plugins/serverless_search/public/application/components/overview.test.tsx b/x-pack/plugins/serverless_search/public/application/components/overview.test.tsx index 971025125ab94..28748db40cedb 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview.test.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/overview.test.tsx @@ -10,7 +10,25 @@ import { ElasticsearchOverview as Overview } from './overview'; describe('', () => { beforeEach(() => { - core.http.fetch.mockResolvedValueOnce({ apiKeys: [] }); + core.http.fetch.mockImplementation((url) => { + let fetchedUrl: string; + if (typeof url === 'string') { + fetchedUrl = url; + } + + return new Promise((resolve, reject) => { + switch (fetchedUrl) { + case '/internal/serverless_search/api_keys': + resolve({ apiKeys: [] }); + return; + case '/internal/serverless_search/connectors': + resolve({}); + return; + default: + return reject(`unknown path requested ${fetchedUrl}`); + } + }); + }); }); test('renders without throwing an error', () => { @@ -33,7 +51,11 @@ describe('', () => { }); test('api key', () => { const { getByRole } = render(); - expect(getByRole('heading', { name: 'Store your API key and Cloud ID' })).toBeDefined(); + expect(getByRole('heading', { level: 2, name: 'Prepare an API Key' })).toBeDefined(); + }); + test('cloud id', () => { + const { getByRole } = render(); + expect(getByRole('heading', { name: 'Copy your connection details' })).toBeDefined(); }); test('configure client', () => { const { getByRole } = render(); @@ -53,6 +75,6 @@ describe('', () => { }); test("what's next?", () => { const { getByRole } = render(); - expect(getByRole('heading', { name: "What's next?" })).toBeDefined(); + expect(getByRole('heading', { name: 'Do more with your data' })).toBeDefined(); }); }); diff --git a/x-pack/plugins/serverless_search/public/application/components/overview.tsx b/x-pack/plugins/serverless_search/public/application/components/overview.tsx index e8ef03a72a1f0..ddcc4e08cce13 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/overview.tsx @@ -6,16 +6,17 @@ */ import { - EuiButton, + EuiAvatar, + EuiButtonEmpty, EuiCard, + EuiCodeBlock, EuiFlexGroup, EuiFlexItem, - EuiImage, - EuiLink, EuiPageTemplate, + EuiPanel, EuiSpacer, EuiText, - EuiTextColor, + EuiThemeProvider, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -52,7 +53,6 @@ export const ElasticsearchOverview = () => { useState(javascriptDefinition); const [clientApiKey, setClientApiKey] = useState(API_KEY_PLACEHOLDER); const { application, cloud, http, userProfile, share } = useKibanaServices(); - const { navigateToApp } = application; const elasticsearchURL = useMemo(() => { return cloud?.elasticsearchUrl ?? ELASTICSEARCH_URL_PLACEHOLDER; @@ -63,7 +63,7 @@ export const ElasticsearchOverview = () => { apiKey: clientApiKey, }; - const { data } = useQuery({ + const { data: _data } = useQuery({ queryKey: ['fetchConnectors'], queryFn: () => http.fetch<{ connectors: Connector[] }>('/internal/serverless_search/connectors'), @@ -111,19 +111,71 @@ export const ElasticsearchOverview = () => { } - links={[ - { - href: docLinks.securityApis, - label: i18n.translate('xpack.serverlessSearch.configureClient.basicConfigLabel', { - defaultMessage: 'Basic configuration', - }), - }, - ]} + links={[]} title={i18n.translate('xpack.serverlessSearch.apiKey.title', { - defaultMessage: 'Store your API key and Cloud ID', + defaultMessage: 'Prepare an API Key', + })} + /> + + + + + +
+ {i18n.translate('xpack.serverlessSearch.cloudIdDetails.id.title', { + defaultMessage: 'Cloud ID', + })} +
+
+ + + + + {cloud.cloudId} + + + +
+ + +
+ {i18n.translate('xpack.serverlessSearch.cloudIdDetails.url.title', { + defaultMessage: 'Cloud URL', + })} +
+
+ + + + + {cloud.elasticsearchUrl} + + + +
+ + } + links={[]} + title={i18n.translate('xpack.serverlessSearch.cloudIdDetails.title', { + defaultMessage: 'Copy your connection details', })} />
@@ -241,203 +293,149 @@ export const ElasticsearchOverview = () => { })} /> - - - - -

- - {i18n.translate('xpack.serverlessSearch.footer.title', { - defaultMessage: "What's next?", - })} - -

-
-
-
- - - - - - navigateToApp('discover')}> - {i18n.translate('xpack.serverlessSearch.footer.discoverCard.buttonText', { - defaultMessage: 'Explore data in Discover', - })} - - - - } - image={ - - } - /> - - - - - - navigateToApp('management', { path: '/ingest/ingest_pipelines' }) - } - > - {i18n.translate('xpack.serverlessSearch.footer.pipelinesCard.buttonText', { - defaultMessage: 'Configure your ingest pipelines', - })} - - - - } - image={ - - } - /> - - - + } + links={[]} + overviewPanelProps={{ color: 'transparent', hasShadow: false }} + /> +
+ + ); +}; + +const OverviewFooter = () => { + const { + application: { navigateToApp }, + cloud, + } = useKibanaServices(); + + return ( + + + + + } + titleSize="xs" + title={i18n.translate('xpack.serverlessSearch.overview.footer.discover.title', { + defaultMessage: 'Discover', + })} + description={i18n.translate( + 'xpack.serverlessSearch.overview.footer.discover.description', + { defaultMessage: - 'Search UI is a free and open source JavaScript library maintained by Elastic for the fast development of modern, engaging search experiences.', - })} - footer={ - - - - {i18n.translate('xpack.serverlessSearch.footer.searchUI.buttonText', { - defaultMessage: 'Build with Search UI', - })} - - - - } - image={ - + 'Search and filter your data, learn how your fields are structured, and create visualizations.', } - /> - - - - - - {cloud.usersAndRolesUrl && ( - - navigateToApp('discover')} + /> + + + - - )} - {cloud.billingUrl && ( - - navigateToApp('management', { path: '/ingest/ingest_pipelines' })} + /> + + + - - )} - - - - - + + + + {cloud.usersAndRolesUrl && ( + + + {i18n.translate('xpack.serverlessSearch.overview.footer.links.inviteUsers', { + defaultMessage: 'Invite more users', })} - /> - - - - + + + )} + + + {i18n.translate('xpack.serverlessSearch.overview.footer.links.community', { + defaultMessage: 'Join our community', + })} + + + + + {i18n.translate('xpack.serverlessSearch.overview.footer.links.feedback', { + defaultMessage: 'Give feedback', + })} + + + + ); }; -const FooterCardImage = ({ - iconSrc, - backgroundSrc, -}: { - iconSrc: string; - backgroundSrc: string; -}) => ( -
- - -
-); - -const FooterIcon = ({ href, imgSrc, title }: { href: string; imgSrc: string; title: string }) => ( - - - - - - - -
{title}
-
-
-
-
+const FooterButtonContainer: React.FC = ({ children }) => ( + + + + {children} + + + ); diff --git a/x-pack/plugins/serverless_search/public/assets/billing_icon.png b/x-pack/plugins/serverless_search/public/assets/billing_icon.png deleted file mode 100644 index 0e315bfb06a3d..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/billing_icon.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/community_icon.png b/x-pack/plugins/serverless_search/public/assets/community_icon.png deleted file mode 100644 index c55e85d1fb208..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/community_icon.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/discover_bg.png b/x-pack/plugins/serverless_search/public/assets/discover_bg.png deleted file mode 100644 index 87ce235b84c7d..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/discover_bg.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/discover_icon.png b/x-pack/plugins/serverless_search/public/assets/discover_icon.png deleted file mode 100644 index 7f2a809a51522..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/discover_icon.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/feedback_icon.png b/x-pack/plugins/serverless_search/public/assets/feedback_icon.png deleted file mode 100644 index 135c6b5910e9f..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/feedback_icon.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/invite_users_icon.png b/x-pack/plugins/serverless_search/public/assets/invite_users_icon.png deleted file mode 100644 index 66bc3ef70f197..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/invite_users_icon.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/searchui_bg.png b/x-pack/plugins/serverless_search/public/assets/searchui_bg.png deleted file mode 100644 index 665b331a1dde5..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/searchui_bg.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/searchui_icon.png b/x-pack/plugins/serverless_search/public/assets/searchui_icon.png deleted file mode 100644 index 9708acc45f53a..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/searchui_icon.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/transform_bg.png b/x-pack/plugins/serverless_search/public/assets/transform_bg.png deleted file mode 100644 index 209921789eb4b..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/transform_bg.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/assets/transform_icon.png b/x-pack/plugins/serverless_search/public/assets/transform_icon.png deleted file mode 100644 index c5980d29bda9d..0000000000000 Binary files a/x-pack/plugins/serverless_search/public/assets/transform_icon.png and /dev/null differ diff --git a/x-pack/plugins/serverless_search/public/layout/nav.tsx b/x-pack/plugins/serverless_search/public/layout/nav.tsx index 65eb1787a07a9..0879f86bc2b73 100644 --- a/x-pack/plugins/serverless_search/public/layout/nav.tsx +++ b/x-pack/plugins/serverless_search/public/layout/nav.tsx @@ -16,6 +16,14 @@ import { i18n } from '@kbn/i18n'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; import type { CloudStart } from '@kbn/cloud-plugin/public'; +// Hiding this until page is in a better space +const _connectorItem = { + link: 'serverlessConnectors', + title: i18n.translate('xpack.serverlessSearch.nav.connectors', { + defaultMessage: 'Connectors', + }), +}; + const navigationTree: NavigationTreeDefinition = { body: [ { type: 'recentlyAccessed' }, @@ -72,12 +80,6 @@ const navigationTree: NavigationTreeDefinition = { defaultMessage: 'Alerts', }), }, - { - link: 'serverlessConnectors', - title: i18n.translate('xpack.serverlessSearch.nav.connectors', { - defaultMessage: 'Connectors', - }), - }, ], }, { diff --git a/x-pack/plugins/serverless_search/public/plugin.ts b/x-pack/plugins/serverless_search/public/plugin.ts index dfe9cbbc5165c..c3faeb15f4384 100644 --- a/x-pack/plugins/serverless_search/public/plugin.ts +++ b/x-pack/plugins/serverless_search/public/plugin.ts @@ -54,6 +54,7 @@ export class ServerlessSearchPlugin defaultMessage: 'Connectors', }), appRoute: '/app/connectors', + searchable: false, async mount({ element }: AppMountParameters) { const { renderApp } = await import('./application/connectors'); const [coreStart, services] = await core.getStartServices(); diff --git a/x-pack/plugins/spaces/public/plugin.test.ts b/x-pack/plugins/spaces/public/plugin.test.ts index 91e1a21752959..2e81e6c039668 100644 --- a/x-pack/plugins/spaces/public/plugin.test.ts +++ b/x-pack/plugins/spaces/public/plugin.test.ts @@ -17,10 +17,14 @@ import { SpacesPlugin } from './plugin'; describe('Spaces plugin', () => { describe('#setup', () => { - it('should register the spaces API and the space selector app', () => { + it('should register the space selector app when buildFlavor is traditional', () => { const coreSetup = coreMock.createSetup(); + const mockInitializerContext = coreMock.createPluginInitializerContext( + {}, + { buildFlavor: 'traditional' } + ); - const plugin = new SpacesPlugin(coreMock.createPluginInitializerContext()); + const plugin = new SpacesPlugin(mockInitializerContext); plugin.setup(coreSetup, {}); expect(coreSetup.application.register).toHaveBeenCalledWith( @@ -33,7 +37,23 @@ describe('Spaces plugin', () => { ); }); - it('should register the management and feature catalogue sections when the management and home plugins are both available', () => { + it('should not register the space selector app when buildFlavor is serverless', () => { + const coreSetup = coreMock.createSetup(); + + const plugin = new SpacesPlugin(coreMock.createPluginInitializerContext()); + plugin.setup(coreSetup, {}); + + expect(coreSetup.application.register).not.toHaveBeenCalledWith( + expect.objectContaining({ + id: 'space_selector', + chromeless: true, + appRoute: '/spaces/space_selector', + mount: expect.any(Function), + }) + ); + }); + + it('should register the management and feature catalogue sections when the management and home plugins are both available when buildFlavor is traditional', () => { const coreSetup = coreMock.createSetup(); const home = homePluginMock.createSetupContract(); @@ -43,7 +63,12 @@ describe('Spaces plugin', () => { management.sections.section.kibana = mockSection; - const plugin = new SpacesPlugin(coreMock.createPluginInitializerContext()); + const mockInitializerContext = coreMock.createPluginInitializerContext( + {}, + { buildFlavor: 'traditional' } + ); + + const plugin = new SpacesPlugin(mockInitializerContext); plugin.setup(coreSetup, { management, home, @@ -62,19 +87,66 @@ describe('Spaces plugin', () => { }) ); }); + + it('should not register spaces in the management plugin or the feature catalog when the management and home plugins are both available when buildFlavor is serverless', () => { + const coreSetup = coreMock.createSetup(); + const home = homePluginMock.createSetupContract(); + + const management = managementPluginMock.createSetupContract(); + const mockSection = createManagementSectionMock(); + mockSection.registerApp = jest.fn(); + + management.sections.section.kibana = mockSection; + + const plugin = new SpacesPlugin(coreMock.createPluginInitializerContext()); + plugin.setup(coreSetup, { + management, + home, + }); + + expect(mockSection.registerApp).not.toHaveBeenCalledWith( + expect.objectContaining({ id: 'spaces' }) + ); + + expect(home.featureCatalogue.register).not.toHaveBeenCalledWith( + expect.objectContaining({ + category: 'admin', + icon: 'spacesApp', + id: 'spaces', + showOnHomePage: false, + }) + ); + }); }); describe('#start', () => { - it('should register the spaces nav control', () => { + it('should register the spaces nav control when buildFlavor is traditional', () => { const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); - const plugin = new SpacesPlugin(coreMock.createPluginInitializerContext()); + const mockInitializerContext = coreMock.createPluginInitializerContext( + {}, + { buildFlavor: 'traditional' } + ); + + const plugin = new SpacesPlugin(mockInitializerContext); plugin.setup(coreSetup, {}); plugin.start(coreStart); expect(coreStart.chrome.navControls.registerLeft).toHaveBeenCalled(); }); + + it('should not register the spaces nav control when buildFlavor is serverless', () => { + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + + const plugin = new SpacesPlugin(coreMock.createPluginInitializerContext()); + plugin.setup(coreSetup, {}); + + plugin.start(coreStart); + + expect(coreStart.chrome.navControls.registerLeft).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 33cbcc3a47227..28a54e7768f3e 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -45,9 +45,11 @@ export class SpacesPlugin implements Plugin(); + this.isServerless = this.initializerContext.env.packageInfo.buildFlavor === 'serverless'; } public setup(core: CoreSetup, plugins: PluginsSetup) { @@ -61,31 +63,35 @@ export class SpacesPlugin implements Plugin this.spacesManager.getActiveSpace(), }; - if (plugins.home) { - plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); - } - - if (plugins.management) { - this.managementService = new ManagementService(); - this.managementService.setup({ - management: plugins.management, + if (!this.isServerless) { + if (plugins.home) { + plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); + } + + if (plugins.management) { + this.managementService = new ManagementService(); + this.managementService.setup({ + management: plugins.management, + getStartServices: core.getStartServices, + spacesManager: this.spacesManager, + config: this.config, + }); + } + + spaceSelectorApp.create({ getStartServices: core.getStartServices, + application: core.application, spacesManager: this.spacesManager, - config: this.config, }); } - spaceSelectorApp.create({ - getStartServices: core.getStartServices, - application: core.application, - spacesManager: this.spacesManager, - }); - return {}; } public start(core: CoreStart) { - initSpacesNavControl(this.spacesManager, core); + if (!this.isServerless) { + initSpacesNavControl(this.spacesManager, core); + } return this.spacesApi; } diff --git a/x-pack/plugins/stack_alerts/common/esql_query_utils.test.ts b/x-pack/plugins/stack_alerts/common/esql_query_utils.test.ts new file mode 100644 index 0000000000000..64aad9c156958 --- /dev/null +++ b/x-pack/plugins/stack_alerts/common/esql_query_utils.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { rowToDocument, toEsQueryHits, transformDatatableToEsqlTable } from './esql_query_utils'; + +describe('ESQL query utils', () => { + describe('rowToDocument', () => { + it('correctly converts ESQL row to document', () => { + expect( + rowToDocument( + [ + { name: '@timestamp', type: 'date' }, + { name: 'ecs.version', type: 'keyword' }, + { name: 'error.code', type: 'keyword' }, + ], + ['2023-07-12T13:32:04.174Z', '1.8.0', null] + ) + ).toEqual({ + '@timestamp': '2023-07-12T13:32:04.174Z', + 'ecs.version': '1.8.0', + 'error.code': null, + }); + }); + }); + + describe('toEsQueryHits', () => { + it('correctly converts ESQL table to ES query hits', () => { + expect( + toEsQueryHits({ + columns: [ + { name: '@timestamp', type: 'date' }, + { name: 'ecs.version', type: 'keyword' }, + { name: 'error.code', type: 'keyword' }, + ], + values: [['2023-07-12T13:32:04.174Z', '1.8.0', null]], + }) + ).toEqual({ + hits: [ + { + _id: 'esql_query_document', + _index: '', + _source: { + '@timestamp': '2023-07-12T13:32:04.174Z', + 'ecs.version': '1.8.0', + 'error.code': null, + }, + }, + ], + total: 1, + }); + }); + }); + + describe('transformDatatableToEsqlTable', () => { + it('correctly converts data table to ESQL table', () => { + expect( + transformDatatableToEsqlTable({ + type: 'datatable', + columns: [ + { id: '@timestamp', name: '@timestamp', meta: { type: 'date' } }, + { id: 'ecs.version', name: 'ecs.version', meta: { type: 'string' } }, + { id: 'error.code', name: 'error.code', meta: { type: 'string' } }, + ], + rows: [ + { + '@timestamp': '2023-07-12T13:32:04.174Z', + 'ecs.version': '1.8.0', + 'error.code': null, + }, + ], + }) + ).toEqual({ + columns: [ + { + name: '@timestamp', + type: 'date', + }, + { + name: 'ecs.version', + type: 'string', + }, + { + name: 'error.code', + type: 'string', + }, + ], + values: [['2023-07-12T13:32:04.174Z', '1.8.0', null]], + }); + }); + }); +}); diff --git a/x-pack/plugins/stack_alerts/common/esql_query_utils.ts b/x-pack/plugins/stack_alerts/common/esql_query_utils.ts new file mode 100644 index 0000000000000..c74d3640c7fd7 --- /dev/null +++ b/x-pack/plugins/stack_alerts/common/esql_query_utils.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Datatable } from '@kbn/expressions-plugin/common'; + +type EsqlDocument = Record; + +interface EsqlHit { + _id: string; + _index: string; + _source: EsqlDocument; +} + +interface EsqlResultColumn { + name: string; + type: string; +} + +type EsqlResultRow = Array; + +export interface EsqlTable { + columns: EsqlResultColumn[]; + values: EsqlResultRow[]; +} + +const ESQL_DOCUMENT_ID = 'esql_query_document'; + +export const rowToDocument = (columns: EsqlResultColumn[], row: EsqlResultRow): EsqlDocument => { + return columns.reduce>((acc, column, i) => { + acc[column.name] = row[i]; + + return acc; + }, {}); +}; + +export const toEsQueryHits = (results: EsqlTable) => { + const hits: EsqlHit[] = results.values.map((row) => { + const document = rowToDocument(results.columns, row); + return { + _id: ESQL_DOCUMENT_ID, + _index: '', + _source: document, + }; + }); + + return { + hits, + total: hits.length, + }; +}; + +export const transformDatatableToEsqlTable = (results: Datatable): EsqlTable => { + const columns: EsqlResultColumn[] = results.columns.map((c) => ({ + name: c.id, + type: c.meta.type, + })); + const values: EsqlResultRow[] = results.rows.map((r) => Object.values(r)); + return { columns, values }; +}; diff --git a/x-pack/plugins/stack_alerts/common/index.ts b/x-pack/plugins/stack_alerts/common/index.ts index 58d2c6022e798..48e5a8b8d65fd 100644 --- a/x-pack/plugins/stack_alerts/common/index.ts +++ b/x-pack/plugins/stack_alerts/common/index.ts @@ -12,3 +12,6 @@ export { getHumanReadableComparator, } from './comparator'; export { STACK_ALERTS_FEATURE_ID, ES_QUERY_ID } from './constants'; + +export type { EsqlTable } from './esql_query_utils'; +export { rowToDocument, transformDatatableToEsqlTable, toEsQueryHits } from './esql_query_utils'; diff --git a/x-pack/plugins/stack_alerts/kibana.jsonc b/x-pack/plugins/stack_alerts/kibana.jsonc index 7333de149470b..5c7bec1a37a0a 100644 --- a/x-pack/plugins/stack_alerts/kibana.jsonc +++ b/x-pack/plugins/stack_alerts/kibana.jsonc @@ -22,7 +22,8 @@ "kibanaUtils" ], "requiredBundles": [ - "esUiShared" + "esUiShared", + "textBasedLanguages" ], "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/constants.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/constants.ts index 55a34c7b92a79..4cce902449d18 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/constants.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/constants.ts @@ -48,6 +48,13 @@ export const ONLY_ES_QUERY_EXPRESSION_ERRORS = { timeField: new Array(), }; +export const ONLY_ESQL_QUERY_EXPRESSION_ERRORS = { + esqlQuery: new Array(), + timeField: new Array(), + thresholdComparator: new Array(), + threshold0: new Array(), +}; + const ALL_EXPRESSION_ERROR_ENTRIES = { ...COMMON_EXPRESSION_ERRORS, ...SEARCH_SOURCE_ONLY_EXPRESSION_ERRORS, diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.test.tsx new file mode 100644 index 0000000000000..b9b8ba0dd38f7 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.test.tsx @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n-react'; + +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { EsqlQueryExpression } from './esql_query_expression'; +import { EsQueryRuleParams, SearchType } from '../types'; + +jest.mock('../validation', () => ({ + hasExpressionValidationErrors: jest.fn(), +})); +const { hasExpressionValidationErrors } = jest.requireMock('../validation'); + +jest.mock('@kbn/text-based-editor', () => ({ + fetchFieldsFromESQL: jest.fn(), +})); +const { fetchFieldsFromESQL } = jest.requireMock('@kbn/text-based-editor'); + +const AppWrapper: React.FC<{ children: React.ReactElement }> = React.memo(({ children }) => ( + {children} +)); + +const dataMock = dataPluginMock.createStartContract(); +const dataViewMock = dataViewPluginMocks.createStartContract(); +const unifiedSearchMock = unifiedSearchPluginMock.createStartContract(); +const chartsStartMock = chartPluginMock.createStartContract(); + +const defaultEsqlQueryExpressionParams: EsQueryRuleParams = { + size: 100, + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + index: ['test-index'], + timeField: '@timestamp', + aggType: 'count', + groupBy: 'all', + searchType: SearchType.esqlQuery, + esqlQuery: { esql: '' }, + excludeHitsFromPreviousRun: false, +}; + +describe('EsqlQueryRuleTypeExpression', () => { + beforeEach(() => { + jest.clearAllMocks(); + + hasExpressionValidationErrors.mockReturnValue(false); + }); + + it('should render EsqlQueryRuleTypeExpression with expected components', () => { + const result = render( + {}} + setRuleProperty={() => {}} + errors={{ esqlQuery: [], timeField: [], timeWindowSize: [] }} + data={dataMock} + dataViews={dataViewMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + onChangeMetaData={() => {}} + />, + { + wrapper: AppWrapper, + } + ); + + expect(result.getByTestId('queryEsqlEditor')).toBeInTheDocument(); + expect(result.getByTestId('timeFieldSelect')).toBeInTheDocument(); + expect(result.getByTestId('timeWindowSizeNumber')).toBeInTheDocument(); + expect(result.getByTestId('timeWindowUnitSelect')).toBeInTheDocument(); + expect(result.queryByTestId('testQuerySuccess')).not.toBeInTheDocument(); + expect(result.queryByTestId('testQueryError')).not.toBeInTheDocument(); + }); + + test('should render Test Query button disabled if alert params are invalid', async () => { + hasExpressionValidationErrors.mockReturnValue(true); + const result = render( + {}} + setRuleProperty={() => {}} + errors={{ esqlQuery: [], timeField: [], timeWindowSize: [] }} + data={dataMock} + dataViews={dataViewMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + onChangeMetaData={() => {}} + />, + { + wrapper: AppWrapper, + } + ); + + const button = result.getByTestId('testQuery'); + expect(button).toBeInTheDocument(); + expect(button).toBeDisabled(); + }); + + test('should show success message if Test Query is successful', async () => { + fetchFieldsFromESQL.mockResolvedValue({ + type: 'datatable', + columns: [ + { id: '@timestamp', name: '@timestamp', meta: { type: 'date' } }, + { id: 'ecs.version', name: 'ecs.version', meta: { type: 'string' } }, + { id: 'error.code', name: 'error.code', meta: { type: 'string' } }, + ], + rows: [ + { + '@timestamp': '2023-07-12T13:32:04.174Z', + 'ecs.version': '1.8.0', + 'error.code': null, + }, + ], + }); + const result = render( + {}} + setRuleProperty={() => {}} + errors={{ esqlQuery: [], timeField: [], timeWindowSize: [] }} + data={dataMock} + dataViews={dataViewMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + onChangeMetaData={() => {}} + />, + { + wrapper: AppWrapper, + } + ); + + fireEvent.click(result.getByTestId('testQuery')); + await waitFor(() => expect(fetchFieldsFromESQL).toBeCalled()); + + expect(result.getByTestId('testQuerySuccess')).toBeInTheDocument(); + expect(result.getByText('Query matched 1 documents in the last 15s.')).toBeInTheDocument(); + expect(result.queryByTestId('testQueryError')).not.toBeInTheDocument(); + }); + + test('should show error message if Test Query is throws error', async () => { + fetchFieldsFromESQL.mockRejectedValue('Error getting test results.!'); + const result = render( + {}} + setRuleProperty={() => {}} + errors={{ esqlQuery: [], timeField: [], timeWindowSize: [] }} + data={dataMock} + dataViews={dataViewMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + onChangeMetaData={() => {}} + />, + { + wrapper: AppWrapper, + } + ); + + fireEvent.click(result.getByTestId('testQuery')); + await waitFor(() => expect(fetchFieldsFromESQL).toBeCalled()); + + expect(result.queryByTestId('testQuerySuccess')).not.toBeInTheDocument(); + expect(result.getByTestId('testQueryError')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx new file mode 100644 index 0000000000000..5a26839a7b284 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, Fragment, useEffect, useCallback } from 'react'; +import { get } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { getFields, RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; +import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; +import { fetchFieldsFromESQL } from '@kbn/text-based-editor'; +import { AggregateQuery, getIndexPatternFromESQLQuery } from '@kbn/es-query'; +import { parseDuration } from '@kbn/alerting-plugin/common'; +import { + firstFieldOption, + getTimeFieldOptions, + getTimeOptions, + parseAggregationResults, +} from '@kbn/triggers-actions-ui-plugin/public/common'; +import { EsQueryRuleParams, EsQueryRuleMetaData, SearchType } from '../types'; +import { DEFAULT_VALUES } from '../constants'; +import { useTriggerUiActionServices } from '../util'; +import { hasExpressionValidationErrors } from '../validation'; +import { TestQueryRow } from '../test_query_row'; +import { rowToDocument, toEsQueryHits, transformDatatableToEsqlTable } from '../../../../common'; + +export const EsqlQueryExpression: React.FC< + RuleTypeParamsExpressionProps, EsQueryRuleMetaData> +> = ({ ruleParams, setRuleParams, setRuleProperty, errors }) => { + const { expressions, http } = useTriggerUiActionServices(); + const { esqlQuery, timeWindowSize, timeWindowUnit, timeField } = ruleParams; + + const [currentRuleParams, setCurrentRuleParams] = useState< + EsQueryRuleParams + >({ + ...ruleParams, + timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, + // ESQL queries compare conditions within the ES query + // so only 'met' results are returned, therefore the threshold should always be 0 + threshold: [0], + thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR, + size: DEFAULT_VALUES.SIZE, + esqlQuery: esqlQuery ?? { esql: '' }, + aggType: DEFAULT_VALUES.AGGREGATION_TYPE, + groupBy: DEFAULT_VALUES.GROUP_BY, + termSize: DEFAULT_VALUES.TERM_SIZE, + searchType: SearchType.esqlQuery, + }); + const [query, setQuery] = useState({ esql: '' }); + const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); + const [detectTimestamp, setDetectTimestamp] = useState(false); + + const setParam = useCallback( + (paramField: string, paramValue: unknown) => { + setCurrentRuleParams((currentParams) => ({ + ...currentParams, + [paramField]: paramValue, + })); + setRuleParams(paramField, paramValue); + }, + [setRuleParams] + ); + + const setDefaultExpressionValues = async () => { + setRuleProperty('params', currentRuleParams); + setQuery(esqlQuery ?? { esql: '' }); + if (timeField) { + setTimeFieldOptions([firstFieldOption, { text: timeField, value: timeField }]); + } + }; + useEffect(() => { + setDefaultExpressionValues(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onTestQuery = useCallback(async () => { + const window = `${timeWindowSize}${timeWindowUnit}`; + const emptyResult = { + testResults: { results: [], truncated: false }, + isGrouped: true, + timeWindow: window, + }; + + if (hasExpressionValidationErrors(currentRuleParams)) { + return emptyResult; + } + const timeWindow = parseDuration(window); + const now = Date.now(); + const table = await fetchFieldsFromESQL(esqlQuery, expressions, { + from: new Date(now - timeWindow).toISOString(), + to: new Date(now).toISOString(), + }); + if (table) { + const esqlTable = transformDatatableToEsqlTable(table); + const hits = toEsQueryHits(esqlTable); + return { + testResults: parseAggregationResults({ + isCountAgg: true, + isGroupAgg: false, + esResult: { + took: 0, + timed_out: false, + _shards: { failed: 0, successful: 0, total: 0 }, + hits, + }, + }), + isGrouped: false, + timeWindow: window, + rawResults: { + cols: esqlTable.columns.map((col) => ({ + id: col.name, + })), + rows: esqlTable.values.slice(0, 5).map((row) => rowToDocument(esqlTable.columns, row)), + }, + }; + } + return emptyResult; + }, [timeWindowSize, timeWindowUnit, currentRuleParams, esqlQuery, expressions]); + + const refreshTimeFields = async (q: AggregateQuery) => { + let hasTimestamp = false; + const indexPattern: string = getIndexPatternFromESQLQuery(get(q, 'esql')); + const currentEsFields = await getFields(http, [indexPattern]); + const timeFields = getTimeFieldOptions(currentEsFields); + setTimeFieldOptions([firstFieldOption, ...timeFields]); + + const timestampField = timeFields.find((field) => field.value === '@timestamp'); + if (timestampField) { + setParam('timeField', timestampField.value); + hasTimestamp = true; + } + setDetectTimestamp(hasTimestamp); + }; + + return ( + + +
+ +
+
+ + + { + setQuery(q); + setParam('esqlQuery', q); + refreshTimeFields(q); + }} + expandCodeEditor={() => true} + isCodeEditorExpanded={true} + onTextLangQuerySubmit={() => {}} + detectTimestamp={detectTimestamp} + hideMinimizeButton={true} + hideRunQueryText={true} + /> + + + +
+ +
+
+ + 0 && timeField !== undefined} + error={errors.timeField} + > + 0 && timeField !== undefined} + fullWidth + name="timeField" + data-test-subj="timeFieldSelect" + value={timeField || ''} + onChange={(e) => { + setParam('timeField', e.target.value); + }} + /> + + + +
+ +
+
+ + + + 0} + error={errors.timeWindowSize} + > + 0} + min={0} + value={timeWindowSize || ''} + onChange={(e) => { + const { value } = e.target; + const timeWindowSizeVal = value !== '' ? parseInt(value, 10) : undefined; + setParam('timeWindowSize', timeWindowSizeVal); + }} + /> + + + + + { + setParam('timeWindowUnit', e.target.value); + }} + options={getTimeOptions(timeWindowSize ?? 1)} + /> + + + + + +
+ ); +}; 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 ecbfd306e1ead..09a4c3da9b5ec 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 @@ -23,7 +23,6 @@ import { EsQueryRuleTypeExpression } from './expression'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { Subject } from 'rxjs'; import { ISearchSource } from '@kbn/data-plugin/common'; -import { IUiSettingsClient } from '@kbn/core/public'; import { findTestSubject } from '@elastic/eui/lib/test'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { act } from 'react-dom/test-utils'; @@ -75,6 +74,20 @@ const defaultSearchSourceRuleParams: EsQueryRuleParams aggType: 'count', }; +const defaultEsqlRuleParams: EsQueryRuleParams = { + size: 100, + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + index: ['test-index'], + timeField: '@timestamp', + searchType: SearchType.esqlQuery, + esqlQuery: { esql: 'test' }, + excludeHitsFromPreviousRun: true, + aggType: 'count', +}; + const dataViewPluginMock = dataViewPluginMocks.createStartContract(); const chartsStartMock = chartPluginMock.createStartContract(); const unifiedSearchMock = unifiedSearchPluginMock.createStartContract(); @@ -82,7 +95,7 @@ const httpMock = httpServiceMock.createStartContract(); const docLinksMock = docLinksServiceMock.createStartContract(); export const uiSettingsMock = { get: jest.fn(), -} as unknown as IUiSettingsClient; +}; const mockSearchResult = new Subject(); const searchSourceFieldsMock = { @@ -150,7 +163,10 @@ dataMock.query.savedQueries.findSavedQueries = jest.fn(() => (httpMock.post as jest.Mock).mockImplementation(() => Promise.resolve({ fields: [] })); const Wrapper: React.FC<{ - ruleParams: EsQueryRuleParams | EsQueryRuleParams; + ruleParams: + | EsQueryRuleParams + | EsQueryRuleParams + | EsQueryRuleParams; metadata?: EsQueryRuleMetaData; }> = ({ ruleParams, metadata }) => { const [currentRuleParams, setCurrentRuleParams] = useState(ruleParams); @@ -192,7 +208,10 @@ const Wrapper: React.FC<{ }; const setup = ( - ruleParams: EsQueryRuleParams | EsQueryRuleParams, + ruleParams: + | EsQueryRuleParams + | EsQueryRuleParams + | EsQueryRuleParams, metadata?: EsQueryRuleMetaData ) => { return mountWithIntl( @@ -213,11 +232,28 @@ const setup = ( }; describe('EsQueryRuleTypeExpression', () => { + beforeEach(() => { + jest.clearAllMocks(); + + uiSettingsMock.get.mockReturnValue(true); + }); + test('should render options by default', async () => { const wrapper = setup({} as EsQueryRuleParams); expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy(); expect(findTestSubject(wrapper, 'queryFormType_searchSource').exists()).toBeTruthy(); expect(findTestSubject(wrapper, 'queryFormType_esQuery').exists()).toBeTruthy(); + expect(findTestSubject(wrapper, 'queryFormType_esqlQuery').exists()).toBeTruthy(); + expect(findTestSubject(wrapper, 'queryFormTypeChooserCancel').exists()).toBeFalsy(); + }); + + test('should hide ESQL option when not enabled', async () => { + uiSettingsMock.get.mockReturnValueOnce(false); + const wrapper = setup({} as EsQueryRuleParams); + expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy(); + expect(findTestSubject(wrapper, 'queryFormType_searchSource').exists()).toBeTruthy(); + expect(findTestSubject(wrapper, 'queryFormType_esQuery').exists()).toBeTruthy(); + expect(findTestSubject(wrapper, 'queryFormType_esqlQuery').exists()).toBeFalsy(); expect(findTestSubject(wrapper, 'queryFormTypeChooserCancel').exists()).toBeFalsy(); }); @@ -257,6 +293,23 @@ describe('EsQueryRuleTypeExpression', () => { expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy(); }); + test('should switch to ESQL form type on selection and return back on cancel', async () => { + let wrapper = setup({} as EsQueryRuleParams); + await act(async () => { + findTestSubject(wrapper, 'queryFormType_esqlQuery').simulate('click'); + }); + wrapper = await wrapper.update(); + expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="queryEsqlEditor"]')).toBeTruthy(); + + await act(async () => { + findTestSubject(wrapper, 'queryFormTypeChooserCancel').simulate('click'); + }); + wrapper = await wrapper.update(); + expect(wrapper.exists('[data-test-subj="queryEsqlEditor"]')).toBeFalsy(); + expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy(); + }); + test('should render QueryDSL view without the form type chooser', async () => { let wrapper: ReactWrapper; await act(async () => { @@ -282,4 +335,19 @@ describe('EsQueryRuleTypeExpression', () => { expect(findTestSubject(wrapper!, 'queryFormTypeChooserCancel').exists()).toBeFalsy(); expect(findTestSubject(wrapper!, 'selectDataViewExpression').exists()).toBeTruthy(); }); + + test('should render ESQL view without the form type chooser', async () => { + let wrapper: ReactWrapper; + await act(async () => { + wrapper = setup(defaultEsqlRuleParams, { + adHocDataViewList: [], + isManagementPage: false, + }); + wrapper = await wrapper.update(); + }); + wrapper = await wrapper!.update(); + expect(findTestSubject(wrapper!, 'queryFormTypeChooserTitle').exists()).toBeFalsy(); + expect(findTestSubject(wrapper!, 'queryFormTypeChooserCancel').exists()).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="queryEsqlEditor"]')).toBeTruthy(); + }); }); 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 30045fee81a29..1653799a233df 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 @@ -15,8 +15,9 @@ import { EsQueryRuleParams, EsQueryRuleMetaData, SearchType } from '../types'; import { SearchSourceExpression, SearchSourceExpressionProps } from './search_source_expression'; import { EsQueryExpression } from './es_query_expression'; import { QueryFormTypeChooser } from './query_form_type_chooser'; -import { isSearchSourceRule } from '../util'; +import { isEsqlQueryRule, isSearchSourceRule } from '../util'; import { ALL_EXPRESSION_ERROR_KEYS } from '../constants'; +import { EsqlQueryExpression } from './esql_query_expression'; function areSearchSourceExpressionPropsEqual( prevProps: Readonly>, @@ -37,6 +38,7 @@ export const EsQueryRuleTypeExpression: React.FunctionComponent< > = (props) => { const { ruleParams, errors, setRuleProperty, setRuleParams } = props; const isSearchSource = isSearchSourceRule(ruleParams); + const isEsqlQuery = isEsqlQueryRule(ruleParams); // metadata provided only when open alert from Discover page const isManagementPage = props.metadata?.isManagementPage ?? true; @@ -95,10 +97,14 @@ export const EsQueryRuleTypeExpression: React.FunctionComponent< )} - {ruleParams.searchType && !isSearchSource && ( + {ruleParams.searchType && !isSearchSource && !isEsqlQuery && ( )} + {ruleParams.searchType && isEsqlQuery && ( + + )} + ); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/query_form_type_chooser.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/query_form_type_chooser.tsx index bff6792bdda3f..a2d45c78de9c9 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/query_form_type_chooser.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/query_form_type_chooser.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiButtonIcon, EuiFlexGroup, @@ -19,39 +19,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { SearchType } from '../types'; - -const FORM_TYPE_ITEMS: Array<{ formType: SearchType; label: string; description: string }> = [ - { - formType: SearchType.searchSource, - label: i18n.translate( - 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.kqlOrLuceneFormTypeLabel', - { - defaultMessage: 'KQL or Lucene', - } - ), - description: i18n.translate( - 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.kqlOrLuceneFormTypeDescription', - { - defaultMessage: 'Use KQL or Lucene to define a text-based query.', - } - ), - }, - { - formType: SearchType.esQuery, - label: i18n.translate( - 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.queryDslFormTypeLabel', - { - defaultMessage: 'Query DSL', - } - ), - description: i18n.translate( - 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.queryDslFormTypeDescription', - { - defaultMessage: 'Use the Elasticsearch Query DSL to define a query.', - } - ), - }, -]; +import { useTriggerUiActionServices } from '../util'; export interface QueryFormTypeProps { searchType: SearchType | null; @@ -62,8 +30,65 @@ export const QueryFormTypeChooser: React.FC = ({ searchType, onFormTypeSelect, }) => { + const { uiSettings } = useTriggerUiActionServices(); + const isEsqlEnabled = uiSettings?.get('discover:enableESQL'); + + const formTypeItems = useMemo(() => { + const items: Array<{ formType: SearchType; label: string; description: string }> = [ + { + formType: SearchType.searchSource, + label: i18n.translate( + 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.kqlOrLuceneFormTypeLabel', + { + defaultMessage: 'KQL or Lucene', + } + ), + description: i18n.translate( + 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.kqlOrLuceneFormTypeDescription', + { + defaultMessage: 'Use KQL or Lucene to define a text-based query.', + } + ), + }, + { + formType: SearchType.esQuery, + label: i18n.translate( + 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.queryDslFormTypeLabel', + { + defaultMessage: 'Query DSL', + } + ), + description: i18n.translate( + 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.queryDslFormTypeDescription', + { + defaultMessage: 'Use the Elasticsearch Query DSL to define a query.', + } + ), + }, + ]; + + if (isEsqlEnabled) { + items.push({ + formType: SearchType.esqlQuery, + label: i18n.translate( + 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.esqlFormTypeLabel', + { + defaultMessage: 'ESQL', + } + ), + description: i18n.translate( + 'xpack.stackAlerts.esQuery.ui.selectQueryFormType.esqlFormTypeDescription', + { + defaultMessage: 'Use ESQL to define a text-based query.', + } + ), + }); + } + return items; + }, [isEsqlEnabled]); + if (searchType) { - const activeFormTypeItem = FORM_TYPE_ITEMS.find((item) => item.formType === searchType); + const activeFormTypeItem = formTypeItems.find((item) => item.formType === searchType); return ( <> @@ -107,7 +132,7 @@ export const QueryFormTypeChooser: React.FC = ({ - {FORM_TYPE_ITEMS.map((item) => ( + {formTypeItems.map((item) => ( Promise<{ @@ -27,14 +29,24 @@ export interface TestQueryRowProps { }>; copyQuery?: () => string; hasValidationErrors: boolean; + showTable?: boolean; } export const TestQueryRow: React.FC = ({ fetch, copyQuery, hasValidationErrors, + showTable, }) => { - const { onTestQuery, testQueryResult, testQueryError, testQueryLoading } = useTestQuery(fetch); + const { + onTestQuery, + testQueryResult, + testQueryError, + testQueryLoading, + testQueryRawResults, + testQueryAlerts, + } = useTestQuery(fetch); + const [copiedMessage, setCopiedMessage] = useState(null); return ( @@ -124,6 +136,12 @@ export const TestQueryRow: React.FC = ({ )} + {showTable && testQueryRawResults && ( + <> + + + + )} ); }; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/test_query_row_table.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/test_query_row_table.test.tsx new file mode 100644 index 0000000000000..54b575be36f11 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/test_query_row_table.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { TestQueryRowTable } from './test_query_row_table'; + +const AppWrapper: React.FC<{ children: React.ReactElement }> = React.memo(({ children }) => ( + {children} +)); + +describe('TestQueryRow', () => { + it('should render the datagrid', () => { + const result = render( + , + { + wrapper: AppWrapper, + } + ); + + expect(result.getByTestId('test-query-row-datagrid')).toBeInTheDocument(); + expect(result.getAllByTestId('dataGridRowCell')).toHaveLength(2); + expect(result.queryByText('Alerts generated')).not.toBeInTheDocument(); + expect(result.queryAllByTestId('alert-badge')).toHaveLength(0); + }); + + it('should render the datagrid and alerts if provided', () => { + const result = render( + , + { + wrapper: AppWrapper, + } + ); + + expect(result.getByTestId('test-query-row-datagrid')).toBeInTheDocument(); + expect(result.getAllByTestId('dataGridRowCell')).toHaveLength(2); + expect(result.getByText('Alerts generated')).toBeInTheDocument(); + expect(result.getAllByTestId('alert-badge')).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/test_query_row_table.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/test_query_row_table.tsx new file mode 100644 index 0000000000000..504880ee8da43 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/test_query_row_table.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { css } from '@emotion/react'; +import { + EuiDataGrid, + EuiPanel, + EuiDataGridColumn, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiBadge, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const styles = { + grid: css` + .euiDataGridHeaderCell { + background: none; + } + .euiDataGridHeader .euiDataGridHeaderCell { + border-top: none; + } + `, +}; + +export interface TestQueryRowTableProps { + rawResults: { cols: EuiDataGridColumn[]; rows: Array> }; + alerts: string[] | null; +} + +export const TestQueryRowTable: React.FC = ({ rawResults, alerts }) => { + return ( + + c.id), + setVisibleColumns: () => {}, + }} + rowCount={rawResults.rows.length} + gridStyle={{ + border: 'horizontal', + rowHover: 'none', + }} + renderCellValue={({ rowIndex, columnId }) => rawResults.rows[rowIndex][columnId]} + pagination={{ + pageIndex: 0, + pageSize: 10, + onChangeItemsPerPage: () => {}, + onChangePage: () => {}, + }} + toolbarVisibility={false} + /> + + {alerts && ( + + + +
+ {i18n.translate('xpack.stackAlerts.esQuery.ui.testQueryAlerts', { + defaultMessage: 'Alerts generated', + })} +
+
+
+ {alerts.map((alert, index) => { + return ( + + + {alert} + + + ); + })} +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.test.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.test.ts index 2067bf816eff7..44c2a1cf55717 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.test.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.test.ts @@ -20,6 +20,10 @@ describe('useTestQuery', () => { }, isGrouped: false, timeWindow: '1s', + rawResults: { + cols: [{ id: 'ungrouped', name: 'ungrouped', field: 'ungrouped', actions: false }], + rows: [{ ungrouped: 'test' }], + }, }), }); await act(async () => { @@ -29,6 +33,11 @@ describe('useTestQuery', () => { expect(result.current.testQueryError).toBe(null); expect(result.current.testQueryResult).toContain('1s'); expect(result.current.testQueryResult).toContain('1 document'); + expect(result.current.testQueryRawResults).toEqual({ + cols: [{ id: 'ungrouped', name: 'ungrouped', field: 'ungrouped', actions: false }], + rows: [{ ungrouped: 'test' }], + }); + expect(result.current.testQueryAlerts).toEqual(['query matched']); }); test('returning a valid result for grouped result', async () => { @@ -44,6 +53,10 @@ describe('useTestQuery', () => { }, isGrouped: true, timeWindow: '1s', + rawResults: { + cols: [{ id: 'grouped', name: 'grouped', field: 'grouped', actions: false }], + rows: [{ grouped: 'test' }], + }, }), }); await act(async () => { @@ -55,6 +68,11 @@ describe('useTestQuery', () => { expect(result.current.testQueryResult).toContain( 'Grouped query matched 2 groups in the last 1s.' ); + expect(result.current.testQueryRawResults).toEqual({ + cols: [{ id: 'grouped', name: 'grouped', field: 'grouped', actions: false }], + rows: [{ grouped: 'test' }], + }); + expect(result.current.testQueryAlerts).toEqual(['a', 'b']); }); test('returning an error', async () => { @@ -68,5 +86,7 @@ describe('useTestQuery', () => { expect(result.current.testQueryLoading).toBe(false); expect(result.current.testQueryError).toContain(errorMsg); expect(result.current.testQueryResult).toBe(null); + expect(result.current.testQueryRawResults).toBe(null); + expect(result.current.testQueryAlerts).toBe(null); }); }); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.ts index 06f230bafdccb..bac58eb2f0f43 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.ts @@ -8,17 +8,22 @@ import { useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import type { ParsedAggregationResults } from '@kbn/triggers-actions-ui-plugin/common'; +import { EuiDataGridColumn } from '@elastic/eui'; interface TestQueryResponse { result: string | null; error: string | null; isLoading: boolean; + rawResults: { cols: EuiDataGridColumn[]; rows: Array> } | null; + alerts: string[] | null; } const TEST_QUERY_INITIAL_RESPONSE: TestQueryResponse = { result: null, error: null, isLoading: false, + rawResults: null, + alerts: null, }; /** @@ -30,6 +35,7 @@ export function useTestQuery( testResults: ParsedAggregationResults; isGrouped: boolean; timeWindow: string; + rawResults?: { cols: EuiDataGridColumn[]; rows: Array> }; }> ) { const [testQueryResponse, setTestQueryResponse] = useState( @@ -46,10 +52,12 @@ export function useTestQuery( result: null, error: null, isLoading: true, + rawResults: null, + alerts: null, }); try { - const { testResults, isGrouped, timeWindow } = await fetch(); + const { testResults, isGrouped, timeWindow, rawResults } = await fetch(); if (isGrouped) { setTestQueryResponse({ @@ -62,6 +70,11 @@ export function useTestQuery( }), error: null, isLoading: false, + rawResults: rawResults ?? null, + alerts: + testResults.results.length > 0 + ? testResults.results.map((result) => result.group) + : null, }); } else { const ungroupedQueryResponse = @@ -73,6 +86,8 @@ export function useTestQuery( }), error: null, isLoading: false, + rawResults: rawResults ?? null, + alerts: ungroupedQueryResponse.count > 0 ? ['query matched'] : null, }); } } catch (err) { @@ -85,6 +100,8 @@ export function useTestQuery( values: { message: message ? `${err.message}: ${message}` : err.message }, }), isLoading: false, + rawResults: null, + alerts: null, }); } }, [fetch]); @@ -94,5 +111,7 @@ export function useTestQuery( testQueryResult: testQueryResponse.result, testQueryError: testQueryResponse.error, testQueryLoading: testQueryResponse.isLoading, + testQueryRawResults: testQueryResponse.rawResults, + testQueryAlerts: testQueryResponse.alerts, }; } diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts index d517d23af52b3..c7a298ebce22a 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts @@ -9,10 +9,12 @@ import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { EuiComboBoxOptionOption } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; +import { AggregateQuery } from '@kbn/es-query'; export enum SearchType { esQuery = 'esQuery', searchSource = 'searchSource', + esqlQuery = 'esqlQuery', } export interface CommonRuleParams { @@ -38,6 +40,8 @@ export interface EsQueryRuleMetaData { export type EsQueryRuleParams = T extends SearchType.searchSource ? CommonEsQueryRuleParams & OnlySearchSourceRuleParams + : T extends SearchType.esqlQuery + ? CommonEsQueryRuleParams & OnlyEsqlQueryRuleParams : CommonEsQueryRuleParams & OnlyEsQueryRuleParams; export interface OnlyEsQueryRuleParams { @@ -53,4 +57,10 @@ export interface OnlySearchSourceRuleParams { savedQueryId?: string; } +export interface OnlyEsqlQueryRuleParams { + searchType?: 'esqlQuery'; + esqlQuery: AggregateQuery; + timeField: string; +} + export type DataViewOption = EuiComboBoxOptionOption; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/util.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/util.ts index 5568924e845e4..7ca42220c3ebf 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/util.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/util.ts @@ -17,6 +17,12 @@ export const isSearchSourceRule = ( return ruleParams.searchType === 'searchSource'; }; +export const isEsqlQueryRule = ( + ruleParams: EsQueryRuleParams +): ruleParams is EsQueryRuleParams => { + return ruleParams.searchType === 'esqlQuery'; +}; + export const convertFieldSpecToFieldOption = (fieldSpec: FieldSpec[]): FieldOption[] => { return (fieldSpec ?? []) .filter((spec: FieldSpec) => spec.isMapped || spec.runtimeField) diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.test.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.test.ts index 0b40110f60072..f43adeab3a3a8 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.test.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.test.ts @@ -278,4 +278,63 @@ describe('expression params validation', () => { expect(validateExpression(initialParams).errors.size.length).toBe(0); expect(hasExpressionValidationErrors(initialParams)).toBe(false); }); + + test('if esqlQuery property is not set should return proper error message', () => { + const initialParams = { + size: 100, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + timeField: '@timestamp', + searchType: SearchType.esqlQuery, + } as EsQueryRuleParams; + expect(validateExpression(initialParams).errors.esqlQuery.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.esqlQuery[0]).toBe(`ESQL query is required.`); + }); + + test('if esqlQuery timeField property is not defined should return proper error message', () => { + const initialParams = { + size: 100, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + esqlQuery: { esql: 'test' }, + searchType: SearchType.esqlQuery, + } as EsQueryRuleParams; + expect(validateExpression(initialParams).errors.timeField.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.timeField[0]).toBe('Time field is required.'); + }); + + test('if esqlQuery thresholdComparator property is not gt should return proper error message', () => { + const initialParams = { + size: 100, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + esqlQuery: { esql: 'test' }, + searchType: SearchType.esqlQuery, + thresholdComparator: '<', + timeField: '@timestamp', + } as EsQueryRuleParams; + expect(validateExpression(initialParams).errors.thresholdComparator.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.thresholdComparator[0]).toBe( + 'Threshold comparator is required to be greater than.' + ); + }); + + test('if esqlQuery threshold property is not 0 should return proper error message', () => { + const initialParams = { + size: 100, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [8], + esqlQuery: { esql: 'test' }, + searchType: SearchType.esqlQuery, + timeField: '@timestamp', + } as EsQueryRuleParams; + expect(validateExpression(initialParams).errors.threshold0.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.threshold0[0]).toBe( + 'Threshold is required to be 0.' + ); + }); }); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.ts index 9e6eaeb7b6de0..7d7c34a76927c 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.ts @@ -12,11 +12,13 @@ import { builtInComparators, builtInAggregationTypes, builtInGroupByTypes, + COMPARATORS, } from '@kbn/triggers-actions-ui-plugin/public'; import { EsQueryRuleParams, SearchType } from './types'; -import { isSearchSourceRule } from './util'; +import { isEsqlQueryRule, isSearchSourceRule } from './util'; import { COMMON_EXPRESSION_ERRORS, + ONLY_ESQL_QUERY_EXPRESSION_ERRORS, ONLY_ES_QUERY_EXPRESSION_ERRORS, SEARCH_SOURCE_ONLY_EXPRESSION_ERRORS, } from './constants'; @@ -221,6 +223,46 @@ const validateEsQueryParams = (ruleParams: EsQueryRuleParams return errors; }; +const validateEsqlQueryParams = (ruleParams: EsQueryRuleParams) => { + const errors: typeof ONLY_ESQL_QUERY_EXPRESSION_ERRORS = defaultsDeep( + {}, + ONLY_ESQL_QUERY_EXPRESSION_ERRORS + ); + if (!ruleParams.esqlQuery) { + errors.esqlQuery.push( + i18n.translate('xpack.stackAlerts.esqlQuery.ui.validation.error.requiredQueryText', { + defaultMessage: 'ESQL query is required.', + }) + ); + } + if (!ruleParams.timeField) { + errors.timeField.push( + i18n.translate('xpack.stackAlerts.esqlQuery.ui.validation.error.requiredTimeFieldText', { + defaultMessage: 'Time field is required.', + }) + ); + } + if (ruleParams.thresholdComparator !== COMPARATORS.GREATER_THAN) { + errors.thresholdComparator.push( + i18n.translate( + 'xpack.stackAlerts.esqlQuery.ui.validation.error.requiredThresholdComparatorText', + { + defaultMessage: 'Threshold comparator is required to be greater than.', + } + ) + ); + } + if (ruleParams.threshold && ruleParams.threshold[0] !== 0) { + errors.threshold0.push( + i18n.translate('xpack.stackAlerts.esqlQuery.ui.validation.error.requiredThreshold0Text', { + defaultMessage: 'Threshold is required to be 0.', + }) + ); + } + + return errors; +}; + export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationResult => { const validationResult = { errors: {} }; @@ -234,8 +276,7 @@ export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationRes * It's important to report searchSource rule related errors only into errors.searchConfiguration prop. * For example errors.index is a mistake to report searchSource rule related errors. It will lead to issues. */ - const isSearchSource = isSearchSourceRule(ruleParams); - if (isSearchSource) { + if (isSearchSourceRule(ruleParams)) { validationResult.errors = { ...validationResult.errors, ...validateSearchSourceParams(ruleParams), @@ -243,6 +284,14 @@ export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationRes return validationResult; } + if (isEsqlQueryRule(ruleParams)) { + validationResult.errors = { + ...validationResult.errors, + ...validateEsqlQueryParams(ruleParams), + }; + return validationResult; + } + const esQueryErrors = validateEsQueryParams(ruleParams as EsQueryRuleParams); validationResult.errors = { ...validationResult.errors, ...esQueryErrors }; return validationResult; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.test.ts index 9936e63aa6ed1..f9f4ab51b660f 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.test.ts @@ -167,6 +167,7 @@ describe('getContextConditionsDescription', () => { comparator: Comparator.GT, threshold: [10], aggType: 'count', + searchType: 'esQuery', }); expect(result).toBe(`Number of matching documents is greater than 10`); }); @@ -177,6 +178,7 @@ describe('getContextConditionsDescription', () => { threshold: [10], aggType: 'count', isRecovered: true, + searchType: 'esQuery', }); expect(result).toBe(`Number of matching documents is NOT greater than 10`); }); @@ -187,6 +189,7 @@ describe('getContextConditionsDescription', () => { threshold: [10, 20], aggType: 'count', isRecovered: true, + searchType: 'esQuery', }); expect(result).toBe(`Number of matching documents is NOT between 10 and 20`); }); @@ -197,6 +200,7 @@ describe('getContextConditionsDescription', () => { threshold: [10], aggType: 'count', group: 'host-1', + searchType: 'esQuery', }); expect(result).toBe(`Number of matching documents for group "host-1" is greater than 10`); }); @@ -208,6 +212,7 @@ describe('getContextConditionsDescription', () => { aggType: 'count', isRecovered: true, group: 'host-1', + searchType: 'esQuery', }); expect(result).toBe(`Number of matching documents for group "host-1" is NOT greater than 10`); }); @@ -218,6 +223,7 @@ describe('getContextConditionsDescription', () => { threshold: [10], aggType: 'min', aggField: 'numericField', + searchType: 'esQuery', }); expect(result).toBe( `Number of matching documents where min of numericField is greater than 10` @@ -231,6 +237,7 @@ describe('getContextConditionsDescription', () => { aggType: 'min', aggField: 'numericField', isRecovered: true, + searchType: 'esQuery', }); expect(result).toBe( `Number of matching documents where min of numericField is NOT greater than 10` @@ -244,6 +251,7 @@ describe('getContextConditionsDescription', () => { group: 'host-1', aggType: 'max', aggField: 'numericField', + searchType: 'esQuery', }); expect(result).toBe( `Number of matching documents for group "host-1" where max of numericField is greater than 10` @@ -258,9 +266,31 @@ describe('getContextConditionsDescription', () => { group: 'host-1', aggType: 'max', aggField: 'numericField', + searchType: 'esQuery', }); expect(result).toBe( `Number of matching documents for group "host-1" where max of numericField is NOT greater than 10` ); }); + + it('should return conditions correctly for ESQL search type', () => { + const result = getContextConditionsDescription({ + comparator: Comparator.GT, + threshold: [0], + aggType: 'count', + searchType: 'esqlQuery', + }); + expect(result).toBe(`Query matched documents`); + }); + + it('should return conditions correctly ESQL search type when isRecovered is true', () => { + const result = getContextConditionsDescription({ + comparator: Comparator.GT, + threshold: [0], + aggType: 'count', + isRecovered: true, + searchType: 'esqlQuery', + }); + expect(result).toBe(`Query did NOT match documents`); + }); }); diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.ts index b84601314fbdd..5de1fc15c1120 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.ts @@ -11,6 +11,7 @@ import { AlertInstanceContext } from '@kbn/alerting-plugin/server'; import { EsQueryRuleParams } from './rule_type_params'; import { Comparator } from '../../../common/comparator_types'; import { getHumanReadableComparator } from '../../../common'; +import { isEsqlQueryRule } from './util'; // rule type context provided to actions export interface ActionContext extends EsQueryRuleActionContext { @@ -79,6 +80,7 @@ export function addMessages({ } interface GetContextConditionsDescriptionOpts { + searchType: 'searchSource' | 'esQuery' | 'esqlQuery'; comparator: Comparator; threshold: number[]; aggType: string; @@ -88,6 +90,7 @@ interface GetContextConditionsDescriptionOpts { } export function getContextConditionsDescription({ + searchType, comparator, threshold, aggType, @@ -95,15 +98,23 @@ export function getContextConditionsDescription({ isRecovered = false, group, }: GetContextConditionsDescriptionOpts) { - return i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', { - defaultMessage: - 'Number of matching documents{groupCondition}{aggCondition} is {negation}{thresholdComparator} {threshold}', - values: { - aggCondition: aggType === 'count' ? '' : ` where ${aggType} of ${aggField}`, - groupCondition: group ? ` for group "${group}"` : '', - thresholdComparator: getHumanReadableComparator(comparator), - threshold: threshold.join(' and '), - negation: isRecovered ? 'NOT ' : '', - }, - }); + return isEsqlQueryRule(searchType) + ? i18n.translate('xpack.stackAlerts.esQuery.esqlAlertTypeContextConditionsDescription', { + defaultMessage: 'Query{negation} documents{groupCondition}', + values: { + groupCondition: group ? ` for group "${group}"` : '', + negation: isRecovered ? ' did NOT match' : ' matched', + }, + }) + : i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', { + defaultMessage: + 'Number of matching documents{groupCondition}{aggCondition} is {negation}{thresholdComparator} {threshold}', + values: { + aggCondition: aggType === 'count' ? '' : ` where ${aggType} of ${aggField}`, + groupCondition: group ? ` for group "${group}"` : '', + thresholdComparator: getHumanReadableComparator(comparator), + threshold: threshold.join(' and '), + negation: isRecovered ? 'NOT ' : '', + }, + }); } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts index c33457fab43b1..43f84acf65e78 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts @@ -17,6 +17,7 @@ import { ISearchStartSearchSource } from '@kbn/data-plugin/common'; import { EsQueryRuleParams } from './rule_type_params'; import { FetchEsQueryOpts } from './lib/fetch_es_query'; import { FetchSearchSourceQueryOpts } from './lib/fetch_search_source_query'; +import { FetchEsqlQueryOpts } from './lib/fetch_esql_query'; const logger = loggerMock.create(); const scopedClusterClientMock = elasticsearchServiceMock.createScopedClusterClient(); @@ -44,6 +45,10 @@ jest.mock('./lib/fetch_search_source_query', () => ({ fetchSearchSourceQuery: (...args: [FetchSearchSourceQueryOpts]) => mockFetchSearchSourceQuery(...args), })); +const mockFetchEsqlQuery = jest.fn(); +jest.mock('./lib/fetch_esql_query', () => ({ + fetchEsqlQuery: (...args: [FetchEsqlQueryOpts]) => mockFetchEsqlQuery(...args), +})); const mockGetRecoveredAlerts = jest.fn().mockReturnValue([]); const mockSetLimitReached = jest.fn(); @@ -86,6 +91,8 @@ describe('es_query executor', () => { excludeHitsFromPreviousRun: true, aggType: 'count', groupBy: 'all', + searchConfiguration: {}, + esqlQuery: { esql: 'test-query' }, }; describe('executor', () => { @@ -171,12 +178,12 @@ describe('es_query executor', () => { }); await executor(coreMock, { ...defaultExecutorOptions, - params: { ...defaultProps, searchConfiguration: {}, searchType: 'searchSource' }, + params: { ...defaultProps, searchType: 'searchSource' }, }); expect(mockFetchSearchSourceQuery).toHaveBeenCalledWith({ ruleId: 'test-rule-id', alertLimit: 1000, - params: { ...defaultProps, searchConfiguration: {}, searchType: 'searchSource' }, + params: { ...defaultProps, searchType: 'searchSource' }, latestTimestamp: undefined, services: { searchSourceClient: searchSourceClientMock, @@ -188,6 +195,42 @@ describe('es_query executor', () => { expect(mockFetchEsQuery).not.toHaveBeenCalled(); }); + it('should call fetchEsqlQuery if searchType is esqlQuery', async () => { + mockFetchEsqlQuery.mockResolvedValueOnce({ + parsedResults: { + results: [ + { + group: 'all documents', + count: 491, + hits: [], + }, + ], + truncated: false, + }, + dateStart: new Date().toISOString(), + dateEnd: new Date().toISOString(), + }); + await executor(coreMock, { + ...defaultExecutorOptions, + params: { ...defaultProps, searchType: 'esqlQuery' }, + }); + expect(mockFetchEsqlQuery).toHaveBeenCalledWith({ + ruleId: 'test-rule-id', + alertLimit: 1000, + params: { ...defaultProps, searchType: 'esqlQuery' }, + services: { + scopedClusterClient: scopedClusterClientMock, + logger, + share: undefined, + dataViews: undefined, + }, + spacePrefix: '', + publicBaseUrl: 'https://localhost:5601', + }); + expect(mockFetchEsQuery).not.toHaveBeenCalled(); + expect(mockFetchSearchSourceQuery).not.toHaveBeenCalled(); + }); + it('should not create alert if compare function returns false for ungrouped alert', async () => { mockFetchEsQuery.mockResolvedValueOnce({ parsedResults: { @@ -455,6 +498,78 @@ describe('es_query executor', () => { expect(mockSetLimitReached).toHaveBeenCalledWith(false); }); + it('should create alert if there are hits for ESQL alert', async () => { + mockFetchEsqlQuery.mockResolvedValueOnce({ + parsedResults: { + results: [ + { + group: 'all documents', + count: 198, + hits: [], + }, + ], + truncated: false, + }, + dateStart: new Date().toISOString(), + dateEnd: new Date().toISOString(), + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + }); + await executor(coreMock, { + ...defaultExecutorOptions, + params: { + ...defaultProps, + searchType: 'esqlQuery', + threshold: [0], + thresholdComparator: '>=' as Comparator, + }, + }); + + expect(mockReport).toHaveBeenCalledTimes(1); + expect(mockReport).toHaveBeenNthCalledWith(1, { + actionGroup: 'query matched', + context: { + conditions: 'Query matched documents', + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is active: + +- Value: 198 +- Conditions Met: Query matched documents over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' matched query", + value: 198, + }, + id: 'query matched', + payload: { + kibana: { + alert: { + evaluation: { + conditions: 'Query matched documents', + value: 198, + }, + reason: `rule 'test-rule-name' is active: + +- Value: 198 +- Conditions Met: Query matched documents over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' matched query", + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + }, + }, + }, + state: { + dateEnd: new Date(mockNow).toISOString(), + dateStart: new Date(mockNow).toISOString(), + latestTimestamp: undefined, + }, + }); + expect(mockSetLimitReached).toHaveBeenCalledTimes(1); + expect(mockSetLimitReached).toHaveBeenCalledWith(false); + }); + it('should set limit as reached if results are truncated', async () => { mockFetchEsQuery.mockResolvedValueOnce({ parsedResults: { @@ -670,6 +785,74 @@ describe('es_query executor', () => { - Value: 0 - Conditions Met: Number of matching documents for group \"host-2\" is NOT greater than or equal to 200 over 5m - Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' recovered", + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + }, + }, + }, + }); + expect(mockSetLimitReached).toHaveBeenCalledTimes(1); + expect(mockSetLimitReached).toHaveBeenCalledWith(false); + }); + + it('should correctly handle recovered alerts for ESQL alert', async () => { + mockGetRecoveredAlerts.mockReturnValueOnce([ + { + alert: { + getId: () => 'query matched', + }, + }, + ]); + mockFetchEsqlQuery.mockResolvedValueOnce({ + parsedResults: { + results: [], + truncated: false, + }, + dateStart: new Date().toISOString(), + dateEnd: new Date().toISOString(), + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + }); + await executor(coreMock, { + ...defaultExecutorOptions, + params: { + ...defaultProps, + searchType: 'esqlQuery', + threshold: [0], + thresholdComparator: '>=' as Comparator, + }, + }); + + expect(mockReport).not.toHaveBeenCalled(); + expect(mockSetAlertData).toHaveBeenCalledTimes(1); + expect(mockSetAlertData).toHaveBeenNthCalledWith(1, { + id: 'query matched', + context: { + conditions: 'Query did NOT match documents', + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is recovered: + +- Value: 0 +- Conditions Met: Query did NOT match documents over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' recovered", + value: 0, + }, + payload: { + kibana: { + alert: { + evaluation: { + conditions: 'Query did NOT match documents', + value: 0, + }, + reason: `rule 'test-rule-name' is recovered: + +- Value: 0 +- Conditions Met: Query did NOT match documents over 5m +- Timestamp: ${new Date(mockNow).toISOString()} - Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, title: "rule 'test-rule-name' recovered", url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts index ae8ae99ba26a2..ac2f619228996 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts @@ -19,15 +19,22 @@ import { EsQueryRuleActionContext, getContextConditionsDescription, } from './action_context'; -import { ExecutorOptions, OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types'; +import { + ExecutorOptions, + OnlyEsQueryRuleParams, + OnlySearchSourceRuleParams, + OnlyEsqlQueryRuleParams, +} from './types'; import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; import { fetchEsQuery } from './lib/fetch_es_query'; import { EsQueryRuleParams } from './rule_type_params'; import { fetchSearchSourceQuery } from './lib/fetch_search_source_query'; -import { isEsQueryRule } from './util'; +import { isEsqlQueryRule, isSearchSourceRule } from './util'; +import { fetchEsqlQuery } from './lib/fetch_esql_query'; export async function executor(core: CoreSetup, options: ExecutorOptions) { - const esQueryRule = isEsQueryRule(options.params.searchType); + const searchSourceRule = isSearchSourceRule(options.params.searchType); + const esqlQueryRule = isEsqlQueryRule(options.params.searchType); const { rule: { id: ruleId, name }, services, @@ -54,34 +61,47 @@ export async function executor(core: CoreSetup, options: ExecutorOptions = {}; for (const result of parsedResults.results) { const alertId = result.group; @@ -105,6 +125,7 @@ export async function executor(core: CoreSetup, options: ExecutorOptions { + const id = 'test-id'; + const { + type, + version, + attributes: { timeFieldName, fields, title }, + } = stubbedSavedObjectIndexPattern(id); + + return new DataView({ + spec: { id, type, version, timeFieldName, fields: JSON.parse(fields), title }, + fieldFormats: fieldFormatsMock, + shortDotsEnable: false, + metaFields: ['_id', '_type', '_score'], + }); +}; + +const defaultParams: OnlyEsqlQueryRuleParams = { + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: Comparator.GT, + threshold: [0], + esqlQuery: { esql: 'from test' }, + excludeHitsFromPreviousRun: false, + searchType: 'esqlQuery', + aggType: 'count', + groupBy: 'all', + timeField: 'time', +}; + +describe('fetchEsqlQuery', () => { + describe('getEsqlQuery', () => { + const dataViewMock = createDataView(); + afterAll(() => { + jest.resetAllMocks(); + }); + + const fakeNow = new Date('2020-02-09T23:15:41.941Z'); + + beforeAll(() => { + jest.resetAllMocks(); + global.Date.now = jest.fn(() => fakeNow.getTime()); + }); + + it('should generate the correct query', async () => { + const params = defaultParams; + const { query, dateStart, dateEnd } = getEsqlQuery(dataViewMock, params, undefined); + + expect(query).toMatchInlineSnapshot(` + Object { + "filter": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "time": Object { + "format": "strict_date_optional_time", + "gt": "2020-02-09T23:10:41.941Z", + "lte": "2020-02-09T23:15:41.941Z", + }, + }, + }, + ], + }, + }, + "query": "from test", + } + `); + expect(dateStart).toMatch('2020-02-09T23:10:41.941Z'); + expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z'); + }); + + it('should generate the correct query with the alertLimit', async () => { + const params = defaultParams; + const { query, dateStart, dateEnd } = getEsqlQuery(dataViewMock, params, 100); + + expect(query).toMatchInlineSnapshot(` + Object { + "filter": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "time": Object { + "format": "strict_date_optional_time", + "gt": "2020-02-09T23:10:41.941Z", + "lte": "2020-02-09T23:15:41.941Z", + }, + }, + }, + ], + }, + }, + "query": "from test | limit 100", + } + `); + expect(dateStart).toMatch('2020-02-09T23:10:41.941Z'); + expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z'); + }); + }); +}); diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts new file mode 100644 index 0000000000000..07192254ca34f --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataView, DataViewsContract, getTime } from '@kbn/data-plugin/common'; +import { parseAggregationResults } from '@kbn/triggers-actions-ui-plugin/common'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { IScopedClusterClient, Logger } from '@kbn/core/server'; +import { OnlyEsqlQueryRuleParams } from '../types'; +import { EsqlTable, toEsQueryHits } from '../../../../common'; + +export interface FetchEsqlQueryOpts { + ruleId: string; + alertLimit: number | undefined; + params: OnlyEsqlQueryRuleParams; + spacePrefix: string; + publicBaseUrl: string; + services: { + logger: Logger; + scopedClusterClient: IScopedClusterClient; + share: SharePluginStart; + dataViews: DataViewsContract; + }; +} + +export async function fetchEsqlQuery({ + ruleId, + alertLimit, + params, + services, + spacePrefix, + publicBaseUrl, +}: FetchEsqlQueryOpts) { + const { logger, scopedClusterClient, dataViews } = services; + const esClient = scopedClusterClient.asCurrentUser; + const dataView = await dataViews.create({ + timeFieldName: params.timeField, + }); + + const { query, dateStart, dateEnd } = getEsqlQuery(dataView, params, alertLimit); + + logger.debug(`ESQL query rule (${ruleId}) query: ${JSON.stringify(query)}`); + + const response = await esClient.transport.request({ + method: 'POST', + path: '/_esql', + body: query, + }); + + const link = `${publicBaseUrl}${spacePrefix}/app/management/insightsAndAlerting/triggersActions/rule/${ruleId}`; + + return { + link, + numMatches: Number(response.values.length), + parsedResults: parseAggregationResults({ + isCountAgg: true, + isGroupAgg: false, + esResult: { + took: 0, + timed_out: false, + _shards: { failed: 0, successful: 0, total: 0 }, + hits: toEsQueryHits(response), + }, + resultLimit: alertLimit, + }), + dateStart, + dateEnd, + }; +} + +export const getEsqlQuery = ( + dataView: DataView, + params: OnlyEsqlQueryRuleParams, + alertLimit: number | undefined +) => { + const timeRange = { + from: `now-${params.timeWindowSize}${params.timeWindowUnit}`, + to: 'now', + }; + const timerangeFilter = getTime(dataView, timeRange); + const dateStart = timerangeFilter?.query.range[params.timeField].gte; + const dateEnd = timerangeFilter?.query.range[params.timeField].lte; + const rangeFilter: unknown[] = [ + { + range: { + [params.timeField]: { + lte: dateEnd, + gt: dateStart, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const query = { + query: alertLimit ? `${params.esqlQuery.esql} | limit ${alertLimit}` : params.esqlQuery.esql, + filter: { + bool: { + filter: rangeFilter, + }, + }, + }; + return { + query, + dateStart, + dateEnd, + }; +}; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts index 199aa47240c54..6adb6e092d56f 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts @@ -19,7 +19,11 @@ import type { ESSearchResponse, ESSearchRequest } from '@kbn/es-types'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { coreMock } from '@kbn/core/server/mocks'; import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; -import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types'; +import { + OnlyEsqlQueryRuleParams, + OnlyEsQueryRuleParams, + OnlySearchSourceRuleParams, +} from './types'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { Comparator } from '../../../common/comparator_types'; import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; @@ -108,6 +112,10 @@ describe('ruleType', () => { "description": "The indices the rule queries.", "name": "index", }, + Object { + "description": "ESQL query field used to fetch data from Elasticsearch.", + "name": "esqlQuery", + }, ], } `); @@ -596,10 +604,6 @@ describe('ruleType', () => { groupBy: 'all', }; - afterAll(() => { - jest.resetAllMocks(); - }); - it('validator succeeds with valid search source params', async () => { expect(ruleType.validate.params.validate(defaultParams)).toBeTruthy(); }); @@ -710,6 +714,144 @@ describe('ruleType', () => { ); }); }); + + describe('ESQL query', () => { + const dataViewMock = { + id: 'test-id', + title: 'test-title', + timeFieldName: 'time-field', + fields: [ + { + name: 'message', + type: 'string', + displayName: 'message', + scripted: false, + filterable: false, + aggregatable: false, + }, + { + name: 'timestamp', + type: 'date', + displayName: 'timestamp', + scripted: false, + filterable: false, + aggregatable: false, + }, + ], + toSpec: () => { + return { id: 'test-id', title: 'test-title', timeFieldName: 'timestamp', fields: [] }; + }, + }; + const defaultParams: OnlyEsqlQueryRuleParams = { + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: Comparator.GT, + threshold: [0], + esqlQuery: { esql: 'test' }, + timeField: 'timestamp', + searchType: 'esqlQuery', + excludeHitsFromPreviousRun: true, + aggType: 'count', + groupBy: 'all', + }; + + it('validator succeeds with valid ESQL query params', async () => { + expect(ruleType.validate.params.validate(defaultParams)).toBeTruthy(); + }); + + it('validator fails with invalid ESQL query params - esQuery provided', async () => { + const paramsSchema = ruleType.validate.params; + const params: Partial> = { + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: Comparator.LT, + threshold: [0], + esQuery: '', + searchType: 'esqlQuery', + aggType: 'count', + groupBy: 'all', + }; + + expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( + `"[esQuery]: a value wasn't expected to be present"` + ); + }); + + it('rule executor handles no documents returned by ES', async () => { + const params = defaultParams; + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + + (ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({ + ...dataViewMock.toSpec(), + toSpec: () => dataViewMock.toSpec(), + }); + + const searchResult = { + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'message', type: 'keyword' }, + ], + values: [], + }; + ruleServices.scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce( + searchResult + ); + + await invokeExecutor({ params, ruleServices }); + expect(ruleServices.alertsClient.report).not.toHaveBeenCalled(); + }); + + it('rule executor schedule actions when condition met', async () => { + const params = defaultParams; + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + + (ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({ + ...dataViewMock.toSpec(), + toSpec: () => dataViewMock.toSpec(), + }); + + const searchResult = { + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'message', type: 'keyword' }, + ], + values: [ + ['timestamp', 'message'], + ['timestamp', 'message'], + ['timestamp', 'message'], + ], + }; + ruleServices.scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce( + searchResult + ); + + await invokeExecutor({ params, ruleServices }); + + expect(ruleServices.alertsClient.report).toHaveBeenCalledTimes(1); + + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + actionGroup: 'query matched', + id: 'query matched', + payload: expect.objectContaining({ + kibana: { + alert: { + url: expect.any(String), + reason: expect.any(String), + title: "rule 'rule-name' matched query", + evaluation: { + conditions: 'Query matched documents', + value: 3, + }, + }, + }, + }), + }) + ); + }); + }); }); function generateResults( @@ -756,7 +898,7 @@ async function invokeExecutor({ ruleServices, state, }: { - params: OnlySearchSourceRuleParams | OnlyEsQueryRuleParams; + params: OnlySearchSourceRuleParams | OnlyEsQueryRuleParams | OnlyEsqlQueryRuleParams; ruleServices: RuleExecutorServicesMock; state?: EsQueryRuleState; }) { diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts index 01d6ee1497b3c..eabe7bf346669 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts @@ -25,7 +25,7 @@ import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { ExecutorOptions } from './types'; import { ActionGroupId, ES_QUERY_ID } from './constants'; import { executor } from './executor'; -import { isEsQueryRule } from './util'; +import { isSearchSourceRule } from './util'; export function getRuleType( core: CoreSetup @@ -134,6 +134,13 @@ export function getRuleType( } ); + const actionVariableEsqlQueryLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextEsqlQueryLabel', + { + defaultMessage: 'ESQL query field used to fetch data from Elasticsearch.', + } + ); + const actionVariableContextLinkLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextLinkLabel', { @@ -179,25 +186,27 @@ export function getRuleType( { name: 'searchConfiguration', description: actionVariableSearchConfigurationLabel }, { name: 'esQuery', description: actionVariableContextQueryLabel }, { name: 'index', description: actionVariableContextIndexLabel }, + { name: 'esqlQuery', description: actionVariableEsqlQueryLabel }, ], }, useSavedObjectReferences: { extractReferences: (params) => { - if (isEsQueryRule(params.searchType)) { - return { params: params as EsQueryRuleParamsExtractedParams, references: [] }; + if (isSearchSourceRule(params.searchType)) { + const [searchConfiguration, references] = extractReferences(params.searchConfiguration); + const newParams = { ...params, searchConfiguration } as EsQueryRuleParamsExtractedParams; + return { params: newParams, references }; } - const [searchConfiguration, references] = extractReferences(params.searchConfiguration); - const newParams = { ...params, searchConfiguration } as EsQueryRuleParamsExtractedParams; - return { params: newParams, references }; + + return { params: params as EsQueryRuleParamsExtractedParams, references: [] }; }, injectReferences: (params, references) => { - if (isEsQueryRule(params.searchType)) { - return params; + if (isSearchSourceRule(params.searchType)) { + return { + ...params, + searchConfiguration: injectReferences(params.searchConfiguration, references), + }; } - return { - ...params, - searchConfiguration: injectReferences(params.searchConfiguration, references), - }; + return params; }, }, minimumLicenseRequired: 'basic', diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type_params.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type_params.ts index b9380eeafe304..1897ebad6c1ee 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type_params.ts @@ -18,6 +18,7 @@ import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { ComparatorFnNames } from '../../../common'; import { Comparator } from '../../../common/comparator_types'; import { getComparatorSchemaType } from '../lib/comparator'; +import { isEsqlQueryRule, isSearchSourceRule } from './util'; export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000; @@ -50,9 +51,12 @@ const EsQueryRuleParamsSchemaProperties = { termField: schema.maybe(schema.string({ minLength: 1 })), // limit on number of groups returned termSize: schema.maybe(schema.number({ min: 1 })), - searchType: schema.oneOf([schema.literal('searchSource'), schema.literal('esQuery')], { - defaultValue: 'esQuery', - }), + searchType: schema.oneOf( + [schema.literal('searchSource'), schema.literal('esQuery'), schema.literal('esqlQuery')], + { + defaultValue: 'esQuery', + } + ), timeField: schema.conditional( schema.siblingRef('searchType'), schema.literal('esQuery'), @@ -79,6 +83,13 @@ const EsQueryRuleParamsSchemaProperties = { schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), schema.never() ), + // esqlQuery rule params only + esqlQuery: schema.conditional( + schema.siblingRef('searchType'), + schema.literal('esqlQuery'), + schema.object({ esql: schema.string({ minLength: 1 }) }), + schema.never() + ), }; export const EsQueryRuleParamsSchema = schema.object(EsQueryRuleParamsSchemaProperties, { @@ -142,7 +153,7 @@ function validateParams(anyParams: unknown): string | undefined { } } - if (searchType === 'searchSource') { + if (isSearchSourceRule(searchType) || isEsqlQueryRule(searchType)) { return; } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts index b20f52f03ebe5..5f3ec1ba5e240 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts @@ -11,15 +11,26 @@ import { ActionContext } from './action_context'; import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params'; import { ActionGroupId } from './constants'; -export type OnlyEsQueryRuleParams = Omit & { +export type OnlyEsQueryRuleParams = Omit & { searchType: 'esQuery'; timeField: string; }; -export type OnlySearchSourceRuleParams = Omit & { +export type OnlySearchSourceRuleParams = Omit< + EsQueryRuleParams, + 'esQuery' | 'index' | 'esqlQuery' +> & { searchType: 'searchSource'; }; +export type OnlyEsqlQueryRuleParams = Omit< + EsQueryRuleParams, + 'esQuery' | 'index' | 'searchConfiguration' +> & { + searchType: 'esqlQuery'; + timeField: string; +}; + export type ExecutorOptions

= RuleExecutorOptions< P, EsQueryRuleState, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.ts index 064a7f64b4c32..d10218fea7d4f 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.ts @@ -8,5 +8,13 @@ import { EsQueryRuleParams } from './rule_type_params'; export function isEsQueryRule(searchType: EsQueryRuleParams['searchType']) { - return searchType !== 'searchSource'; + return searchType === 'esQuery'; +} + +export function isSearchSourceRule(searchType: EsQueryRuleParams['searchType']) { + return searchType === 'searchSource'; +} + +export function isEsqlQueryRule(searchType: EsQueryRuleParams['searchType']) { + return searchType === 'esqlQuery'; } diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index 207e883aa8902..08a4f0ca99e9c 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -44,6 +44,9 @@ "@kbn/discover-plugin", "@kbn/rule-data-utils", "@kbn/alerts-as-data-utils", + "@kbn/text-based-languages", + "@kbn/text-based-editor", + "@kbn/expressions-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/stack_connectors/common/experimental_features.ts b/x-pack/plugins/stack_connectors/common/experimental_features.ts new file mode 100644 index 0000000000000..61b63cff732a6 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/experimental_features.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type ExperimentalFeatures = typeof allowedExperimentalValues; + +/** + * A list of allowed values that can be used in `xpack.stack_connectors.enableExperimental`. + * This object is then used to validate and parse the value entered. + */ +export const allowedExperimentalValues = Object.freeze({ + isMustacheAutocompleteOn: false, +}); + +type ExperimentalConfigKeys = Array; +type Mutable = { -readonly [P in keyof T]: T[P] }; + +const InvalidExperimentalValue = class extends Error {}; +const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly; + +/** + * Parses the string value used in `xpack.stack_connectors.enableExperimental` kibana configuration, + * which should be a string of values delimited by a comma (`,`) + * + * @param configValue + * @throws InvalidExperimentalValue + */ +export const parseExperimentalConfigValue = (configValue: string[]): ExperimentalFeatures => { + const enabledFeatures: Mutable> = {}; + + for (const value of configValue) { + if (!isValidExperimentalValue(value)) { + throw new InvalidExperimentalValue(`[${value}] is not valid.`); + } + // @ts-expect-error ts upgrade v4.7.4 + enabledFeatures[value as keyof ExperimentalFeatures] = true; + } + + return { + ...allowedExperimentalValues, + ...enabledFeatures, + }; +}; + +export const isValidExperimentalValue = (value: string): boolean => { + return allowedKeys.includes(value as keyof ExperimentalFeatures); +}; + +export const getExperimentalAllowedValues = (): string[] => [...allowedKeys]; diff --git a/x-pack/plugins/stack_connectors/common/types.ts b/x-pack/plugins/stack_connectors/common/types.ts new file mode 100644 index 0000000000000..6bc9ef7eb72e6 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface StackConnectorsConfigType { + enableExperimental: string[]; +} diff --git a/x-pack/plugins/stack_connectors/kibana.jsonc b/x-pack/plugins/stack_connectors/kibana.jsonc index dc4023890d656..da8e973b6f990 100644 --- a/x-pack/plugins/stack_connectors/kibana.jsonc +++ b/x-pack/plugins/stack_connectors/kibana.jsonc @@ -13,6 +13,7 @@ "requiredPlugins": [ "actions", "esUiShared", + "kibanaReact", "triggersActionsUi" ], "extraPublicDirs": [ diff --git a/x-pack/plugins/stack_connectors/public/common/experimental_features_service.ts b/x-pack/plugins/stack_connectors/public/common/experimental_features_service.ts new file mode 100644 index 0000000000000..c701af1376dbb --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/common/experimental_features_service.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExperimentalFeatures } from '../../common/experimental_features'; + +export class ExperimentalFeaturesService { + private static experimentalFeatures?: ExperimentalFeatures; + + public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) { + this.experimentalFeatures = experimentalFeatures; + } + + public static get(): ExperimentalFeatures { + if (!this.experimentalFeatures) { + this.throwUninitializedError(); + } + + return this.experimentalFeatures; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Experimental features services not initialized - are you trying to import this module from outside of the stack connectors?' + ); + } +} diff --git a/x-pack/plugins/stack_connectors/public/common/get_experimental_features.ts b/x-pack/plugins/stack_connectors/public/common/get_experimental_features.ts new file mode 100644 index 0000000000000..41a321bbd2981 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/common/get_experimental_features.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 { + ExperimentalFeatures, + isValidExperimentalValue, + getExperimentalAllowedValues, +} from '../../common/experimental_features'; +import { ExperimentalFeaturesService } from './experimental_features_service'; + +const allowedExperimentalValueKeys = getExperimentalAllowedValues(); + +export const getIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => { + if (!isValidExperimentalValue(feature)) { + throw new Error( + `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValueKeys.join( + ', ' + )}` + ); + } + + return ExperimentalFeaturesService.get()[feature]; +}; diff --git a/x-pack/plugins/stack_connectors/public/components/text_area_with_autocomplete.tsx b/x-pack/plugins/stack_connectors/public/components/text_area_with_autocomplete.tsx new file mode 100644 index 0000000000000..0f51b3ced86c6 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/components/text_area_with_autocomplete.tsx @@ -0,0 +1,366 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import getCaretCoordinates from 'textarea-caret'; +import { Properties } from 'csstype'; +import { + EuiTextArea, + EuiFormRow, + EuiSelectable, + EuiSelectableOption, + EuiPortal, + EuiHighlight, + EuiOutsideClickDetector, + useEuiTheme, + useEuiBackgroundColor, +} from '@elastic/eui'; +import { ActionVariable } from '@kbn/alerting-plugin/common'; +import { AddMessageVariables } from '@kbn/alerts-ui-shared'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { filterSuggestions } from '../lib/filter_suggestions_for_autocomplete'; +import { templateActionVariable } from '../lib/template_action_variable'; + +export interface TextAreaWithAutocompleteProps { + editAction: (property: string, value: any, index: number) => void; + errors?: string[]; + index: number; + inputTargetValue?: string; + isDisabled?: boolean; + label: string; + messageVariables?: ActionVariable[]; + paramsProperty: string; +} +const selectableListProps = { className: 'euiSelectableMsgAutoComplete' }; + +export const TextAreaWithAutocomplete: React.FunctionComponent = ({ + editAction, + errors, + index, + inputTargetValue, + isDisabled = false, + label, + messageVariables, + paramsProperty, +}) => { + const { euiTheme } = useEuiTheme(); + const backgroundColor = useEuiBackgroundColor('plain'); + + const textAreaRef = React.useRef(null); + const selectableRef = React.useRef(null); + + const [matches, setMatches] = useState([]); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, height: 0, width: 0 }); + const [isListOpen, setListOpen] = useState(false); + const [autoCompleteIndex, setAutoCompleteIndex] = useState(-1); + const [selectableHasFocus, setSelectableHasFocus] = useState(false); + const [searchWord, setSearchWord] = useState(''); + + const optionsToShow: EuiSelectableOption[] = useMemo(() => { + return matches?.map((variable) => ({ + label: variable, + data: { + description: variable, + }, + 'data-test-subj': `${variable}-selectableOption`, + })); + }, [matches]); + + const closeList = useCallback((doNotResetAutoCompleteIndex = false) => { + if (!doNotResetAutoCompleteIndex) { + setAutoCompleteIndex(-1); + } + setListOpen(false); + setSelectableHasFocus(false); + }, []); + + const onOptionPick = useCallback( + (newOptions: EuiSelectableOption[]) => { + if (!textAreaRef.current) return; + const { value, selectionStart, scrollTop } = textAreaRef.current; + const lastSpaceIndex = value.slice(0, selectionStart).lastIndexOf(' '); + const lastOpenDoubleCurlyBracketsIndex = value.slice(0, selectionStart).lastIndexOf('{{'); + const currentWordStartIndex = Math.max(lastSpaceIndex, lastOpenDoubleCurlyBracketsIndex); + + const checkedElement = newOptions.find(({ checked }) => checked === 'on'); + if (checkedElement) { + const newInputText = + value.slice(0, currentWordStartIndex) + + '{{' + + checkedElement.label + + '}}' + + value.slice(selectionStart); + + editAction(paramsProperty, newInputText.trim(), index); + setMatches([]); + closeList(); + textAreaRef.current.focus(); + // We use setTimeout here, because editAction is async function and we need to wait before it executes + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.selectionStart = + currentWordStartIndex + checkedElement.label.length + 4; + textAreaRef.current.selectionEnd = textAreaRef.current.selectionStart; + textAreaRef.current.scrollTop = scrollTop; + } + }, 0); + } + }, + [editAction, index, paramsProperty, closeList] + ); + + const recalcMenuPosition = useCallback(() => { + if (!textAreaRef.current) return; + const newPosition = getCaretCoordinates( + textAreaRef.current, + textAreaRef.current.selectionStart + ); + const textAreaClientRect = textAreaRef.current?.getBoundingClientRect(); + + const top = + textAreaClientRect.top - + textAreaRef.current.scrollTop + + window.scrollY + + newPosition.top + + newPosition.height; + const left = textAreaClientRect.left + window.pageXOffset; + const height = newPosition.height; + const width = textAreaClientRect.width; + setPopupPosition({ top, left, width, height }); + setListOpen(true); + }, []); + + const onChangeWithMessageVariable = useCallback(() => { + if (!textAreaRef.current) return; + const { value, selectionStart } = textAreaRef.current; + const lastTwoLetter = value.slice(selectionStart - 2, selectionStart); + + const currentWord = + autoCompleteIndex !== -1 ? value.slice(autoCompleteIndex, selectionStart) : ''; + + if (lastTwoLetter === '{{' || currentWord.startsWith('{{')) { + if (lastTwoLetter === '{{') { + setAutoCompleteIndex(selectionStart - 2); + } + const filteredMatches = filterSuggestions({ + actionVariablesList: messageVariables + ?.filter(({ deprecated }) => !deprecated) + .map(({ name }) => name), + propertyPath: currentWord.slice(2), + }); + setSearchWord(currentWord.slice(2)); + setMatches(filteredMatches); + setTimeout(() => recalcMenuPosition(), 0); + } else if (lastTwoLetter === '}}') { + closeList(); + } else { + setMatches([]); + } + editAction(paramsProperty, value, index); + }, [ + autoCompleteIndex, + closeList, + editAction, + index, + messageVariables, + paramsProperty, + recalcMenuPosition, + ]); + + const textareaOnKeyPress = useCallback( + (event) => { + if (selectableRef.current && isListOpen) { + if (!selectableHasFocus && (event.code === 'ArrowUp' || event.code === 'ArrowDown')) { + event.preventDefault(); + event.stopPropagation(); + selectableRef.current.onFocus(); + setSelectableHasFocus(true); + } else if (event.code === 'ArrowUp') { + event.preventDefault(); + event.stopPropagation(); + selectableRef.current.incrementActiveOptionIndex(-1); + } else if (event.code === 'ArrowDown') { + event.preventDefault(); + event.stopPropagation(); + selectableRef.current.incrementActiveOptionIndex(1); + } else if (event.code === 'Escape') { + event.preventDefault(); + event.stopPropagation(); + closeList(); + } else if (event.code === 'Enter' || event.code === 'Space') { + const optionIndex = selectableRef.current.state.activeOptionIndex; + onOptionPick( + optionsToShow.map((ots, idx) => { + if (idx === optionIndex) { + return { + ...ots, + checked: 'on', + }; + } + return ots; + }) + ); + closeList(); + } + } else { + setSelectableHasFocus((prevValue) => { + if (prevValue) { + return false; + } + return prevValue; + }); + } + }, + [closeList, isListOpen, onOptionPick, optionsToShow, selectableHasFocus] + ); + + const clickOutSideTextArea = useCallback( + (event) => { + const box = document + .querySelector('.euiSelectableMsgAutoComplete') + ?.getBoundingClientRect() || { + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + if ( + event.clientX > box.left && + event.clientX < box.right && + event.clientY > box.top && + event.clientY < box.bottom + ) { + return; + } + closeList(); + }, + [closeList] + ); + + const onSelectMessageVariable = useCallback( + (variable: ActionVariable) => { + if (!textAreaRef.current) return; + const { selectionStart: startPosition, selectionEnd: endPosition } = textAreaRef.current; + const templatedVar = templateActionVariable(variable); + + const newValue = + (inputTargetValue ?? '').substring(0, startPosition) + + templatedVar + + (inputTargetValue ?? '').substring(endPosition, (inputTargetValue ?? '').length); + + editAction(paramsProperty, newValue, index); + }, + [editAction, index, inputTargetValue, paramsProperty] + ); + + const renderSelectableOption = (option: any) => { + if (searchWord) { + return {option.label}; + } + return option.label; + }; + + const selectableStyle: Properties = useMemo( + () => ({ + position: 'absolute', + top: popupPosition.top, + width: popupPosition.width, + left: popupPosition.left, + border: `${euiTheme.border.width.thin} solid ${euiTheme.border.color}`, + background: backgroundColor, + zIndex: euiThemeVars.euiZLevel1, + }), + [ + backgroundColor, + euiTheme.border.color, + euiTheme.border.width.thin, + popupPosition.left, + popupPosition.top, + popupPosition.width, + ] + ); + + const onFocus = useCallback(() => setListOpen(true), []); + const onBlur = useCallback(() => { + if (!inputTargetValue && !isListOpen) { + editAction(paramsProperty, '', index); + } + }, [editAction, index, inputTargetValue, isListOpen, paramsProperty]); + const onClick = useCallback(() => closeList(), [closeList]); + + const onScroll = useCallback( + (evt) => { + // FUTURE ENGINEER -> we need to make sure to not close the autocomplete option list + if (selectableRef?.current?.listId !== evt.target?.firstElementChild?.id) { + closeList(true); + } + }, + [closeList] + ); + + useEffect(() => { + window.addEventListener('scroll', onScroll, { passive: true, capture: true }); + return () => { + window.removeEventListener('scroll', onScroll, { capture: true }); + }; + }, [onScroll]); + + return ( + 0 && inputTargetValue !== undefined} + label={label} + labelAppend={ + + } + > + <> + + 0 && inputTargetValue !== undefined} + name={paramsProperty} + value={inputTargetValue || ''} + data-test-subj={`${paramsProperty}TextArea`} + onChange={onChangeWithMessageVariable} + onFocus={onFocus} + onKeyDown={textareaOnKeyPress} + onBlur={onBlur} + onClick={onClick} + /> + + {matches.length > 0 && isListOpen && ( + + 5 ? 32 * 5.5 : matches.length * 32} + options={optionsToShow} + onChange={onOptionPick} + singleSelection + renderOption={renderSelectableOption} + listProps={selectableListProps} + > + {(list) => list} + + + )} + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { TextAreaWithAutocomplete as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx index a0a41a57a8331..76cc3b136455a 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx @@ -7,10 +7,35 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { render, fireEvent, screen } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; import EmailParamsFields from './email_params'; +import { getIsExperimentalFeatureEnabled } from '../../common/get_experimental_features'; + +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + useKibana: jest.fn(), +})); +jest.mock('../../common/get_experimental_features'); + +const useKibanaMock = useKibana as jest.Mock; +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + triggersActionsUi: triggersActionsUiMock.createStart(), + }, + }); +}; describe('EmailParamsFields renders', () => { - test('all params fields is rendered', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockKibana(); + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + }); + + test('all params fields is rendered', async () => { const actionParams = { cc: [], bcc: [], @@ -19,21 +44,22 @@ describe('EmailParamsFields renders', () => { message: 'test message', }; - const wrapper = mountWithIntl( - {}} - index={0} - /> + render( + + {}} + defaultMessage={'Some default message'} + index={0} + /> + ); - expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="toEmailAddressInput"]').first().prop('selectedOptions') - ).toStrictEqual([{ label: 'test@test.com' }]); - expect(wrapper.find('[data-test-subj="subjectInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); + expect(screen.getByTestId('toEmailAddressInput')).toBeVisible(); + expect(screen.getByTestId('toEmailAddressInput').textContent).toStrictEqual('test@test.com'); + expect(screen.getByTestId('subjectInput')).toBeVisible(); + expect(await screen.findByTestId('messageTextArea')).toBeVisible(); }); test('message param field is rendered with default value if not set', () => { @@ -95,36 +121,57 @@ describe('EmailParamsFields renders', () => { }; const editAction = jest.fn(); - const wrapper = mountWithIntl( - + const { rerender } = render( + + + ); expect(editAction).toHaveBeenCalledWith('message', 'Some default message', 0); // simulate value being updated const valueToSimulate = 'some new value'; - wrapper - .find('[data-test-subj="messageTextArea"]') - .last() - .simulate('change', { target: { value: valueToSimulate } }); - expect(editAction).toHaveBeenCalledWith('message', valueToSimulate, 0); - wrapper.setProps({ - actionParams: { - ...actionParams, - message: valueToSimulate, - }, + fireEvent.change(screen.getByTestId('messageTextArea'), { + target: { value: valueToSimulate }, }); - // simulate default changing - wrapper.setProps({ - defaultMessage: 'Some different default message', - }); + expect(editAction).toHaveBeenCalledWith('message', valueToSimulate, 0); + + rerender( + + + + ); + + rerender( + + + + ); expect(editAction).not.toHaveBeenCalledWith('message', 'Some different default message', 0); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx index a8df45ba0e33f..2100e2b0d823c 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx @@ -5,16 +5,18 @@ * 2.0. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiComboBox, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public'; import { - TextAreaWithMessageVariables, TextFieldWithMessageVariables, + TextAreaWithMessageVariables, } from '@kbn/triggers-actions-ui-plugin/public'; import { EmailActionParams } from '../types'; +import { getIsExperimentalFeatureEnabled } from '../../common/get_experimental_features'; +import { TextAreaWithAutocomplete } from '../../components/text_area_with_autocomplete'; const noop = () => {}; @@ -31,6 +33,7 @@ export const EmailParamsFields = ({ showEmailSubjectAndMessage = true, useDefaultMessage, }: ActionParamsProps) => { + const isMustacheAutocompleteOn = getIsExperimentalFeatureEnabled('isMustacheAutocompleteOn'); const { to, cc, bcc, subject, message } = actionParams; const toOptions = to ? to.map((label: string) => ({ label })) : []; const ccOptions = cc ? cc.map((label: string) => ({ label })) : []; @@ -60,6 +63,11 @@ export const EmailParamsFields = ({ const isCCInvalid: boolean = errors.cc !== undefined && errors.cc.length > 0 && cc !== undefined; const isBCCInvalid: boolean = errors.bcc !== undefined && errors.bcc.length > 0 && bcc !== undefined; + + const TextAreaComponent = useMemo(() => { + return isMustacheAutocompleteOn ? TextAreaWithAutocomplete : TextAreaWithMessageVariables; + }, [isMustacheAutocompleteOn]); + return ( <> )} {showEmailSubjectAndMessage && ( - new StackConnectorsPublicPlugin(); +export const plugin = (context: PluginInitializerContext) => + new StackConnectorsPublicPlugin(context); diff --git a/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.test.ts b/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.test.ts new file mode 100644 index 0000000000000..b51aa70475515 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.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 { filterSuggestions } from './filter_suggestions_for_autocomplete'; + +const defaultActionVariablesList = [ + 'kibana.alert.id', + 'kibana.context.cloud.group', + 'context.container', + 'context.originalAlertState', + 'date', + 'rule.spaceId', + 'kibana.alertActionGroup', + 'tags', +]; +describe('Unit tests for filterSuggestions function', () => { + test('should return empty list if actionVariablesList argument is undefined', () => { + expect(filterSuggestions({ propertyPath: 'alert.id' })).toEqual([]); + }); + + test('should return full sorted list of suggestions if propertyPath is empty string', () => { + expect( + filterSuggestions({ actionVariablesList: defaultActionVariablesList, propertyPath: '' }) + ).toEqual([ + 'context', + 'context.container', + 'context.originalAlertState', + 'date', + 'kibana', + 'kibana.alert', + 'kibana.alert.id', + 'kibana.alertActionGroup', + 'kibana.context', + 'kibana.context.cloud', + 'kibana.context.cloud.group', + 'rule', + 'rule.spaceId', + 'tags', + ]); + }); + + test('should return sorted of filtered suggestions, v1', () => { + expect( + filterSuggestions({ actionVariablesList: defaultActionVariablesList, propertyPath: 'ki' }) + ).toEqual([ + 'kibana', + 'kibana.alert', + 'kibana.alert.id', + 'kibana.alertActionGroup', + 'kibana.context', + 'kibana.context.cloud', + 'kibana.context.cloud.group', + ]); + }); + + test('should return sorted of filtered suggestions, v2', () => { + expect( + filterSuggestions({ + actionVariablesList: defaultActionVariablesList, + propertyPath: 'kibana.al', + }) + ).toEqual(['kibana.alert', 'kibana.alert.id', 'kibana.alertActionGroup']); + }); + + test('should return sorted of filtered suggestions, v3', () => { + expect( + filterSuggestions({ + actionVariablesList: defaultActionVariablesList, + propertyPath: 'kibana.context.cloud.g', + }) + ).toEqual(['kibana.context.cloud.group']); + }); +}); diff --git a/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.ts b/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.ts new file mode 100644 index 0000000000000..80362d2b6770b --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.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. + */ + +export const filterSuggestions = ({ + actionVariablesList, + propertyPath, +}: { + actionVariablesList?: string[]; + propertyPath: string; +}) => { + if (!actionVariablesList) return []; + const allSuggestions: string[] = []; + actionVariablesList.forEach((suggestion: string) => { + const splittedWords = suggestion.split('.'); + for (let i = 0; i < splittedWords.length; i++) { + const currentSuggestion = splittedWords.slice(0, i + 1).join('.'); + if (!allSuggestions.includes(currentSuggestion)) { + allSuggestions.push(currentSuggestion); + } + } + }); + return allSuggestions.sort().filter((suggestion) => suggestion.startsWith(propertyPath)); +}; diff --git a/x-pack/plugins/stack_connectors/public/lib/template_action_variable.test.ts b/x-pack/plugins/stack_connectors/public/lib/template_action_variable.test.ts new file mode 100644 index 0000000000000..3ac06967b641a --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/lib/template_action_variable.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { templateActionVariable } from './template_action_variable'; + +describe('templateActionVariable', () => { + const actionVariable = { + name: 'myVar', + description: 'My variable description', + }; + + test('variable returns with double braces by default', () => { + expect(templateActionVariable(actionVariable)).toEqual('{{myVar}}'); + }); + + test('variable returns with triple braces when specified', () => { + expect( + templateActionVariable({ ...actionVariable, useWithTripleBracesInTemplates: true }) + ).toEqual('{{{myVar}}}'); + }); +}); diff --git a/x-pack/plugins/stack_connectors/public/lib/template_action_variable.ts b/x-pack/plugins/stack_connectors/public/lib/template_action_variable.ts new file mode 100644 index 0000000000000..887564a8213c4 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/lib/template_action_variable.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionVariable } from '@kbn/alerting-plugin/common'; + +export function templateActionVariable(variable: ActionVariable) { + return variable.useWithTripleBracesInTemplates + ? `{{{${variable.name}}}}` + : `{{${variable.name}}}`; +} diff --git a/x-pack/plugins/stack_connectors/public/plugin.ts b/x-pack/plugins/stack_connectors/public/plugin.ts index bc9d855a14303..3c153d0de2573 100644 --- a/x-pack/plugins/stack_connectors/public/plugin.ts +++ b/x-pack/plugins/stack_connectors/public/plugin.ts @@ -5,10 +5,16 @@ * 2.0. */ -import { CoreSetup, Plugin } from '@kbn/core/public'; +import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { TriggersAndActionsUIPublicPluginSetup } from '@kbn/triggers-actions-ui-plugin/public'; import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { registerConnectorTypes } from './connector_types'; +import { ExperimentalFeaturesService } from './common/experimental_features_service'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; +import { StackConnectorsConfigType } from '../common/types'; export type Setup = void; export type Start = void; @@ -21,6 +27,13 @@ export interface StackConnectorsPublicSetupDeps { export class StackConnectorsPublicPlugin implements Plugin { + private config: StackConnectorsConfigType; + readonly experimentalFeatures: ExperimentalFeatures; + + constructor(ctx: PluginInitializerContext) { + this.config = ctx.config.get(); + this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []); + } public setup(core: CoreSetup, { triggersActionsUi, actions }: StackConnectorsPublicSetupDeps) { registerConnectorTypes({ connectorTypeRegistry: triggersActionsUi.actionTypeRegistry, @@ -28,6 +41,7 @@ export class StackConnectorsPublicPlugin validateEmailAddresses: actions.validateEmailAddresses, }, }); + ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures }); } public start() {} diff --git a/x-pack/plugins/stack_connectors/server/config.ts b/x-pack/plugins/stack_connectors/server/config.ts new file mode 100644 index 0000000000000..d58e58b0e450c --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/config.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 { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from '@kbn/core/server'; + +import { + ExperimentalFeatures, + getExperimentalAllowedValues, + isValidExperimentalValue, + parseExperimentalConfigValue, +} from '../common/experimental_features'; + +const allowedExperimentalValues = getExperimentalAllowedValues(); + +export const configSchema = schema.object({ + enableExperimental: schema.arrayOf(schema.string(), { + defaultValue: () => [], + validate(list) { + for (const key of list) { + if (!isValidExperimentalValue(key)) { + return `[${key}] is not allowed. Allowed values are: ${allowedExperimentalValues.join( + ', ' + )}`; + } + } + }, + }), +}); + +export type ConfigSchema = TypeOf; + +export type ConfigType = ConfigSchema & { + experimentalFeatures: ExperimentalFeatures; +}; + +export const createConfig = (context: PluginInitializerContext): ConfigType => { + const pluginConfig = context.config.get>(); + const experimentalFeatures = parseExperimentalConfigValue(pluginConfig.enableExperimental); + + return { + ...pluginConfig, + experimentalFeatures, + }; +}; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/es_index/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/es_index/index.test.ts index d2961d4725d39..900f7cd334241 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/es_index/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/es_index/index.test.ts @@ -609,6 +609,21 @@ describe('execute()', () => { took: 0, errors: true, items: [ + { + create: { + _index: 'indexme', + _id: '7buTjHQB0SuNSiS9Hayt', + status: 400, + error: { + type: 'document_parsing_exception', + reason: `[1:10] failed to parse field [bytes] of type [long] in document with id '39XQLIoB8kAjguvyIMeJ'. Preview of field's value: 'foo'`, + caused_by: { + type: 'illegal_argument_exception', + reason: `For input string: \"foo\"`, + }, + }, + }, + }, { index: { _index: 'indexme', @@ -633,7 +648,175 @@ describe('execute()', () => { config, secrets, params, - services, + services: { ...services, scopedClusterClient }, + configurationUtilities, + logger: mockedLogger, + }) + ).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "message": "error indexing documents", + "serviceMessage": "[1:10] failed to parse field [bytes] of type [long] in document with id '39XQLIoB8kAjguvyIMeJ'. Preview of field's value: 'foo';failed to parse (For input string: \\"foo\\";field name cannot be an empty string)", + "status": "error", + } + `); + }); + + test('resolves with an error when an error occurs in the indexing operation - malformed response', async () => { + const secrets = {}; + // minimal params + const config = { index: 'index-value', refresh: false, executionTimeField: null }; + const params = { + documents: [{ '': 'bob' }], + indexOverride: null, + }; + + const actionId = 'some-id'; + const scopedClusterClient = elasticsearchClientMock + .createClusterClient() + .asScoped().asCurrentUser; + // @ts-expect-error + scopedClusterClient.bulk.mockResponse({ + took: 0, + errors: true, + }); + + expect( + await connectorType.executor({ + actionId, + config, + secrets, + params, + services: { ...services, scopedClusterClient }, + configurationUtilities, + logger: mockedLogger, + }) + ).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "message": "error indexing documents", + "serviceMessage": "Indexing error but no reason returned.", + "status": "error", + } + `); + }); + + test('resolves with an error when an error occurs in the indexing operation - malformed error response', async () => { + const secrets = {}; + // minimal params + const config = { index: 'index-value', refresh: false, executionTimeField: null }; + const params = { + documents: [{ '': 'bob' }], + indexOverride: null, + }; + + const actionId = 'some-id'; + const scopedClusterClient = elasticsearchClientMock + .createClusterClient() + .asScoped().asCurrentUser; + scopedClusterClient.bulk.mockResponse({ + took: 0, + errors: true, + items: [ + { + create: { + _index: 'indexme', + _id: '7buTjHQB0SuNSiS9Hayt', + status: 400, + error: { + type: 'document_parsing_exception', + reason: `[1:10] failed to parse field [bytes] of type [long] in document with id '39XQLIoB8kAjguvyIMeJ'. Preview of field's value: 'foo'`, + caused_by: { + type: 'illegal_argument_exception', + reason: `For input string: \"foo\"`, + }, + }, + }, + }, + { + index: { + _index: 'indexme', + _id: '7buTjHQB0SuNSiS9Hayt', + status: 400, + }, + }, + ], + }); + + expect( + await connectorType.executor({ + actionId, + config, + secrets, + params, + services: { ...services, scopedClusterClient }, + configurationUtilities, + logger: mockedLogger, + }) + ).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "message": "error indexing documents", + "serviceMessage": "[1:10] failed to parse field [bytes] of type [long] in document with id '39XQLIoB8kAjguvyIMeJ'. Preview of field's value: 'foo' (For input string: \\"foo\\")", + "status": "error", + } + `); + }); + + test('resolves with an error when an error occurs in the indexing operation - error with no reason', async () => { + const secrets = {}; + // minimal params + const config = { index: 'index-value', refresh: false, executionTimeField: null }; + const params = { + documents: [{ '': 'bob' }], + indexOverride: null, + }; + + const actionId = 'some-id'; + const scopedClusterClient = elasticsearchClientMock + .createClusterClient() + .asScoped().asCurrentUser; + scopedClusterClient.bulk.mockResponse({ + took: 0, + errors: true, + items: [ + { + create: { + _index: 'indexme', + _id: '7buTjHQB0SuNSiS9Hayt', + status: 400, + error: { + type: 'document_parsing_exception', + caused_by: { + type: 'illegal_argument_exception', + }, + }, + }, + }, + { + index: { + _index: 'indexme', + _id: '7buTjHQB0SuNSiS9Hayt', + status: 400, + error: { + type: 'mapper_parsing_exception', + reason: 'failed to parse', + caused_by: { + type: 'illegal_argument_exception', + }, + }, + }, + }, + ], + }); + + expect( + await connectorType.executor({ + actionId, + config, + secrets, + params, + services: { ...services, scopedClusterClient }, configurationUtilities, logger: mockedLogger, }) @@ -641,7 +824,7 @@ describe('execute()', () => { Object { "actionId": "some-id", "message": "error indexing documents", - "serviceMessage": "Cannot read properties of undefined (reading 'items')", + "serviceMessage": "failed to parse", "status": "error", } `); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/es_index/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/es_index/index.ts index b4ee9ff01e474..5057fdb5d8312 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/es_index/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/es_index/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { find } from 'lodash'; +import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { Logger } from '@kbn/core/server'; @@ -25,6 +25,10 @@ import { ALERT_HISTORY_PREFIX, buildAlertHistoryDocument, } from '@kbn/actions-plugin/common'; +import { + BulkOperationType, + BulkResponseItem, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; export type ESIndexConnectorType = ConnectorType< ConnectorTypeConfigType, @@ -124,15 +128,29 @@ async function executor( try { const result = await services.scopedClusterClient.bulk(bulkParams); - const err = find(result.items, 'index.error.reason'); - if (err) { - return wrapErr( - `${err.index?.error?.reason}${ - err.index?.error?.caused_by ? ` (${err.index?.error?.caused_by?.reason})` : '' - }`, - actionId, - logger - ); + if (result.errors) { + const errReason: string[] = []; + const errCausedBy: string[] = []; + // extract error reason and caused by + (result.items ?? []).forEach((item: Partial>) => { + for (const [_, responseItem] of Object.entries(item)) { + const reason = get(responseItem, 'error.reason'); + const causedBy = get(responseItem, 'error.caused_by.reason'); + if (reason) { + errReason.push(reason); + } + if (causedBy) { + errCausedBy.push(causedBy); + } + } + }); + + const errMessage = + errReason.length > 0 + ? `${errReason.join(';')}${errCausedBy.length > 0 ? ` (${errCausedBy.join(';')})` : ''}` + : `Indexing error but no reason returned.`; + + return wrapErr(errMessage, actionId, logger); } return { status: 'ok', data: result, actionId }; diff --git a/x-pack/plugins/stack_connectors/server/index.ts b/x-pack/plugins/stack_connectors/server/index.ts index 2cc792da9f9a3..c41b13ee2b15c 100644 --- a/x-pack/plugins/stack_connectors/server/index.ts +++ b/x-pack/plugins/stack_connectors/server/index.ts @@ -4,8 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { PluginInitializerContext } from '@kbn/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; import { StackConnectorsPlugin } from './plugin'; +import { configSchema, ConfigSchema } from './config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + enableExperimental: true, + }, + schema: configSchema, +}; export const plugin = (initContext: PluginInitializerContext) => new StackConnectorsPlugin(initContext); diff --git a/x-pack/plugins/stack_connectors/tsconfig.json b/x-pack/plugins/stack_connectors/tsconfig.json index f18dfaea77cca..0009b86c8348c 100644 --- a/x-pack/plugins/stack_connectors/tsconfig.json +++ b/x-pack/plugins/stack_connectors/tsconfig.json @@ -33,7 +33,10 @@ "@kbn/core-saved-objects-common", "@kbn/core-http-browser-mocks", "@kbn/core-saved-objects-api-server-mocks", + "@kbn/alerts-ui-shared", + "@kbn/alerting-plugin", "@kbn/securitysolution-ecs", + "@kbn/ui-theme", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts index d5f6fa883e4b5..d6a40ff10a642 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts @@ -33,7 +33,7 @@ export const OverviewStatusMetaDataCodec = t.interface({ monitorQueryId: t.string, configId: t.string, status: t.string, - location: t.string, + locationId: t.string, timestamp: t.string, ping: OverviewPingCodec, }); diff --git a/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts b/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts index fe019d4a07af5..51adff582c478 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts @@ -148,6 +148,7 @@ export const PingType = t.intersection([ timestamp: t.string, monitor: MonitorType, docId: t.string, + observer: ObserverCodec, }), t.partial({ '@timestamp': t.string, @@ -198,7 +199,6 @@ export const PingType = t.intersection([ uid: t.string, }), }), - observer: ObserverCodec, resolve: t.partial({ ip: t.string, rtt: t.partial({ @@ -263,37 +263,6 @@ export const PingStatusType = t.intersection([ export type PingStatus = t.TypeOf; -// Convenience function for tests etc that makes an empty ping -// object with the minimum of fields. -export const makePing = (f: { - docId?: string; - type?: string; - id?: string; - timestamp?: string; - ip?: string; - status?: string; - duration?: number; - location?: string; - name?: string; - url?: string; -}): Ping => { - return { - docId: f.docId || 'myDocId', - timestamp: f.timestamp || '2020-07-07T01:14:08Z', - monitor: { - id: f.id || 'myId', - type: f.type || 'myType', - ip: f.ip || '127.0.0.1', - status: f.status || 'up', - duration: { us: f.duration || 100000 }, - name: f.name, - check_group: 'myCheckGroup', - }, - ...(f.location ? { observer: { geo: { name: f.location } } } : {}), - ...(f.url ? { url: { full: f.url } } : {}), - }; -}; - export const PingsResponseType = t.type({ total: t.number, pings: t.array(PingType), diff --git a/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts b/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts index 65d466a5ab2f8..47b0eaf9d143c 100644 --- a/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts +++ b/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts @@ -20,3 +20,11 @@ export interface TestNowResponse { configId: string; monitor: SyntheticsMonitor; } + +export interface AgentPolicyInfo { + id: string; + name: string; + agents: number; + status: string; + description?: string; +} diff --git a/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts b/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts index cbed9b9f259c3..22576524545c1 100644 --- a/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts +++ b/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts @@ -7,13 +7,13 @@ import { DocOverrides } from './sample_docs'; -export const getGeoData = (locationName?: string) => ({ +export const getGeoData = (locationName?: string, locationId?: string) => ({ observer: { geo: { name: locationName ?? 'North America - US Central', location: '41.8780, 93.0977', }, - name: locationName ?? 'North America - US Central', + name: locationId ?? 'us_central', }, }); diff --git a/x-pack/plugins/synthetics/kibana.jsonc b/x-pack/plugins/synthetics/kibana.jsonc index 511666996f829..d03d0d384938f 100644 --- a/x-pack/plugins/synthetics/kibana.jsonc +++ b/x-pack/plugins/synthetics/kibana.jsonc @@ -41,7 +41,8 @@ "kibanaUtils", "observability", "spaces", - "indexLifecycleManagement" + "indexLifecycleManagement", + "unifiedDocViewer" ] } } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/view_document.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/view_document.tsx new file mode 100644 index 0000000000000..a37a5d8afd17e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/view_document.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonIcon, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import React, { useState } from 'react'; +import { useUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public'; +import { buildDataTableRecord } from '@kbn/discover-utils'; +import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { useFetcher } from '@kbn/observability-shared-plugin/public'; +import { DataTableRecord } from '@kbn/discover-utils/src/types'; +import { useDateFormat } from '../../../../../hooks/use_date_format'; +import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout'; +import { useSyntheticsDataView } from '../../../contexts/synthetics_data_view_context'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { Ping } from '../../../../../../common/runtime_types'; + +export const ViewDocument = ({ ping }: { ping: Ping }) => { + const { data } = useUnifiedDocViewerServices(); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + const dataView = useSyntheticsDataView(); + const formatter = useDateFormat(); + + const { data: hit } = useFetcher>(async () => { + if (!dataView?.id || !isFlyoutVisible) return; + const response = await data.search + .search({ + params: { + index: SYNTHETICS_INDEX_PATTERN, + body: { + query: { + ids: { + values: [ping.docId], + }, + }, + fields: ['*'], + _source: false, + }, + }, + }) + .toPromise(); + const docs = response?.rawResponse?.hits?.hits ?? []; + if (docs.length > 0) { + return buildDataTableRecord(docs[0], dataView); + } + }, [data, dataView, ping.docId, isFlyoutVisible]); + + return ( + <> + { + setIsFlyoutVisible(true); + }} + /> + {isFlyoutVisible && ( + setIsFlyoutVisible(false)} ownFocus={true}> + + +

+ {INDEXED_AT} {formatter(ping.timestamp)} +

+ + + + {dataView?.id && hit ? ( + + ) : ( + + )} + + + )} + + ); +}; + +const INDEXED_AT = i18n.translate('xpack.synthetics.monitorDetails.summary.indexedAt', { + defaultMessage: 'Indexed at', +}); + +export const INSPECT_DOCUMENT = i18n.translate( + 'xpack.synthetics.monitorDetails.action.inspectDocument', + { + defaultMessage: 'Inspect document', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx index d66a079d8a2ea..caa3a2d66fcfe 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx @@ -82,9 +82,7 @@ describe('GettingStartedPage', () => { loading: false, }, agentPolicies: { - data: { - total: 0, - }, + data: [], isAddingNewPrivateLocation: true, }, }, @@ -109,10 +107,7 @@ describe('GettingStartedPage', () => { loading: false, }, agentPolicies: { - data: { - total: 1, - items: [{}], - }, + data: [{}], isAddingNewPrivateLocation: true, }, }, @@ -141,10 +136,7 @@ describe('GettingStartedPage', () => { loading: false, }, agentPolicies: { - data: { - total: 1, - items: [{}], - }, + data: [{}], isAddingNewPrivateLocation: true, }, }, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx index 4e4f9a6c61b17..969e98a21c36c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx @@ -8,7 +8,7 @@ import { useTimeZone } from '@kbn/observability-shared-plugin/public'; import { useParams } from 'react-router-dom'; import { useMemo } from 'react'; import { useSelectedLocation } from './use_selected_location'; -import { PingState } from '../../../../../../common/runtime_types'; +import { Ping, PingState } from '../../../../../../common/runtime_types'; import { EXCLUDE_RUN_ONCE_FILTER, SUMMARY_FILTER, @@ -113,10 +113,11 @@ export function useMonitorErrors(monitorIdArg?: string) { return prev; }, defaultValues) ?? defaultValues; + const hits = data?.aggregations?.latest.hits.hits ?? []; + const hasActiveError: boolean = - data?.aggregations?.latest.hits.hits.length === 1 && - (data?.aggregations?.latest.hits.hits[0]._source as { monitor: { status: string } }).monitor - .status === 'down' && + hits.length === 1 && + (hits[0]?._source as Ping).monitor?.status === 'down' && !!errorStates?.length; return { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx index 97b5f2b521716..0bbbd3a5f247b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import { Criteria } from '@elastic/eui/src/components/basic_table/basic_table'; import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; +import { INSPECT_DOCUMENT, ViewDocument } from '../../common/components/view_document'; import { ExpandRowColumn, toggleDetails, @@ -206,6 +207,20 @@ export const TestRunsTable = ({ show: false, }, }, + { + align: 'right' as const, + actions: [ + { + 'data-test-subj': 'syntheticsViewPingDocument', + isPrimary: true, + name: INSPECT_DOCUMENT, + description: INSPECT_DOCUMENT, + icon: 'inspect' as const, + type: 'button' as const, + render: (ping: Ping) => , + }, + ], + }, ...(!isBrowserMonitor ? [ { @@ -229,10 +244,17 @@ export const TestRunsTable = ({ 'data-test-subj': `row-${item.monitor.check_group}`, onClick: (evt: MouseEvent) => { const targetElem = evt.target as HTMLElement; + const isTableRow = + targetElem.parentElement?.classList.contains('euiTableCellContent') || + targetElem.parentElement?.classList.contains('euiTableCellContent__text') || + targetElem?.classList.contains('euiTableCellContent') || + targetElem?.classList.contains('euiBadge__text'); // we dont want to capture image click event if ( + isTableRow && targetElem.tagName !== 'IMG' && targetElem.tagName !== 'path' && + targetElem.tagName !== 'BUTTON' && !targetElem.parentElement?.classList.contains('euiLink') ) { if (item.monitor.type !== MONITOR_TYPES.BROWSER) { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 2c0885c7d6258..ce6de294d7e60 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -65,10 +65,10 @@ export const MetricItem = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const isErrorPopoverOpen = useSelector(selectErrorPopoverState); const locationName = - useLocationName({ locationId: monitor.location?.id })?.label || monitor.location?.id; + useLocationName({ locationId: monitor.location.id })?.label || monitor.location?.id; const { status, timestamp, ping, configIdByLocation } = useStatusByLocationOverview( monitor.configId, - locationName + monitor.location.id ); const theme = useTheme(); @@ -116,7 +116,7 @@ export const MetricItem = ({ configId: monitor.configId, id: monitor.id, location: locationName, - locationId: monitor.location?.id, + locationId: monitor.location.id, }); } }} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx index 8cf6ac4ac0971..c736d47846ada 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx @@ -40,6 +40,7 @@ describe('Monitor Detail Flyout', () => { full: 'https://www.elastic.co', }, tags: ['tag1', 'tag2'], + observer: {}, }, }); jest.spyOn(statusByLocation, 'useStatusByLocation').mockReturnValue({ diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx index cc2b835fbfdd8..7a8fdd67c6efd 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx @@ -29,7 +29,7 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo const { control, register, watch } = useFormContext(); const { errors } = useFormState(); const selectedPolicyId = watch('agentPolicyId'); - const selectedPolicy = data?.items.find((item) => item.id === selectedPolicyId); + const selectedPolicy = data?.find((item) => item.id === selectedPolicyId); const tagsList = privateLocations.reduce((acc, item) => { const tags = item.tags || []; @@ -38,7 +38,7 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo return ( <> - {data?.items.length === 0 && } + {data?.length === 0 && } { const { data: agentPolicies } = useSelector(selectAgentPolicies); - if (agentPolicies?.total === 0 && showNeedAgentPolicy) { + if (agentPolicies?.length === 0 && showNeedAgentPolicy) { return ; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx index 818b398315610..41086bacde0be 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx @@ -46,12 +46,7 @@ describe('', () => { const { getByText, getByRole, findByText } = render(, { state: { agentPolicies: { - data: { - items: [], - total: 0, - page: 1, - perPage: 20, - }, + data: [], loading: false, error: null, isManageFlyoutOpen: false, @@ -85,12 +80,7 @@ describe('', () => { const { getByText, getByRole, findByText } = render(, { state: { agentPolicies: { - data: { - items: [{}], - total: 1, - page: 1, - perPage: 20, - }, + data: [{}], loading: false, error: null, isManageFlyoutOpen: false, @@ -140,12 +130,7 @@ describe('', () => { const { getByText, getByRole, findByText } = render(, { state: { agentPolicies: { - data: { - items: [{}], - total: 1, - page: 1, - perPage: 20, - }, + data: [{}], loading: false, error: null, isManageFlyoutOpen: false, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx index e91a8b529131c..483e80fae65b7 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx @@ -34,7 +34,7 @@ export const PolicyHostsField = ({ }) => { const { data } = useSelector(selectAgentPolicies); - const policyHostsOptions = data?.items.map((item) => { + const policyHostsOptions = data?.map((item) => { const hasLocation = privateLocations.find((location) => location.agentPolicyId === item.id); return { disabled: Boolean(hasLocation), diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_name.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_name.tsx index f336d83a98ee7..ea4ac5b75688b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_name.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_name.tsx @@ -20,7 +20,7 @@ export const PolicyName = ({ agentPolicyId }: { agentPolicyId: string }) => { const { data: policies, loading } = useSelector(selectAgentPolicies); - const policy = policies?.items.find((policyT) => policyT.id === agentPolicyId); + const policy = policies?.find((policyT) => policyT.id === agentPolicyId); if (loading) { return ; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/simple/ping_list/columns/expand_row.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/simple/ping_list/columns/expand_row.tsx index b756cfb082cb5..9ef5ceacc6183 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/simple/ping_list/columns/expand_row.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/simple/ping_list/columns/expand_row.tsx @@ -16,6 +16,11 @@ export const toggleDetails = ( expandedRows: Record, setExpandedRows: (update: Record) => any ) => { + // prevent expanding on row click if not expandable + if (!rowShouldExpand(ping)) { + return; + } + // If already expanded, collapse if (expandedRows[ping.docId]) { delete expandedRows[ping.docId]; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx new file mode 100644 index 0000000000000..4e38a3de25388 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext } from 'react'; +import { useFetcher } from '@kbn/observability-shared-plugin/public'; +import { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../common/constants'; + +export const SyntheticsDataViewContext = createContext({} as DataView); + +export const SyntheticsDataViewContextProvider: React.FC<{ + dataViews: DataViewsPublicPluginStart; +}> = ({ children, dataViews }) => { + const { data } = useFetcher>(async () => { + return dataViews.create({ title: SYNTHETICS_INDEX_PATTERN }); + }, []); + + return ; +}; + +export const useSyntheticsDataView = () => useContext(SyntheticsDataViewContext); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.test.tsx index 16fdc7cd5a497..30a09853ba27f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.test.tsx @@ -120,34 +120,34 @@ describe('useMonitorsSortedByStatus', () => { [`test-monitor-1-${location2.label}`]: { configId: 'test-monitor-1', monitorQueryId: 'test-monitor-1', - location: location2.label, + locationId: location2.label, }, [`test-monitor-2-${location2.label}`]: { configId: 'test-monitor-2', monitorQueryId: 'test-monitor-2', - location: location2.label, + locationId: location2.label, }, [`test-monitor-3-${location2.label}`]: { configId: 'test-monitor-3', monitorQueryId: 'test-monitor-3', - location: location2.label, + locationId: location2.label, }, }, downConfigs: { [`test-monitor-1-${location1.label}`]: { configId: 'test-monitor-1', monitorQueryId: 'test-monitor-1', - location: location1.label, + locationId: location1.label, }, [`test-monitor-2-${location1.label}`]: { configId: 'test-monitor-2', monitorQueryId: 'test-monitor-2', - location: location1.label, + locationId: location1.label, }, [`test-monitor-3${location1.label}`]: { configId: 'test-monitor-3', monitorQueryId: 'test-monitor-3', - location: location1.label, + locationId: location1.label, }, }, pendingConfigs: { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx index 2148b7e5eeb30..fd4c353a1f91d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx @@ -37,11 +37,11 @@ export function useMonitorsSortedByStatus() { const { downConfigs, pendingConfigs } = status; const downMonitorMap: Record = {}; - Object.values(downConfigs).forEach(({ location, configId }) => { + Object.values(downConfigs).forEach(({ locationId, configId }) => { if (downMonitorMap[configId]) { - downMonitorMap[configId].push(location); + downMonitorMap[configId].push(locationId); } else { - downMonitorMap[configId] = [location]; + downMonitorMap[configId] = [locationId]; } }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts index 019e50d8cbe4d..46e39f6eb0e66 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts @@ -9,22 +9,22 @@ import { useSelector } from 'react-redux'; import { OverviewStatusState } from '../../../../common/runtime_types'; import { selectOverviewStatus } from '../state/overview_status'; -export function useStatusByLocationOverview(configId: string, locationName?: string) { +export function useStatusByLocationOverview(configId: string, locationId: string) { const { status } = useSelector(selectOverviewStatus); - return getConfigStatusByLocation(status, configId, locationName); + return getConfigStatusByLocation(status, configId, locationId); } export const getConfigStatusByLocation = ( status: OverviewStatusState | null, configId: string, - locationName?: string + locationId: string ) => { - if (!locationName || !status) { + if (!status) { return { status: 'unknown' }; } const allConfigs = status.allConfigs; - const configIdByLocation = `${configId}-${locationName}`; + const configIdByLocation = `${configId}-${locationId}`; const config = allConfigs[configIdByLocation]; return { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/actions.ts index 60586a99c380e..05fc1e950c112 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/actions.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/actions.ts @@ -6,10 +6,10 @@ */ import { createAction } from '@reduxjs/toolkit'; +import { AgentPolicyInfo } from '../../../../../common/types'; import { createAsyncAction } from '../utils/actions'; -import { AgentPoliciesList } from '.'; -export const getAgentPoliciesAction = createAsyncAction( +export const getAgentPoliciesAction = createAsyncAction( '[AGENT POLICIES] GET' ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts index c3baa6f21ddc5..805d5672ae9bd 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { AgentPolicyInfo } from '../../../../../common/types'; import { SYNTHETICS_API_URLS } from '../../../../../common/constants'; import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; import { apiService } from '../../../../utils/api_service/api_service'; -import { AgentPoliciesList } from '.'; -export const fetchAgentPolicies = async (): Promise => { +export const fetchAgentPolicies = async (): Promise => { return await apiService.get(SYNTHETICS_API_URLS.AGENT_POLICIES); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts index 5b023a8bd0b55..d38c6b6702660 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts @@ -6,19 +6,12 @@ */ import { createReducer } from '@reduxjs/toolkit'; -import { AgentPolicy } from '@kbn/fleet-plugin/common'; +import { AgentPolicyInfo } from '../../../../../common/types'; import { IHttpSerializedFetchError } from '..'; import { getAgentPoliciesAction, setAddingNewPrivateLocation } from './actions'; -export interface AgentPoliciesList { - items: AgentPolicy[]; - total: number; - page: number; - perPage: number; -} - export interface AgentPoliciesState { - data: AgentPoliciesList | null; + data: AgentPolicyInfo[] | null; loading: boolean; error: IHttpSerializedFetchError | null; isManageFlyoutOpen?: boolean; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx index f9b37b64df403..bac16812f1bf2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -18,6 +18,7 @@ import { import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { InspectorContextProvider } from '@kbn/observability-shared-plugin/public'; import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public'; +import { SyntheticsDataViewContextProvider } from './contexts/synthetics_data_view_context'; import { SyntheticsAppProps } from './contexts'; import { @@ -99,32 +100,34 @@ const Application = (props: SyntheticsAppProps) => { fleet: startPlugins.fleet, }} > - - - - - - - -
- - - - - - - -
-
-
-
-
-
-
-
+ + + + + + + + +
+ + + + + + + +
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/synthetics/server/alert_rules/common.test.ts b/x-pack/plugins/synthetics/server/alert_rules/common.test.ts index f58eb9e4ce3c3..2edcd37a95927 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/common.test.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/common.test.ts @@ -202,7 +202,7 @@ describe('setRecoveredAlertsContext', () => { configId, monitorQueryId: 'stale-config', status: 'up', - location: '', + locationId: '', ping: { '@timestamp': new Date().toISOString(), state: { @@ -235,7 +235,7 @@ describe('setRecoveredAlertsContext', () => { configId, monitorQueryId: 'stale-config', status: 'down', - location: 'location', + locationId: 'location', ping: { '@timestamp': new Date().toISOString(), state: { @@ -294,7 +294,7 @@ describe('setRecoveredAlertsContext', () => { configId, monitorQueryId: 'stale-config', status: 'down', - location: 'location', + locationId: 'location', ping: { '@timestamp': new Date().toISOString(), state: { @@ -355,7 +355,7 @@ describe('setRecoveredAlertsContext', () => { configId, monitorQueryId: 'stale-config', status: 'down', - location: 'location', + locationId: 'location', ping: { state: { id: '123456', diff --git a/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts b/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts index 61173f7b0c227..dde24f409b0e3 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts @@ -93,7 +93,7 @@ export const registerSyntheticsStatusCheckRule = ( ); Object.entries(downConfigs).forEach(([idWithLocation, { ping, configId }]) => { - const locationId = statusRule.getLocationId(ping.observer?.geo?.name!) ?? ''; + const locationId = ping.observer.name ?? ''; const alertId = idWithLocation; const monitorSummary = getMonitorSummary( ping, diff --git a/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts b/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts index bb9183b661b04..ff8ecebccf968 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts @@ -97,7 +97,7 @@ describe('StatusRuleExecutor', () => { const staleDownConfigs = await statusRule.markDeletedConfigs({ id1: { - location: 'us-east-1', + locationId: 'us-east-1', configId: 'id1', status: 'down', timestamp: '2021-06-01T00:00:00.000Z', @@ -105,7 +105,7 @@ describe('StatusRuleExecutor', () => { ping: {} as any, }, '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': { - location: 'US Central DEV', + locationId: 'us_central_dev', configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04', status: 'down', timestamp: '2021-06-01T00:00:00.000Z', @@ -113,7 +113,7 @@ describe('StatusRuleExecutor', () => { ping: {} as any, }, '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_qa': { - location: 'US Central QA', + locationId: 'us_central_qa', configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04', status: 'down', timestamp: '2021-06-01T00:00:00.000Z', @@ -126,7 +126,7 @@ describe('StatusRuleExecutor', () => { id1: { configId: 'id1', isDeleted: true, - location: 'us-east-1', + locationId: 'us-east-1', monitorQueryId: 'test', ping: {}, status: 'down', @@ -135,7 +135,7 @@ describe('StatusRuleExecutor', () => { '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': { configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04', isLocationRemoved: true, - location: 'US Central DEV', + locationId: 'us_central_dev', monitorQueryId: 'test', ping: {}, status: 'down', @@ -175,7 +175,7 @@ describe('StatusRuleExecutor', () => { const staleDownConfigs = await statusRule.markDeletedConfigs({ id1: { - location: 'us-east-1', + locationId: 'us-east-1', configId: 'id1', status: 'down', timestamp: '2021-06-01T00:00:00.000Z', @@ -183,7 +183,7 @@ describe('StatusRuleExecutor', () => { ping: {} as any, }, '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': { - location: 'US Central DEV', + locationId: 'us_central_dev', configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04', status: 'down', timestamp: '2021-06-01T00:00:00.000Z', @@ -191,7 +191,7 @@ describe('StatusRuleExecutor', () => { ping: {} as any, }, '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_qa': { - location: 'US Central QA', + locationId: 'us_central_qa', configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04', status: 'down', timestamp: '2021-06-01T00:00:00.000Z', @@ -204,7 +204,7 @@ describe('StatusRuleExecutor', () => { id1: { configId: 'id1', isDeleted: true, - location: 'us-east-1', + locationId: 'us-east-1', monitorQueryId: 'test', ping: {}, status: 'down', @@ -213,7 +213,7 @@ describe('StatusRuleExecutor', () => { '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': { configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04', isLocationRemoved: true, - location: 'US Central DEV', + locationId: 'us_central_dev', monitorQueryId: 'test', ping: {}, status: 'down', diff --git a/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts b/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts index 2bb27ddb4a23b..0861b29c8134d 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts @@ -13,7 +13,6 @@ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { SyntheticsServerSetup } from '../../types'; import { UptimeEsClient } from '../../lib'; import { SYNTHETICS_INDEX_PATTERN } from '../../../common/constants'; -import { getAllLocations } from '../../synthetics_service/get_all_locations'; import { getAllMonitors, processMonitors, @@ -49,8 +48,6 @@ export class StatusRuleExecutor { syntheticsMonitorClient: SyntheticsMonitorClient; monitors: Array> = []; - public locationIdNameMap: Record = {}; - constructor( previousStartedAt: Date | null, p: StatusRuleParams, @@ -69,28 +66,7 @@ export class StatusRuleExecutor { this.syntheticsMonitorClient = syntheticsMonitorClient; } - async getAllLocationNames() { - const { publicLocations, privateLocations } = await getAllLocations({ - server: this.server, - syntheticsMonitorClient: this.syntheticsMonitorClient, - savedObjectsClient: this.soClient, - }); - - publicLocations.forEach((loc) => { - this.locationIdNameMap[loc.label] = loc.id; - }); - - privateLocations.forEach((loc) => { - this.locationIdNameMap[loc.label] = loc.id; - }); - } - - getLocationId(name: string) { - return this.locationIdNameMap[name]; - } - async getMonitors() { - await this.getAllLocationNames(); this.monitors = await getAllMonitors({ soClient: this.soClient, filter: `${monitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`, @@ -99,20 +75,15 @@ export class StatusRuleExecutor { const { allIds, enabledMonitorQueryIds, - listOfLocations, + monitorLocationIds, monitorLocationMap, projectMonitorsCount, monitorQueryIdToConfigIdMap, - } = await processMonitors( - this.monitors, - this.server, - this.soClient, - this.syntheticsMonitorClient - ); + } = processMonitors(this.monitors, this.server, this.soClient, this.syntheticsMonitorClient); return { enabledMonitorQueryIds, - listOfLocations, + monitorLocationIds, allIds, monitorLocationMap, projectMonitorsCount, @@ -124,7 +95,7 @@ export class StatusRuleExecutor { prevDownConfigs: OverviewStatus['downConfigs'] = {} ): Promise { const { - listOfLocations, + monitorLocationIds, enabledMonitorQueryIds, allIds, monitorLocationMap, @@ -138,7 +109,7 @@ export class StatusRuleExecutor { if (enabledMonitorQueryIds.length > 0) { const currentStatus = await queryMonitorStatus( this.esClient, - listOfLocations, + monitorLocationIds, { to: 'now', from, @@ -201,9 +172,7 @@ export class StatusRuleExecutor { delete downConfigs[locPlusId]; } else { const { locations } = monitor.attributes; - const isLocationRemoved = !locations.some( - (l) => l.id === this.getLocationId(downConfig.location) - ); + const isLocationRemoved = !locations.some((l) => l.id === downConfig.locationId); if (isLocationRemoved) { staleDownConfigs[locPlusId] = { ...downConfig, isLocationRemoved: true }; delete downConfigs[locPlusId]; diff --git a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts index dbd97c2e0b51c..0ca15a53efc0a 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts @@ -70,20 +70,15 @@ export class TLSRuleExecutor { const { allIds, enabledMonitorQueryIds, - listOfLocations, + monitorLocationIds, monitorLocationMap, projectMonitorsCount, monitorQueryIdToConfigIdMap, - } = await processMonitors( - this.monitors, - this.server, - this.soClient, - this.syntheticsMonitorClient - ); + } = processMonitors(this.monitors, this.server, this.soClient, this.syntheticsMonitorClient); return { enabledMonitorQueryIds, - listOfLocations, + monitorLocationIds, allIds, monitorLocationMap, projectMonitorsCount, diff --git a/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts b/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts index b714177e6cee9..e18e4f23b34c8 100644 --- a/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts +++ b/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts @@ -34,7 +34,7 @@ const fields = [ export async function queryMonitorStatus( esClient: UptimeEsClient, - listOfLocations: string[], + monitorLocationIds: string[], range: { from: string; to: string }, monitorQueryIds: string[], monitorLocationsMap: Record, @@ -50,7 +50,7 @@ export async function queryMonitorStatus( | 'allIds' > > { - const idSize = Math.trunc(DEFAULT_MAX_ES_BUCKET_SIZE / listOfLocations.length || 1); + const idSize = Math.trunc(DEFAULT_MAX_ES_BUCKET_SIZE / monitorLocationIds.length || 1); const pageCount = Math.ceil(monitorQueryIds.length / idSize); let up = 0; let down = 0; @@ -95,8 +95,8 @@ export async function queryMonitorStatus( aggs: { location: { terms: { - field: 'observer.geo.name', - size: listOfLocations.length || 100, + field: 'observer.name', + size: monitorLocationIds.length || 100, }, aggs: { status: { @@ -122,10 +122,10 @@ export async function queryMonitorStatus( }, }); - if (listOfLocations.length > 0) { + if (monitorLocationIds.length > 0) { params.body.query.bool.filter.push({ terms: { - 'observer.geo.name': listOfLocations, + 'observer.name': monitorLocationIds, }, }); } @@ -144,7 +144,7 @@ export async function queryMonitorStatus( // discard any locations that are not in the monitorLocationsMap for the given monitor as well as those which are // in monitorLocationsMap but not in listOfLocations const monLocations = monitorLocationsMap?.[queryId]; - const monQueriedLocations = intersection(monLocations, listOfLocations); + const monQueriedLocations = intersection(monLocations, monitorLocationIds); monQueriedLocations?.forEach((monLocation) => { const locationSummary = locationSummaries.find( (summary) => summary.location === monLocation @@ -154,14 +154,14 @@ export async function queryMonitorStatus( const { ping } = locationSummary; const downCount = ping.summary?.down ?? 0; const upCount = ping.summary?.up ?? 0; - const configId = ping.config_id!; + const configId = ping.config_id; const monitorQueryId = ping.monitor.id; const meta = { ping, configId, monitorQueryId, - location: monLocation, + locationId: monLocation, timestamp: ping['@timestamp'], }; diff --git a/x-pack/plugins/synthetics/server/routes/certs/get_certificates.ts b/x-pack/plugins/synthetics/server/routes/certs/get_certificates.ts index 982d9f60f90ff..405863865757d 100644 --- a/x-pack/plugins/synthetics/server/routes/certs/get_certificates.ts +++ b/x-pack/plugins/synthetics/server/routes/certs/get_certificates.ts @@ -57,7 +57,7 @@ export const getSyntheticsCertsRoute: SyntheticsRestApiRouteFactory< }; } - const { enabledMonitorQueryIds } = await processMonitors( + const { enabledMonitorQueryIds } = processMonitors( monitors, server, savedObjectsClient, diff --git a/x-pack/plugins/synthetics/server/routes/common.ts b/x-pack/plugins/synthetics/server/routes/common.ts index 84fa407296f09..b29e3578aba87 100644 --- a/x-pack/plugins/synthetics/server/routes/common.ts +++ b/x-pack/plugins/synthetics/server/routes/common.ts @@ -81,7 +81,7 @@ export const getMonitors = async ( monitorQueryIds, } = context.request.query; - const filterStr = await getMonitorFilters({ + const { filtersStr } = await getMonitorFilters({ filter, monitorTypes, tags, @@ -100,7 +100,7 @@ export const getMonitors = async ( sortOrder, searchFields: SEARCH_FIELDS, search: query ? `${query}*` : undefined, - filter: filterStr, + filter: filtersStr, searchAfter, fields, }; @@ -129,7 +129,7 @@ export const getMonitorFilters = async ({ }) => { const locationFilter = await parseLocationFilter(context, locations); - return [ + const filtersStr = [ filter, getKqlFilter({ field: 'tags', values: tags }), getKqlFilter({ field: 'project_id', values: projects }), @@ -140,6 +140,7 @@ export const getMonitorFilters = async ({ ] .filter((f) => !!f) .join(' AND '); + return { filtersStr, locationFilter }; }; export const getKqlFilter = ({ @@ -172,7 +173,7 @@ export const getKqlFilter = ({ const parseLocationFilter = async (context: RouteContext, locations?: string | string[]) => { if (!locations || locations?.length === 0) { - return ''; + return; } const { allLocations } = await getAllLocations(context); @@ -183,7 +184,7 @@ const parseLocationFilter = async (context: RouteContext, locations?: string | s .filter((val) => !!val); } - return findLocationItem(locations, allLocations)?.id ?? ''; + return [findLocationItem(locations, allLocations)?.id ?? '']; }; export const findLocationItem = ( diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts index 770fb102c5369..549c812eec27f 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts @@ -91,7 +91,7 @@ export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory = locations: queriedLocations, } = request.query as MonitorsQuery; - const filtersStr = await getMonitorFilters({ + const { filtersStr } = await getMonitorFilters({ ...request.query, context: routeContext, }); diff --git a/x-pack/plugins/synthetics/server/routes/overview_status/overview_status.test.ts b/x-pack/plugins/synthetics/server/routes/overview_status/overview_status.test.ts index 983ae3cb3d854..02e5dfa61e2bc 100644 --- a/x-pack/plugins/synthetics/server/routes/overview_status/overview_status.test.ts +++ b/x-pack/plugins/synthetics/server/routes/overview_status/overview_status.test.ts @@ -15,39 +15,80 @@ import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_ty import { RouteContext } from '../types'; import { getUptimeESMockClient } from '../../legacy_uptime/lib/requests/test_helpers'; +import * as commonLibs from '../common'; +import { SyntheticsServerSetup } from '../../types'; +import { mockEncryptedSO } from '../../synthetics_service/utils/mocks'; +import { savedObjectsServiceMock } from '@kbn/core-saved-objects-server-mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import * as allLocationsFn from '../../synthetics_service/get_all_locations'; +const allLocations: any = [ + { + id: 'us_central_qa', + label: 'US Central QA', + }, + { + id: 'us_central', + label: 'North America - US Central', + }, +]; +jest.spyOn(allLocationsFn, 'getAllLocations').mockResolvedValue({ + publicLocations: allLocations, + privateLocations: [], + allLocations, +}); + jest.mock('../../saved_objects/synthetics_monitor/get_all_monitors', () => ({ ...jest.requireActual('../../saved_objects/synthetics_monitor/get_all_monitors'), getAllMonitors: jest.fn(), })); -jest.mock('../common', () => ({ - getMonitors: jest.fn().mockReturnValue({ - per_page: 10, - saved_objects: [ - { - id: 'mon-1', - attributes: { - enabled: false, - locations: ['us-east1', 'us-west1', 'japan'], - }, +jest.spyOn(commonLibs, 'getMonitors').mockResolvedValue({ + per_page: 10, + saved_objects: [ + { + id: 'mon-1', + attributes: { + enabled: false, + locations: [{ id: 'us-east1' }, { id: 'us-west1' }, { id: 'japan' }], }, - { - id: 'mon-2', - attributes: { - enabled: true, - locations: ['us-east1', 'us-west1', 'japan'], - schedule: { - number: '10', - unit: 'm', - }, + }, + { + id: 'mon-2', + attributes: { + enabled: true, + locations: [{ id: 'us-east1' }, { id: 'us-west1' }, { id: 'japan' }], + schedule: { + number: '10', + unit: 'm', }, }, - ], - }), - getMonitorFilters: () => '', -})); + }, + ], +} as any); describe('current status route', () => { + const logger = loggerMock.create(); + + const serverMock: SyntheticsServerSetup = { + logger, + config: { + service: { + username: 'dev', + password: '12345', + manifestUrl: 'http://localhost:8080/api/manifest', + }, + }, + spaces: { + spacesService: { + getSpaceId: jest.fn().mockReturnValue('test-space'), + }, + }, + encryptedSavedObjects: mockEncryptedSO(), + coreStart: { + savedObjects: savedObjectsServiceMock.createStartContract(), + }, + } as unknown as SyntheticsServerSetup; + describe('periodToMs', () => { it('returns 0 for unsupported unit type', () => { // @ts-expect-error Providing invalid value to test handler in function @@ -190,7 +231,7 @@ describe('current status route', () => { 'id1-Asia/Pacific - Japan': { configId: 'id1', monitorQueryId: 'id1', - location: 'Asia/Pacific - Japan', + locationId: 'Asia/Pacific - Japan', status: 'up', ping: expect.any(Object), timestamp: expect.any(String), @@ -198,7 +239,7 @@ describe('current status route', () => { 'id2-Asia/Pacific - Japan': { configId: 'id2', monitorQueryId: 'id2', - location: 'Asia/Pacific - Japan', + locationId: 'Asia/Pacific - Japan', status: 'up', ping: expect.any(Object), timestamp: expect.any(String), @@ -208,7 +249,7 @@ describe('current status route', () => { 'id2-Europe - Germany': { configId: 'id2', monitorQueryId: 'id2', - location: 'Europe - Germany', + locationId: 'Europe - Germany', status: 'down', ping: expect.any(Object), timestamp: expect.any(String), @@ -355,7 +396,7 @@ describe('current status route', () => { 'id1-Asia/Pacific - Japan': { configId: 'id1', monitorQueryId: 'id1', - location: 'Asia/Pacific - Japan', + locationId: 'Asia/Pacific - Japan', status: 'up', ping: expect.any(Object), timestamp: expect.any(String), @@ -363,7 +404,7 @@ describe('current status route', () => { 'id2-Asia/Pacific - Japan': { configId: 'id2', monitorQueryId: 'id2', - location: 'Asia/Pacific - Japan', + locationId: 'Asia/Pacific - Japan', status: 'up', ping: expect.any(Object), timestamp: expect.any(String), @@ -373,7 +414,7 @@ describe('current status route', () => { 'id2-Europe - Germany': { configId: 'id2', monitorQueryId: 'id2', - location: 'Europe - Germany', + locationId: 'Europe - Germany', status: 'down', ping: expect.any(Object), timestamp: expect.any(String), @@ -525,7 +566,7 @@ describe('current status route', () => { 'id1-Asia/Pacific - Japan': { configId: 'id1', monitorQueryId: 'id1', - location: 'Asia/Pacific - Japan', + locationId: 'Asia/Pacific - Japan', status: 'up', ping: expect.any(Object), timestamp: expect.any(String), @@ -533,7 +574,7 @@ describe('current status route', () => { 'id2-Asia/Pacific - Japan': { configId: 'id2', monitorQueryId: 'id2', - location: 'Asia/Pacific - Japan', + locationId: 'Asia/Pacific - Japan', status: 'up', ping: expect.any(Object), timestamp: expect.any(String), @@ -543,7 +584,7 @@ describe('current status route', () => { 'id2-Europe - Germany': { configId: 'id2', monitorQueryId: 'id2', - location: 'Europe - Germany', + locationId: 'Europe - Germany', status: 'down', ping: expect.any(Object), timestamp: expect.any(String), @@ -729,17 +770,17 @@ describe('current status route', () => { }, ]) ); - expect( - await getStatus( - { - uptimeEsClient, - savedObjectsClient: savedObjectsClientMock.create(), - } as unknown as RouteContext, - { - locations, - } - ) - ).toEqual( + const result = await getStatus( + { + uptimeEsClient, + savedObjectsClient: savedObjectsClientMock.create(), + server: serverMock, + } as unknown as RouteContext, + { + locations, + } + ); + expect(result).toEqual( expect.objectContaining({ disabledCount, }) diff --git a/x-pack/plugins/synthetics/server/routes/overview_status/overview_status.ts b/x-pack/plugins/synthetics/server/routes/overview_status/overview_status.ts index 4de7341d10626..7d28d9b4f0576 100644 --- a/x-pack/plugins/synthetics/server/routes/overview_status/overview_status.ts +++ b/x-pack/plugins/synthetics/server/routes/overview_status/overview_status.ts @@ -38,9 +38,8 @@ export function periodToMs(schedule: { number: string; unit: Unit }) { export async function getStatus(context: RouteContext, params: OverviewStatusQuery) { const { uptimeEsClient, syntheticsMonitorClient, savedObjectsClient, server } = context; - const { query, locations: qLocations, scopeStatusByLocation = true } = params; + const { query, scopeStatusByLocation = true } = params; - const queryLocations = qLocations && !Array.isArray(qLocations) ? [qLocations] : qLocations; /** * Walk through all monitor saved objects, bucket IDs by disabled/enabled status. * @@ -48,7 +47,7 @@ export async function getStatus(context: RouteContext, params: OverviewStatusQue * latest ping for all enabled monitors. */ - const filtersStr = await getMonitorFilters({ + const { filtersStr, locationFilter: queryLocations } = await getMonitorFilters({ ...params, context, }); @@ -73,12 +72,12 @@ export async function getStatus(context: RouteContext, params: OverviewStatusQue allIds, disabledCount, maxPeriod, - listOfLocations, + monitorLocationIds, monitorLocationMap, disabledMonitorsCount, projectMonitorsCount, monitorQueryIdToConfigIdMap, - } = await processMonitors( + } = processMonitors( allMonitors, server, savedObjectsClient, @@ -89,8 +88,8 @@ export async function getStatus(context: RouteContext, params: OverviewStatusQue // Account for locations filter const listOfLocationAfterFilter = queryLocations && scopeStatusByLocation - ? intersection(listOfLocations, queryLocations) - : listOfLocations; + ? intersection(monitorLocationIds, queryLocations) + : monitorLocationIds; const range = { from: moment().subtract(maxPeriod, 'milliseconds').subtract(20, 'minutes').toISOString(), diff --git a/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts b/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts index 204731474ca71..668beba0a8f95 100644 --- a/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts +++ b/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { AgentPolicyInfo } from '../../../../common/types'; import { SyntheticsServerSetup } from '../../../types'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; @@ -13,7 +14,7 @@ export const getAgentPoliciesRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'GET', path: SYNTHETICS_API_URLS.AGENT_POLICIES, validate: {}, - handler: async ({ server, context, uptimeEsClient }): Promise => { + handler: async ({ server }): Promise => { return getAgentPoliciesAsInternalUser(server); }, }); @@ -22,7 +23,7 @@ export const getAgentPoliciesAsInternalUser = async (server: SyntheticsServerSet const soClient = server.coreStart.savedObjects.createInternalRepository(); const esClient = server.coreStart.elasticsearch.client.asInternalUser; - return server.fleet?.agentPolicyService.list(soClient, { + const agentPolicies = await server.fleet?.agentPolicyService.list(soClient, { page: 1, perPage: 10000, sortField: 'name', @@ -31,4 +32,12 @@ export const getAgentPoliciesAsInternalUser = async (server: SyntheticsServerSet esClient, withAgentCount: true, }); + + return agentPolicies.items.map((agentPolicy) => ({ + id: agentPolicy.id, + name: agentPolicy.name, + agents: agentPolicy.agents ?? 0, + status: agentPolicy.status, + description: agentPolicy.description, + })); }; diff --git a/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_private_locations.ts b/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_private_locations.ts index 3a8e8a043d8c8..44a31bad08cff 100644 --- a/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_private_locations.ts +++ b/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_private_locations.ts @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AgentPolicy } from '@kbn/fleet-plugin/common'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { AgentPolicyInfo } from '../../../../common/types'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { SyntheticsPrivateLocations } from '../../../../common/runtime_types'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; @@ -33,7 +33,7 @@ export const getPrivateLocationsRoute: SyntheticsRestApiRouteFactory< export const getPrivateLocationsAndAgentPolicies = async ( savedObjectsClient: SavedObjectsClientContract, syntheticsMonitorClient: SyntheticsMonitorClient -): Promise => { +): Promise => { try { const [privateLocations, agentPolicies] = await Promise.all([ getPrivateLocations(savedObjectsClient), diff --git a/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts b/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts index 7f64b1bed7425..b41c8f5e7538d 100644 --- a/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts +++ b/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AgentPolicy } from '@kbn/fleet-plugin/common'; +import { AgentPolicyInfo } from '../../../../common/types'; import type { SyntheticsPrivateLocations } from '../../../../common/runtime_types'; import type { SyntheticsPrivateLocationsAttributes, @@ -14,7 +14,7 @@ import { PrivateLocation } from '../../../../common/runtime_types'; export const toClientContract = ( attributes: SyntheticsPrivateLocationsAttributes, - agentPolicies?: AgentPolicy[] + agentPolicies?: AgentPolicyInfo[] ): SyntheticsPrivateLocations => { return { locations: attributes.locations.map((location) => ({ diff --git a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor.ts b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor.ts index de2296947e182..d206cd73285ea 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor.ts @@ -190,8 +190,8 @@ export const getSyntheticsMonitorSavedObjectType = ( getTitle: (savedObject) => savedObject.attributes.name + ' - ' + - i18n.translate('xpack.synthetics.syntheticsMonitors', { - defaultMessage: 'Uptime - Monitor', + i18n.translate('xpack.synthetics.syntheticsMonitors.label', { + defaultMessage: 'Synthetics - Monitor', }), }, }; diff --git a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.test.ts b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.test.ts index 1d25503159c58..33af8075616da 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.test.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.test.ts @@ -45,7 +45,7 @@ describe('processMonitors', () => { const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock); it('should return a processed data', async () => { - const result = await processMonitors(testMonitors, serverMock, soClient, monitorClient); + const result = processMonitors(testMonitors, serverMock, soClient, monitorClient); expect(result).toEqual({ allIds: [ 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22', @@ -60,15 +60,15 @@ describe('processMonitors', () => { '7f796001-a795-4c0b-afdb-3ce74edea775', ], disabledMonitorQueryIds: ['test-project-id-default'], - listOfLocations: ['US Central QA', 'US Central Staging', 'North America - US Central'], + monitorLocationIds: ['us_central_qa', 'us_central_staging', 'us_central'], maxPeriod: 600000, monitorLocationMap: { '7f796001-a795-4c0b-afdb-3ce74edea775': [ - 'US Central QA', - 'North America - US Central', - 'US Central Staging', + 'us_central_qa', + 'us_central', + 'us_central_staging', ], - 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22': ['US Central QA', 'US Central Staging'], + 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22': ['us_central_qa', 'us_central_staging'], }, monitorQueryIdToConfigIdMap: { '7f796001-a795-4c0b-afdb-3ce74edea775': '7f796001-a795-4c0b-afdb-3ce74edea775', @@ -81,7 +81,7 @@ describe('processMonitors', () => { it('should return a processed data where location label is missing', async () => { testMonitors[0].attributes.locations[0].label = undefined; - const result = await processMonitors(testMonitors, serverMock, soClient, monitorClient); + const result = processMonitors(testMonitors, serverMock, soClient, monitorClient); expect(result).toEqual({ allIds: [ 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22', @@ -96,20 +96,15 @@ describe('processMonitors', () => { '7f796001-a795-4c0b-afdb-3ce74edea775', ], disabledMonitorQueryIds: ['test-project-id-default'], - listOfLocations: [ - 'US Central Staging', - 'us_central_qa', - 'US Central QA', - 'North America - US Central', - ], + monitorLocationIds: ['us_central_qa', 'us_central_staging', 'us_central'], maxPeriod: 600000, monitorLocationMap: { '7f796001-a795-4c0b-afdb-3ce74edea775': [ - 'US Central QA', - 'North America - US Central', - 'US Central Staging', + 'us_central_qa', + 'us_central', + 'us_central_staging', ], - 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22': ['US Central Staging', 'us_central_qa'], + 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22': ['us_central_qa', 'us_central_staging'], }, monitorQueryIdToConfigIdMap: { '7f796001-a795-4c0b-afdb-3ce74edea775': '7f796001-a795-4c0b-afdb-3ce74edea775', @@ -160,7 +155,7 @@ describe('processMonitors', () => { ) ); - const result = await processMonitors(testMonitors, serverMock, soClient, monitorClient); + const result = processMonitors(testMonitors, serverMock, soClient, monitorClient); expect(result).toEqual({ allIds: [ 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22', @@ -175,15 +170,15 @@ describe('processMonitors', () => { '7f796001-a795-4c0b-afdb-3ce74edea775', ], disabledMonitorQueryIds: ['test-project-id-default'], - listOfLocations: ['US Central Staging', 'US Central QA', 'North America - US Central'], + monitorLocationIds: ['us_central_qa', 'us_central_staging', 'us_central'], maxPeriod: 600000, monitorLocationMap: { '7f796001-a795-4c0b-afdb-3ce74edea775': [ - 'US Central QA', - 'North America - US Central', - 'US Central Staging', + 'us_central_qa', + 'us_central', + 'us_central_staging', ], - 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22': ['US Central Staging', 'US Central QA'], + 'aa925d91-40b0-4f8f-b695-bb9b53cd4e22': ['us_central_qa', 'us_central_staging'], }, monitorQueryIdToConfigIdMap: { '7f796001-a795-4c0b-afdb-3ce74edea775': '7f796001-a795-4c0b-afdb-3ce74edea775', diff --git a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts index 9e629ba304a14..b8e9a08bf3dd1 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts @@ -10,16 +10,13 @@ import { SavedObjectsFindOptions, SavedObjectsFindResult, } from '@kbn/core-saved-objects-api-server'; -import pMap from 'p-map'; import { intersection } from 'lodash'; import { SyntheticsServerSetup } from '../../types'; import { syntheticsMonitorType } from '../../../common/types/saved_objects'; import { periodToMs } from '../../routes/overview_status/overview_status'; -import { getAllLocations } from '../../synthetics_service/get_all_locations'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes, - ServiceLocation, SourceType, } from '../../../common/runtime_types'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; @@ -59,7 +56,7 @@ export const getAllMonitors = async ({ return hits; }; -export const processMonitors = async ( +export const processMonitors = ( allMonitors: Array>, server: SyntheticsServerSetup, soClient: SavedObjectsClientContract, @@ -83,22 +80,6 @@ export const processMonitors = async ( const monitorLocationMap: Record = {}; const monitorQueryIdToConfigIdMap: Record = {}; - let allLocations: ServiceLocation[] | null = null; - - const getLocationLabel = async (locationId: string) => { - if (!allLocations) { - const { publicLocations, privateLocations } = await getAllLocations({ - server, - syntheticsMonitorClient, - savedObjectsClient: soClient, - }); - - allLocations = [...publicLocations, ...privateLocations]; - } - - return allLocations.find((loc) => loc.id === locationId)?.label ?? locationId; - }; - for (const monitor of allMonitors) { const attrs = monitor.attributes; @@ -108,8 +89,9 @@ export const processMonitors = async ( monitorQueryIdToConfigIdMap[attrs[ConfigKey.MONITOR_QUERY_ID]] = attrs[ConfigKey.CONFIG_ID]; + const monitorLocations = attrs[ConfigKey.LOCATIONS].map((location) => location.id); + if (attrs[ConfigKey.ENABLED] === false) { - const monitorLocations = attrs[ConfigKey.LOCATIONS].map((location) => location.label); const queriedLocations = Array.isArray(queryLocations) ? queryLocations : [queryLocations]; const intersectingLocations = intersection( monitorLocations, @@ -119,32 +101,12 @@ export const processMonitors = async ( disabledMonitorsCount += 1; disabledMonitorQueryIds.push(attrs[ConfigKey.MONITOR_QUERY_ID]); } else { - const missingLabels = new Set(); - enabledMonitorQueryIds.push(attrs[ConfigKey.MONITOR_QUERY_ID]); - const monLocs = new Set([ - ...(attrs[ConfigKey.LOCATIONS] - .filter((loc) => { - if (!loc.label) { - missingLabels.add(loc.id); - } - return loc.label; - }) - .map((location) => location.label) as string[]), - ]); - - // since label wasn't always part of location, there can be a case where we have a location - // with an id but no label. We need to fetch the label from the API - // Adding a migration to add the label to the saved object is a future consideration - const locLabels = await pMap([...missingLabels], async (locationId) => - getLocationLabel(locationId) - ); - const monitorLocations = [...monLocs, ...locLabels]; monitorLocationMap[attrs[ConfigKey.MONITOR_QUERY_ID]] = queryLocations ? intersection(monitorLocations, queryLocations) : monitorLocations; - listOfLocationsSet = new Set([...listOfLocationsSet, ...monLocs, ...locLabels]); + listOfLocationsSet = new Set([...listOfLocationsSet, ...monitorLocations]); maxPeriod = Math.max(maxPeriod, periodToMs(attrs[ConfigKey.SCHEDULE])); } @@ -159,7 +121,7 @@ export const processMonitors = async ( monitorLocationMap, disabledMonitorsCount, projectMonitorsCount, - listOfLocations: [...listOfLocationsSet], + monitorLocationIds: [...listOfLocationsSet], monitorQueryIdToConfigIdMap, }; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts index a1d22b3404f03..69788a6f10ae0 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts @@ -435,9 +435,7 @@ export class SyntheticsPrivateLocation { } async getAgentPolicies() { - const agentPolicies = await getAgentPoliciesAsInternalUser(this.server); - - return agentPolicies.items; + return await getAgentPoliciesAsInternalUser(this.server); } } diff --git a/x-pack/plugins/synthetics/tsconfig.json b/x-pack/plugins/synthetics/tsconfig.json index 4862aa4729aa1..c140e4053e4a9 100644 --- a/x-pack/plugins/synthetics/tsconfig.json +++ b/x-pack/plugins/synthetics/tsconfig.json @@ -78,6 +78,8 @@ "@kbn/shared-ux-page-kibana-template", "@kbn/observability-ai-assistant-plugin", "@kbn/stack-alerts-plugin", + "@kbn/unified-doc-viewer-plugin", + "@kbn/discover-utils", ], "exclude": [ "target/**/*", 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 61ddfa8e00505..8e517ada4ddca 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -2728,76 +2728,148 @@ "services_per_agent": { "properties": { "android/java": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the android/java agent within the last day" + } }, "dotnet": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the dotnet (.Net) agent within the last day" + } }, "iOS/swift": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the iOS/swift agent within the last day" + } }, "go": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the go agent within the last day" + } }, "java": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the Java agent within the last day" + } }, "js-base": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the js-base agent within the last day" + } }, "nodejs": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the nodeJS agent within the last day" + } }, "php": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the PHH agent within the last day" + } }, "python": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the Python agent within the last day" + } }, "ruby": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the Ruby agent within the last day" + } }, "rum-js": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the rum-js agent within the last day" + } }, "otlp": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the otlp agent within the last day" + } }, "opentelemetry/cpp": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/cpp agent within the last day" + } }, "opentelemetry/dotnet": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/dotnet agent within the last day" + } }, "opentelemetry/erlang": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/erlang agent within the last day" + } }, "opentelemetry/go": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/go agent within the last day" + } }, "opentelemetry/java": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/java agent within the last day" + } }, "opentelemetry/nodejs": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/nodejs agent within the last day" + } }, "opentelemetry/php": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/php agent within the last day" + } }, "opentelemetry/python": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/python agent within the last day" + } }, "opentelemetry/ruby": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/ruby agent within the last day" + } }, "opentelemetry/rust": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/rust agent within the last day" + } }, "opentelemetry/swift": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/swift agent within the last day" + } }, "opentelemetry/webjs": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/webjs agent within the last day" + } } } }, @@ -4220,60 +4292,96 @@ "current_implementation": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "no_observer_name": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "no_rum": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "no_rum_no_observer_name": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "only_rum": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } }, "only_rum_no_observer_name": { "properties": { "expected_metric_document_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } }, "transaction_count": { - "type": "long" + "type": "long", + "_meta": { + "description": "" + } } } } @@ -4458,10 +4566,10 @@ "services": { "properties": { "1d": { - "type": "long" - }, - "all": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of unique services within the last day" + } } } }, @@ -4644,7 +4752,10 @@ "shards": { "properties": { "total": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of shards for metric indices" + } } } }, @@ -4655,14 +4766,20 @@ "docs": { "properties": { "count": { - "type": "long" + "type": "long", + "_meta": { + "description": "Total number of metric documents overall" + } } } }, "store": { "properties": { "size_in_bytes": { - "type": "long" + "type": "long", + "_meta": { + "description": "Size of the metric indicess in byte units overall." + } } } } @@ -4679,7 +4796,7 @@ "total": { "type": "long", "_meta": { - "description": "Total number of shards overall" + "description": "Total number of shards for span and trasnaction indices" } } } @@ -4928,7 +5045,10 @@ "pod": { "properties": { "name": { - "type": "keyword" + "type": "keyword", + "_meta": { + "description": "Kuberneted pod name " + } } } } @@ -4937,7 +5057,10 @@ "container": { "properties": { "id": { - "type": "keyword" + "type": "keyword", + "_meta": { + "description": "Container id" + } } } } diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts index 5b300a613be94..65e0433a81264 100644 --- a/x-pack/plugins/transform/common/api_schemas/transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema, type TypeOf } from '@kbn/config-schema'; import type { ES_FIELD_TYPES } from '@kbn/field-types'; diff --git a/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts b/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts index 459d37414e244..a9d177a341e21 100644 --- a/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts +++ b/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts @@ -13,7 +13,7 @@ import { getTransformsRequestSchema } from './transforms'; export const getTransformsStatsRequestSchema = getTransformsRequestSchema; -export type GetTransformsRequestSchema = TypeOf; +export type GetTransformsStatsRequestSchema = TypeOf; export interface GetTransformsStatsResponseSchema { node_failures?: object; diff --git a/x-pack/plugins/transform/common/api_schemas/type_guards.ts b/x-pack/plugins/transform/common/api_schemas/type_guards.ts deleted file mode 100644 index d5dbad0056c35..0000000000000 --- a/x-pack/plugins/transform/common/api_schemas/type_guards.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; - -import type { EsIndex } from '../types/es_index'; -import type { EsIngestPipeline } from '../types/es_ingest_pipeline'; - -// To be able to use the type guards on the client side, we need to make sure we don't import -// the code of '@kbn/config-schema' but just its types, otherwise the client side code will -// fail to build. -import type { FieldHistogramsResponseSchema } from './field_histograms'; -import type { GetTransformsAuditMessagesResponseSchema } from './audit_messages'; -import type { DeleteTransformsResponseSchema } from './delete_transforms'; -import type { ResetTransformsResponseSchema } from './reset_transforms'; -import type { StartTransformsResponseSchema } from './start_transforms'; -import type { StopTransformsResponseSchema } from './stop_transforms'; -import type { ScheduleNowTransformsResponseSchema } from './schedule_now_transforms'; -import type { - GetTransformNodesResponseSchema, - GetTransformsResponseSchema, - PostTransformsPreviewResponseSchema, - PutTransformsResponseSchema, -} from './transforms'; -import type { GetTransformsStatsResponseSchema } from './transforms_stats'; -import type { PostTransformsUpdateResponseSchema } from './update_transforms'; - -const isGenericResponseSchema = (arg: any): arg is T => { - return isPopulatedObject(arg, ['count', 'transforms']) && Array.isArray(arg.transforms); -}; - -export const isGetTransformNodesResponseSchema = ( - arg: unknown -): arg is GetTransformNodesResponseSchema => { - return isPopulatedObject(arg, ['count']) && typeof arg.count === 'number'; -}; - -export const isGetTransformsResponseSchema = (arg: unknown): arg is GetTransformsResponseSchema => { - return isGenericResponseSchema(arg); -}; - -export const isGetTransformsStatsResponseSchema = ( - arg: unknown -): arg is GetTransformsStatsResponseSchema => { - return isGenericResponseSchema(arg); -}; - -export const isDeleteTransformsResponseSchema = ( - arg: unknown -): arg is DeleteTransformsResponseSchema => { - return ( - isPopulatedObject(arg) && - Object.values(arg).every((d) => isPopulatedObject(d, ['transformDeleted'])) - ); -}; - -export const isResetTransformsResponseSchema = ( - arg: unknown -): arg is ResetTransformsResponseSchema => { - return ( - isPopulatedObject(arg) && - Object.values(arg).every((d) => isPopulatedObject(d, ['transformReset'])) - ); -}; - -export const isEsIndices = (arg: unknown): arg is EsIndex[] => { - return Array.isArray(arg); -}; - -export const isEsIngestPipelines = (arg: unknown): arg is EsIngestPipeline[] => { - return Array.isArray(arg) && arg.every((d) => isPopulatedObject(d, ['name'])); -}; - -export const isEsSearchResponse = (arg: unknown): arg is estypes.SearchResponse => { - return isPopulatedObject(arg, ['hits']); -}; - -type SearchResponseWithAggregations = Required> & - estypes.SearchResponse; -export const isEsSearchResponseWithAggregations = ( - arg: unknown -): arg is SearchResponseWithAggregations => { - return isEsSearchResponse(arg) && {}.hasOwnProperty.call(arg, 'aggregations'); -}; - -export const isFieldHistogramsResponseSchema = ( - arg: unknown -): arg is FieldHistogramsResponseSchema => { - return Array.isArray(arg); -}; - -export const isGetTransformsAuditMessagesResponseSchema = ( - arg: unknown -): arg is GetTransformsAuditMessagesResponseSchema => { - return isPopulatedObject(arg, ['messages', 'total']); -}; - -export const isPostTransformsPreviewResponseSchema = ( - arg: unknown -): arg is PostTransformsPreviewResponseSchema => { - return ( - isPopulatedObject(arg, ['generated_dest_index', 'preview']) && - typeof arg.generated_dest_index !== undefined && - Array.isArray(arg.preview) - ); -}; - -export const isPostTransformsUpdateResponseSchema = ( - arg: unknown -): arg is PostTransformsUpdateResponseSchema => { - return isPopulatedObject(arg, ['id']) && typeof arg.id === 'string'; -}; - -export const isPutTransformsResponseSchema = (arg: unknown): arg is PutTransformsResponseSchema => { - return ( - isPopulatedObject(arg, ['transformsCreated', 'errors']) && - Array.isArray(arg.transformsCreated) && - Array.isArray(arg.errors) - ); -}; - -const isGenericSuccessResponseSchema = (arg: unknown) => - isPopulatedObject(arg) && Object.values(arg).every((d) => isPopulatedObject(d, ['success'])); - -export const isStartTransformsResponseSchema = ( - arg: unknown -): arg is StartTransformsResponseSchema => { - return isGenericSuccessResponseSchema(arg); -}; - -export const isStopTransformsResponseSchema = ( - arg: unknown -): arg is StopTransformsResponseSchema => { - return isGenericSuccessResponseSchema(arg); -}; - -export const isScheduleNowTransformsResponseSchema = ( - arg: unknown -): arg is ScheduleNowTransformsResponseSchema => { - return isGenericSuccessResponseSchema(arg); -}; diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index 633f0dc7849ac..a1ba2d8277af9 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -32,6 +32,21 @@ const EXTERNAL_API_BASE_PATH = '/api/transform/'; export const addInternalBasePath = (uri: string): string => `${INTERNAL_API_BASE_PATH}${uri}`; export const addExternalBasePath = (uri: string): string => `${EXTERNAL_API_BASE_PATH}${uri}`; +export const TRANSFORM_REACT_QUERY_KEYS = { + DATA_SEARCH: 'transform.data_search', + DATA_VIEW_EXISTS: 'transform.data_view_exists', + GET_DATA_VIEW_TITLES: 'transform.get_data_view_titles', + GET_ES_INDICES: 'transform.get_es_indices', + GET_ES_INGEST_PIPELINES: 'transform.get_es_ingest_pipelines', + GET_HISTOGRAMS_FOR_FIELDS: 'transform.get_histograms_for_fields', + GET_TRANSFORM: 'transform.get_transform', + GET_TRANSFORM_NODES: 'transform.get_transform_nodes', + GET_TRANSFORM_AUDIT_MESSAGES: 'transform.get_transform_audit_messages', + GET_TRANSFORM_STATS: 'transform.get_transform_stats', + GET_TRANSFORMS: 'transform.get_transforms', + GET_TRANSFORMS_PREVIEW: 'transform.get_transforms_preview', +} as const; + // In order to create a transform, the API requires the following privileges: // - transform_admin (builtin) // - cluster privileges: manage_transform @@ -71,22 +86,6 @@ export const APP_CLUSTER_PRIVILEGES = [ // Minimum privileges required to return transform node count export const NODES_INFO_PRIVILEGES = ['cluster:monitor/transform/get']; -// Equivalent of capabilities.canGetTransform -export const APP_GET_TRANSFORM_CLUSTER_PRIVILEGES = [ - 'cluster.cluster:monitor/transform/get', - 'cluster.cluster:monitor/transform/stats/get', -]; - -// Equivalent of capabilities.canCreateTransform -export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [ - 'cluster.cluster:monitor/transform/get', - 'cluster.cluster:monitor/transform/stats/get', - 'cluster.cluster:admin/transform/preview', - 'cluster.cluster:admin/transform/put', - 'cluster.cluster:admin/transform/start', - 'cluster.cluster:admin/transform/start_task', -]; - export const APP_INDEX_PRIVILEGES = ['monitor']; // reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformStats.java#L214 diff --git a/x-pack/plugins/transform/common/privilege/has_privilege_factory.ts b/x-pack/plugins/transform/common/privilege/has_privilege_factory.ts deleted file mode 100644 index 9dee0c1a73cf1..0000000000000 --- a/x-pack/plugins/transform/common/privilege/has_privilege_factory.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; - -import { cloneDeep } from 'lodash'; -import { APP_INDEX_PRIVILEGES } from '../constants'; -import { Privileges } from '../types/privileges'; - -export interface PrivilegesAndCapabilities { - privileges: Privileges; - capabilities: Capabilities; -} - -export interface TransformCapabilities { - canGetTransform: boolean; - canDeleteTransform: boolean; - canPreviewTransform: boolean; - canCreateTransform: boolean; - canReauthorizeTransform: boolean; - canScheduleNowTransform: boolean; - canStartStopTransform: boolean; - canCreateTransformAlerts: boolean; - canUseTransformAlerts: boolean; - canResetTransform: boolean; -} -export type Capabilities = { [k in keyof TransformCapabilities]: boolean }; - -export const INITIAL_CAPABILITIES = Object.freeze({ - canGetTransform: false, - canDeleteTransform: false, - canPreviewTransform: false, - canCreateTransform: false, - canReauthorizeTransform: false, - canScheduleNowTransform: false, - canStartStopTransform: false, - canCreateTransformAlerts: false, - canUseTransformAlerts: false, - canResetTransform: false, -}); - -export type Privilege = [string, string]; - -function isPrivileges(arg: unknown): arg is Privileges { - return ( - isPopulatedObject(arg, ['hasAllPrivileges', 'missingPrivileges']) && - typeof arg.hasAllPrivileges === 'boolean' && - typeof arg.missingPrivileges === 'object' && - arg.missingPrivileges !== null - ); -} - -export const toArray = (value: string | string[]): string[] => - Array.isArray(value) ? value : [value]; - -export const hasPrivilegeFactory = - (privileges: Privileges | undefined | null) => (privilege: Privilege) => { - const [section, requiredPrivilege] = privilege; - if (isPrivileges(privileges) && !privileges.missingPrivileges[section]) { - // if the section does not exist in our missingPrivileges, everything is OK - return true; - } - if (isPrivileges(privileges) && privileges.missingPrivileges[section]!.length === 0) { - return true; - } - if (requiredPrivilege === '*') { - // If length > 0 and we require them all... KO - return false; - } - // If we require _some_ privilege, we make sure that the one - // we require is *not* in the missingPrivilege array - return ( - isPrivileges(privileges) && - !privileges.missingPrivileges[section]!.includes(requiredPrivilege) - ); - }; - -export const extractMissingPrivileges = ( - privilegesObject: { [key: string]: boolean } = {} -): string[] => - Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { - if (!privilegesObject[privilegeName]) { - privileges.push(privilegeName); - } - return privileges; - }, []); - -export const getPrivilegesAndCapabilities = ( - clusterPrivileges: Record, - hasOneIndexWithAllPrivileges: boolean, - hasAllPrivileges: boolean -): PrivilegesAndCapabilities => { - const privilegesResult: Privileges = { - hasAllPrivileges: true, - missingPrivileges: { - cluster: [], - index: [], - }, - }; - - // Find missing cluster privileges and set overall app privileges - privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(clusterPrivileges); - privilegesResult.hasAllPrivileges = hasAllPrivileges; - - if (!hasOneIndexWithAllPrivileges) { - privilegesResult.missingPrivileges.index = [...APP_INDEX_PRIVILEGES]; - } - - const hasPrivilege = hasPrivilegeFactory(privilegesResult); - - const capabilities = cloneDeep(INITIAL_CAPABILITIES); - capabilities.canGetTransform = - hasPrivilege(['cluster', 'cluster:monitor/transform/get']) && - hasPrivilege(['cluster', 'cluster:monitor/transform/stats/get']); - - capabilities.canCreateTransform = hasPrivilege(['cluster', 'cluster:admin/transform/put']); - - capabilities.canDeleteTransform = hasPrivilege(['cluster', 'cluster:admin/transform/delete']); - - capabilities.canResetTransform = hasPrivilege(['cluster', 'cluster:admin/transform/reset']); - - capabilities.canPreviewTransform = hasPrivilege(['cluster', 'cluster:admin/transform/preview']); - - capabilities.canStartStopTransform = - hasPrivilege(['cluster', 'cluster:admin/transform/start']) && - hasPrivilege(['cluster', 'cluster:admin/transform/start_task']) && - hasPrivilege(['cluster', 'cluster:admin/transform/stop']); - - capabilities.canCreateTransformAlerts = capabilities.canCreateTransform; - - capabilities.canUseTransformAlerts = capabilities.canGetTransform; - - capabilities.canScheduleNowTransform = capabilities.canStartStopTransform; - - capabilities.canReauthorizeTransform = capabilities.canStartStopTransform; - - return { privileges: privilegesResult, capabilities }; -}; -// create the text for button's tooltips if the user -// doesn't have the permission to press that button -export function createCapabilityFailureMessage( - capability: keyof TransformCapabilities | 'noTransformNodes' -) { - let message = ''; - - switch (capability) { - case 'canCreateTransform': - message = i18n.translate('xpack.transform.capability.noPermission.createTransformTooltip', { - defaultMessage: 'You do not have permission to create transforms.', - }); - break; - case 'canCreateTransformAlerts': - message = i18n.translate( - 'xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip', - { - defaultMessage: 'You do not have permission to create transform alert rules.', - } - ); - break; - case 'canScheduleNowTransform': - message = i18n.translate( - 'xpack.transform.capability.noPermission.scheduleNowTransformTooltip', - { - defaultMessage: - 'You do not have permission to schedule transforms to process data instantly.', - } - ); - break; - case 'canStartStopTransform': - message = i18n.translate( - 'xpack.transform.capability.noPermission.startOrStopTransformTooltip', - { - defaultMessage: 'You do not have permission to start or stop transforms.', - } - ); - break; - - case 'canReauthorizeTransform': - message = i18n.translate( - 'xpack.transform.capability.noPermission.reauthorizeTransformTooltip', - { - defaultMessage: 'You do not have permission to reauthorize transforms.', - } - ); - break; - - case 'canDeleteTransform': - message = i18n.translate('xpack.transform.capability.noPermission.deleteTransformTooltip', { - defaultMessage: 'You do not have permission to delete transforms.', - }); - break; - - case 'canResetTransform': - message = i18n.translate('xpack.transform.capability.noPermission.resetTransformTooltip', { - defaultMessage: 'You do not have permission to reset transforms.', - }); - break; - - case 'noTransformNodes': - message = i18n.translate('xpack.transform.capability.noPermission.noTransformNodesTooltip', { - defaultMessage: 'There are no transform nodes available.', - }); - break; - } - - return i18n.translate('xpack.transform.capability.pleaseContactAdministratorTooltip', { - defaultMessage: '{message} Please contact your administrator.', - values: { - message, - }, - }); -} diff --git a/x-pack/plugins/transform/common/types/capabilities.ts b/x-pack/plugins/transform/common/types/capabilities.ts new file mode 100644 index 0000000000000..ae5e241e3b0fe --- /dev/null +++ b/x-pack/plugins/transform/common/types/capabilities.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 { isPopulatedObject } from '@kbn/ml-is-populated-object'; + +export const getInitialTransformCapabilities = (initialSetting = false) => ({ + canCreateTransform: initialSetting, + canCreateTransformAlerts: initialSetting, + canDeleteIndex: initialSetting, + canDeleteTransform: initialSetting, + canGetTransform: initialSetting, + canPreviewTransform: initialSetting, + canReauthorizeTransform: initialSetting, + canResetTransform: initialSetting, + canScheduleNowTransform: initialSetting, + canStartStopTransform: initialSetting, + canUseTransformAlerts: initialSetting, +}); + +export const isTransformCapabilities = (arg: unknown): arg is TransformCapabilities => { + return ( + isPopulatedObject(arg, Object.keys(getInitialTransformCapabilities())) && + Object.values(arg).every((d) => typeof d === 'boolean') + ); +}; + +export type TransformCapabilities = ReturnType; +export type TransformCapability = keyof TransformCapabilities; + +export interface PrivilegesAndCapabilities { + privileges: Privileges; + capabilities: TransformCapabilities; +} + +export type Privilege = [string, string]; + +export interface MissingPrivileges { + [key: string]: string[] | undefined; +} + +export interface Privileges { + hasAllPrivileges: boolean; + missingPrivileges: MissingPrivileges; +} diff --git a/x-pack/plugins/transform/common/utils/create_capability_failure_message.ts b/x-pack/plugins/transform/common/utils/create_capability_failure_message.ts new file mode 100644 index 0000000000000..7f0f660bd2514 --- /dev/null +++ b/x-pack/plugins/transform/common/utils/create_capability_failure_message.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +import type { TransformCapabilities } from '../types/capabilities'; + +// create the text for button's tooltips if the user +// doesn't have the permission to press that button +export function createCapabilityFailureMessage( + capability: keyof TransformCapabilities | 'noTransformNodes' +) { + let message = ''; + + switch (capability) { + case 'canCreateTransform': + message = i18n.translate('xpack.transform.capability.noPermission.createTransformTooltip', { + defaultMessage: 'You do not have permission to create transforms.', + }); + break; + case 'canCreateTransformAlerts': + message = i18n.translate( + 'xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip', + { + defaultMessage: 'You do not have permission to create transform alert rules.', + } + ); + break; + case 'canScheduleNowTransform': + message = i18n.translate( + 'xpack.transform.capability.noPermission.scheduleNowTransformTooltip', + { + defaultMessage: + 'You do not have permission to schedule transforms to process data instantly.', + } + ); + break; + case 'canStartStopTransform': + message = i18n.translate( + 'xpack.transform.capability.noPermission.startOrStopTransformTooltip', + { + defaultMessage: 'You do not have permission to start or stop transforms.', + } + ); + break; + + case 'canReauthorizeTransform': + message = i18n.translate( + 'xpack.transform.capability.noPermission.reauthorizeTransformTooltip', + { + defaultMessage: 'You do not have permission to reauthorize transforms.', + } + ); + break; + + case 'canDeleteTransform': + message = i18n.translate('xpack.transform.capability.noPermission.deleteTransformTooltip', { + defaultMessage: 'You do not have permission to delete transforms.', + }); + break; + + case 'canResetTransform': + message = i18n.translate('xpack.transform.capability.noPermission.resetTransformTooltip', { + defaultMessage: 'You do not have permission to reset transforms.', + }); + break; + + case 'noTransformNodes': + message = i18n.translate('xpack.transform.capability.noPermission.noTransformNodesTooltip', { + defaultMessage: 'There are no transform nodes available.', + }); + break; + } + + return i18n.translate('xpack.transform.capability.pleaseContactAdministratorTooltip', { + defaultMessage: '{message} Please contact your administrator.', + values: { + message, + }, + }); +} diff --git a/x-pack/plugins/transform/common/utils/to_array.ts b/x-pack/plugins/transform/common/utils/to_array.ts new file mode 100644 index 0000000000000..f41ceea9fc180 --- /dev/null +++ b/x-pack/plugins/transform/common/utils/to_array.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 const toArray = (value: T | T[]): T[] => (Array.isArray(value) ? value : [value]); diff --git a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_health_rule_trigger.tsx b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_health_rule_trigger.tsx index 56bd9a2db877e..49b8ee1a684fb 100644 --- a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_health_rule_trigger.tsx +++ b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_health_rule_trigger.tsx @@ -6,16 +6,15 @@ */ import { EuiForm, EuiSpacer } from '@elastic/eui'; -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; import type { TransformHealthRuleParams } from '../../../common/types/alerting'; import { TestsSelectionControl } from './tests_selection_control'; import { TransformSelectorControl } from './transform_selector_control'; -import { useApi } from '../../app/hooks'; +import { useGetTransforms } from '../../app/hooks'; import { useToastNotifications } from '../../app/app_dependencies'; -import { GetTransformsResponseSchema } from '../../../common/api_schemas/transforms'; import { ALL_TRANSFORMS_SELECTION } from '../../../common/constants'; export type TransformHealthRuleTriggerProps = @@ -29,9 +28,12 @@ const TransformHealthRuleTrigger: FC = ({ const formErrors = Object.values(errors).flat(); const isFormInvalid = formErrors.length > 0; - const api = useApi(); const toast = useToastNotifications(); - const [transformOptions, setTransformOptions] = useState([]); + const { error, data } = useGetTransforms(); + const transformOptions = useMemo( + () => data?.transforms.filter((v) => v.config.sync).map((v) => v.id) ?? [], + [data] + ); const onAlertParamChange = useCallback( (param: T) => @@ -41,34 +43,18 @@ const TransformHealthRuleTrigger: FC = ({ [setRuleParams] ); - useEffect( - function fetchTransforms() { - let unmounted = false; - api - .getTransforms() - .then((r) => { - if (!unmounted) { - setTransformOptions( - (r as GetTransformsResponseSchema).transforms.filter((v) => v.sync).map((v) => v.id) - ); + useEffect(() => { + if (error !== null) { + toast.addError(error, { + title: i18n.translate( + 'xpack.transform.alertingRuleTypes.transformHealth.fetchErrorMessage', + { + defaultMessage: 'Unable to fetch transforms', } - }) - .catch((e) => { - toast.addError(e, { - title: i18n.translate( - 'xpack.transform.alertingRuleTypes.transformHealth.fetchErrorMessage', - { - defaultMessage: 'Unable to fetch transforms', - } - ), - }); - }); - return () => { - unmounted = true; - }; - }, - [api, toast] - ); + ), + }); + } + }, [error, toast]); const excludeTransformOptions = useMemo(() => { if (ruleParams.includeTransforms?.some((v) => v === ALL_TRANSFORMS_SELECTION)) { diff --git a/x-pack/plugins/transform/public/app/app.tsx b/x-pack/plugins/transform/public/app/app.tsx index ba4a43bfa0876..505009784d26e 100644 --- a/x-pack/plugins/transform/public/app/app.tsx +++ b/x-pack/plugins/transform/public/app/app.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useContext, FC } from 'react'; +import React, { type FC } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -13,68 +13,50 @@ import { EuiErrorBoundary } from '@elastic/eui'; import { Router, Routes, Route } from '@kbn/shared-ux-router'; import { ScopedHistory } from '@kbn/core/public'; -import { FormattedMessage } from '@kbn/i18n-react'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import { addInternalBasePath } from '../../common/constants'; - -import { SectionError } from './components'; import { SECTION_SLUG } from './common/constants'; -import { AuthorizationContext, AuthorizationProvider } from './lib/authorization'; import { AppDependencies } from './app_dependencies'; import { CloneTransformSection } from './sections/clone_transform'; import { CreateTransformSection } from './sections/create_transform'; import { TransformManagementSection } from './sections/transform_management'; -export const App: FC<{ history: ScopedHistory }> = ({ history }) => { - const { apiError } = useContext(AuthorizationContext); - if (apiError !== null) { - return ( - - } - error={apiError} +export const App: FC<{ history: ScopedHistory }> = ({ history }) => ( + + + - ); - } - - return ( - - - - - - - - ); -}; + + + + +); export const renderApp = (element: HTMLElement, appDependencies: AppDependencies) => { const I18nContext = appDependencies.i18n.Context; - const queryClient = new QueryClient(); + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: Infinity, + retry: false, + }, + }, + }); render( - - - - - + + + diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index c7656974ec569..2bea1421277ef 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -19,12 +19,7 @@ export { toggleSelectedField, } from './fields'; export type { DropDownLabel, DropDownOption, Label } from './dropdown'; -export { - isTransformIdValid, - refreshTransformList$, - useRefreshTransformList, - REFRESH_TRANSFORM_LIST_STATE, -} from './transform'; +export { isTransformIdValid } from './transform'; export type { TransformListAction, TransformListRow } from './transform_list'; export { TRANSFORM_LIST_COLUMN } from './transform_list'; export { getTransformProgress, isCompletedBatchTransform } from './transform_stats'; diff --git a/x-pack/plugins/transform/public/app/common/navigation.tsx b/x-pack/plugins/transform/public/app/common/navigation.tsx index 601e701c7f16d..ef6696d3e757a 100644 --- a/x-pack/plugins/transform/public/app/common/navigation.tsx +++ b/x-pack/plugins/transform/public/app/common/navigation.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { Redirect } from 'react-router-dom'; import { SECTION_SLUG } from './constants'; diff --git a/x-pack/plugins/transform/public/app/common/transform.ts b/x-pack/plugins/transform/public/app/common/transform.ts index 35ead5691a866..b689ee8096983 100644 --- a/x-pack/plugins/transform/public/app/common/transform.ts +++ b/x-pack/plugins/transform/public/app/common/transform.ts @@ -5,11 +5,8 @@ * 2.0. */ -import { useEffect } from 'react'; -import { BehaviorSubject } from 'rxjs'; -import { filter, distinctUntilChanged } from 'rxjs/operators'; -import { Subscription } from 'rxjs'; import { cloneDeep } from 'lodash'; + import type { TransformConfigUnion, TransformId } from '../../../common/types/transform'; // Via https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/utils/TransformStrings.java#L24 @@ -23,64 +20,6 @@ export function isTransformIdValid(transformId: TransformId) { export const TRANSFORM_ERROR_TYPE = { DANGLING_TASK: 'dangling_task', } as const; -export enum REFRESH_TRANSFORM_LIST_STATE { - ERROR = 'error', - IDLE = 'idle', - LOADING = 'loading', - REFRESH = 'refresh', -} -export const refreshTransformList$ = new BehaviorSubject( - REFRESH_TRANSFORM_LIST_STATE.IDLE -); - -export const useRefreshTransformList = ( - callback: { - isLoading?(d: boolean): void; - onRefresh?(): void; - } = {} -) => { - useEffect(() => { - const distinct$ = refreshTransformList$.pipe(distinctUntilChanged()); - - const subscriptions: Subscription[] = []; - - if (typeof callback.onRefresh === 'function') { - // initial call to refresh - callback.onRefresh(); - - subscriptions.push( - distinct$ - .pipe(filter((state) => state === REFRESH_TRANSFORM_LIST_STATE.REFRESH)) - .subscribe(() => typeof callback.onRefresh === 'function' && callback.onRefresh()) - ); - } - - if (typeof callback.isLoading === 'function') { - subscriptions.push( - distinct$.subscribe( - (state) => - typeof callback.isLoading === 'function' && - callback.isLoading(state === REFRESH_TRANSFORM_LIST_STATE.LOADING) - ) - ); - } - - return () => { - subscriptions.map((sub) => sub.unsubscribe()); - }; - // The effect should only be called once. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { - refresh: () => { - // A refresh is followed immediately by setting the state to loading - // to trigger data fetching and loading indicators in one go. - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.LOADING); - }, - }; -}; export const overrideTransformForCloning = (originalConfig: TransformConfigUnion) => { // 'Managed' means job is preconfigured and deployed by other solutions diff --git a/x-pack/plugins/transform/public/app/components/capabilities_wrapper.tsx b/x-pack/plugins/transform/public/app/components/capabilities_wrapper.tsx new file mode 100644 index 0000000000000..ce850aa2ffc6c --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/capabilities_wrapper.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { type FC } from 'react'; + +import { EuiFlexItem, EuiFlexGroup, EuiPageTemplate, EuiEmptyPrompt } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { TransformCapability } from '../../../common/types/capabilities'; +import { toArray } from '../../../common/utils/to_array'; + +import { useTransformCapabilities } from '../hooks'; + +interface Props { + title: React.ReactNode; + message: React.ReactNode | string; +} + +export const NotAuthorizedSection = ({ title, message }: Props) => ( + {title}} body={

{message}

} /> +); + +const MissingCapabilities: FC = () => ( + + + + + } + message={ + + } + /> + + + +); + +export const CapabilitiesWrapper: FC<{ + requiredCapabilities: TransformCapability | TransformCapability[]; +}> = ({ children, requiredCapabilities }) => { + const capabilities = useTransformCapabilities(); + + const hasCapabilities = toArray(requiredCapabilities).every((c) => capabilities[c]); + + return hasCapabilities ? <>{children} : ; +}; diff --git a/x-pack/plugins/transform/public/app/components/index.ts b/x-pack/plugins/transform/public/app/components/index.ts index 7c3cb454aecb3..2edcf498e5832 100644 --- a/x-pack/plugins/transform/public/app/components/index.ts +++ b/x-pack/plugins/transform/public/app/components/index.ts @@ -5,6 +5,4 @@ * 2.0. */ -export { SectionError } from './section_error'; -export { SectionLoading } from './section_loading'; export { ToastNotificationText } from './toast_notification_text'; diff --git a/x-pack/plugins/transform/public/app/components/job_icon.tsx b/x-pack/plugins/transform/public/app/components/job_icon.tsx index 09db5a1c52377..12a5687156611 100644 --- a/x-pack/plugins/transform/public/app/components/job_icon.tsx +++ b/x-pack/plugins/transform/public/app/components/job_icon.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { AuditMessageBase } from '../../../common/types/messages'; diff --git a/x-pack/plugins/transform/public/app/components/section_error.tsx b/x-pack/plugins/transform/public/app/components/section_error.tsx deleted file mode 100644 index fc7f6bbc14d72..0000000000000 --- a/x-pack/plugins/transform/public/app/components/section_error.tsx +++ /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 { EuiEmptyPrompt, EuiPageContent_Deprecated as EuiPageContent } from '@elastic/eui'; -import React from 'react'; - -interface Props { - title: React.ReactNode; - error: Error | null; - actions?: JSX.Element; -} - -export const SectionError: React.FunctionComponent = ({ - title, - error, - actions, - ...rest -}) => { - const errorMessage = error?.message ?? JSON.stringify(error, null, 2); - - return ( - - {title}} - body={ -

-

{errorMessage}
- {actions ? actions : null} -

- } - /> -
- ); -}; diff --git a/x-pack/plugins/transform/public/app/components/section_loading.tsx b/x-pack/plugins/transform/public/app/components/section_loading.tsx deleted file mode 100644 index c1548ad960bb0..0000000000000 --- a/x-pack/plugins/transform/public/app/components/section_loading.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { - EuiEmptyPrompt, - EuiLoadingSpinner, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiTextColor, -} from '@elastic/eui'; - -interface Props { - inline?: boolean; - children: React.ReactNode; - [key: string]: any; -} - -export const SectionLoading: React.FunctionComponent = ({ inline, children, ...rest }) => { - if (inline) { - return ( - - - - - - - {children} - - - - ); - } - - return ( - } - body={{children}} - data-test-subj="sectionLoading" - /> - ); -}; diff --git a/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx b/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx index 6409a8fcf3b45..8b2f154e8124e 100644 --- a/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx +++ b/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiButtonEmpty, @@ -19,41 +19,45 @@ import { import { i18n } from '@kbn/i18n'; -import { CoreStart } from '@kbn/core/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { useAppDependencies } from '../app_dependencies'; const MAX_SIMPLE_MESSAGE_LENGTH = 140; -// Because of the use of `toMountPoint`, `useKibanaContext` doesn't work via `useAppDependencies`. -// That's why we need to pass in `overlays` as a prop cannot get it via context. interface ToastNotificationTextProps { - overlays: CoreStart['overlays']; - theme: CoreStart['theme']; text: any; previewTextLength?: number; + inline?: boolean; + forceModal?: boolean; } export const ToastNotificationText: FC = ({ - overlays, text, - theme, previewTextLength, + inline = false, + forceModal = false, }) => { - if (typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) { + const { overlays, theme, i18n: i18nStart } = useAppDependencies(); + + if (!forceModal && typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) { return text; } if ( + !forceModal && typeof text === 'object' && + text !== null && typeof text.message === 'string' && text.message.length <= MAX_SIMPLE_MESSAGE_LENGTH ) { return text.message; } - const unformattedText = text.message ? text.message : text; - const formattedText = typeof unformattedText === 'object' ? JSON.stringify(text, null, 2) : text; + const unformattedText = + typeof text === 'object' && text !== null && text.message ? text.message : text; + const formattedText = + typeof unformattedText === 'object' ? JSON.stringify(text, null, 2) : unformattedText; const textLength = previewTextLength ?? 140; const previewText = `${formattedText.substring(0, textLength)}${ formattedText.length > textLength ? ' ...' : '' @@ -83,15 +87,19 @@ export const ToastNotificationText: FC = ({ , - { theme$: theme.theme$ } + { theme, i18n: i18nStart } ) ); }; return ( <> -
{previewText}
- + {!inline &&
{previewText}
} + {i18n.translate('xpack.transform.toastText.openModalButtonText', { defaultMessage: 'View details', })} diff --git a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts deleted file mode 100644 index 2e0407b0ac974..0000000000000 --- a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IHttpFetchError } from '@kbn/core-http-browser'; - -import { KBN_FIELD_TYPES } from '@kbn/field-types'; -import { DEFAULT_SAMPLER_SHARD_SIZE } from '@kbn/ml-agg-utils'; - -import type { TransformId } from '../../../../common/types/transform'; -import type { FieldHistogramsResponseSchema } from '../../../../common/api_schemas/field_histograms'; -import type { GetTransformsAuditMessagesResponseSchema } from '../../../../common/api_schemas/audit_messages'; -import type { - DeleteTransformsRequestSchema, - DeleteTransformsResponseSchema, -} from '../../../../common/api_schemas/delete_transforms'; -import type { - StartTransformsRequestSchema, - StartTransformsResponseSchema, -} from '../../../../common/api_schemas/start_transforms'; -import type { - StopTransformsRequestSchema, - StopTransformsResponseSchema, -} from '../../../../common/api_schemas/stop_transforms'; -import type { - GetTransformsResponseSchema, - PostTransformsPreviewRequestSchema, - PostTransformsPreviewResponseSchema, - PutTransformsRequestSchema, - PutTransformsResponseSchema, -} from '../../../../common/api_schemas/transforms'; -import type { GetTransformsStatsResponseSchema } from '../../../../common/api_schemas/transforms_stats'; -import type { - PostTransformsUpdateRequestSchema, - PostTransformsUpdateResponseSchema, -} from '../../../../common/api_schemas/update_transforms'; - -import type { EsIndex } from '../../../../common/types/es_index'; - -import type { SavedSearchQuery } from '../use_search_items'; - -export interface FieldHistogramRequestConfig { - fieldName: string; - type?: KBN_FIELD_TYPES; -} - -const apiFactory = () => ({ - async getTransform( - transformId: TransformId - ): Promise { - return Promise.resolve({ count: 0, transforms: [] }); - }, - async getTransforms(): Promise { - return Promise.resolve({ count: 0, transforms: [] }); - }, - async getTransformStats( - transformId: TransformId - ): Promise { - return Promise.resolve({ count: 0, transforms: [] }); - }, - async getTransformsStats(): Promise { - return Promise.resolve({ count: 0, transforms: [] }); - }, - async createTransform( - transformId: TransformId, - transformConfig: PutTransformsRequestSchema - ): Promise { - return Promise.resolve({ transformsCreated: [], errors: [] }); - }, - async updateTransform( - transformId: TransformId, - transformConfig: PostTransformsUpdateRequestSchema - ): Promise { - return Promise.resolve({ - id: 'the-test-id', - source: { index: ['the-index-name'], query: { match_all: {} } }, - dest: { index: 'user-the-destination-index-name' }, - frequency: '10m', - pivot: { - group_by: { the_group: { terms: { field: 'the-group-by-field' } } }, - aggregations: { the_agg: { value_count: { field: 'the-agg-field' } } }, - }, - description: 'the-description', - settings: { docs_per_second: null }, - version: '8.0.0', - create_time: 1598860879097, - }); - }, - async deleteTransforms( - reqBody: DeleteTransformsRequestSchema - ): Promise { - return Promise.resolve({}); - }, - async getTransformsPreview( - obj: PostTransformsPreviewRequestSchema - ): Promise { - return Promise.resolve({ - generated_dest_index: { - mappings: { - _meta: { - _transform: { - transform: 'the-transform', - version: { create: 'the-version' }, - creation_date_in_millis: 0, - }, - created_by: 'mock', - }, - properties: {}, - }, - settings: { index: { number_of_shards: '1', auto_expand_replicas: '0-1' } }, - aliases: {}, - }, - preview: [], - }); - }, - async startTransforms( - reqBody: StartTransformsRequestSchema - ): Promise { - return Promise.resolve({}); - }, - async stopTransforms( - transformsInfo: StopTransformsRequestSchema - ): Promise { - return Promise.resolve({}); - }, - async getTransformAuditMessages( - transformId: TransformId - ): Promise { - return Promise.resolve({ messages: [], total: 0 }); - }, - - async getEsIndices(): Promise { - return Promise.resolve([]); - }, - async getHistogramsForFields( - dataViewTitle: string, - fields: FieldHistogramRequestConfig[], - query: string | SavedSearchQuery, - samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE - ): Promise { - return Promise.resolve([]); - }, -}); - -export const useApi = () => { - return apiFactory(); -}; diff --git a/x-pack/plugins/transform/public/app/hooks/index.ts b/x-pack/plugins/transform/public/app/hooks/index.ts index f6a4c72b39a44..749706f97cd70 100644 --- a/x-pack/plugins/transform/public/app/hooks/index.ts +++ b/x-pack/plugins/transform/public/app/hooks/index.ts @@ -5,10 +5,23 @@ * 2.0. */ -export { useApi } from './use_api'; +export { useCreateTransform } from './use_create_transform'; +export { useDocumentationLinks } from './use_documentation_links'; +export { useGetDataViewTitles } from './use_get_data_view_titles'; +export { useGetEsIndices } from './use_get_es_indices'; +export { useGetEsIngestPipelines } from './use_get_es_ingest_pipelines'; +export { useGetTransformAuditMessages } from './use_get_transform_audit_messages'; +export { useGetTransform } from './use_get_transform'; +export { useGetTransformNodes } from './use_get_transform_nodes'; export { useGetTransforms } from './use_get_transforms'; +export { useGetTransformsPreview } from './use_get_transforms_preview'; +export { useGetTransformStats } from './use_get_transform_stats'; export { useDeleteTransforms, useDeleteIndexAndTargetIndex } from './use_delete_transform'; +export { useRefreshTransformList } from './use_refresh_transform_list'; export { useResetTransforms } from './use_reset_transform'; +export { useSearchItems } from './use_search_items'; export { useScheduleNowTransforms } from './use_schedule_now_transform'; export { useStartTransforms } from './use_start_transform'; export { useStopTransforms } from './use_stop_transform'; +export { useTransformCapabilities } from './use_transform_capabilities'; +export { useUpdateTransform } from './use_update_transform'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts deleted file mode 100644 index 066977fb841df..0000000000000 --- a/x-pack/plugins/transform/public/app/hooks/use_api.ts +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo } from 'react'; - -import type { IHttpFetchError } from '@kbn/core-http-browser'; - -import { KBN_FIELD_TYPES } from '@kbn/field-types'; -import { DEFAULT_SAMPLER_SHARD_SIZE } from '@kbn/ml-agg-utils'; - -import { - ReauthorizeTransformsRequestSchema, - ReauthorizeTransformsResponseSchema, -} from '../../../common/api_schemas/reauthorize_transforms'; -import type { GetTransformsAuditMessagesResponseSchema } from '../../../common/api_schemas/audit_messages'; -import type { - DeleteTransformsRequestSchema, - DeleteTransformsResponseSchema, -} from '../../../common/api_schemas/delete_transforms'; -import type { - FieldHistogramsRequestSchema, - FieldHistogramsResponseSchema, -} from '../../../common/api_schemas/field_histograms'; -import type { - ResetTransformsRequestSchema, - ResetTransformsResponseSchema, -} from '../../../common/api_schemas/reset_transforms'; -import type { - StartTransformsRequestSchema, - StartTransformsResponseSchema, -} from '../../../common/api_schemas/start_transforms'; -import type { - StopTransformsRequestSchema, - StopTransformsResponseSchema, -} from '../../../common/api_schemas/stop_transforms'; -import type { - ScheduleNowTransformsRequestSchema, - ScheduleNowTransformsResponseSchema, -} from '../../../common/api_schemas/schedule_now_transforms'; -import type { - GetTransformNodesResponseSchema, - GetTransformsResponseSchema, - PostTransformsPreviewRequestSchema, - PostTransformsPreviewResponseSchema, - PutTransformsRequestSchema, - PutTransformsResponseSchema, -} from '../../../common/api_schemas/transforms'; -import type { - PostTransformsUpdateRequestSchema, - PostTransformsUpdateResponseSchema, -} from '../../../common/api_schemas/update_transforms'; -import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats'; -import type { TransformId } from '../../../common/types/transform'; -import { addInternalBasePath } from '../../../common/constants'; -import type { EsIndex } from '../../../common/types/es_index'; -import type { EsIngestPipeline } from '../../../common/types/es_ingest_pipeline'; - -import { useAppDependencies } from '../app_dependencies'; - -import type { SavedSearchQuery } from './use_search_items'; - -export interface FieldHistogramRequestConfig { - fieldName: string; - type?: KBN_FIELD_TYPES; -} - -interface FetchOptions { - asSystemRequest?: boolean; -} - -export const useApi = () => { - const { http } = useAppDependencies(); - - return useMemo( - () => ({ - async getTransformNodes(): Promise { - try { - return await http.get(addInternalBasePath(`transforms/_nodes`), { version: '1' }); - } catch (e) { - return e; - } - }, - async getTransform( - transformId: TransformId - ): Promise { - try { - return await http.get(addInternalBasePath(`transforms/${transformId}`), { - version: '1', - }); - } catch (e) { - return e; - } - }, - async getTransforms( - fetchOptions: FetchOptions = {} - ): Promise { - try { - return await http.get(addInternalBasePath(`transforms`), { - ...fetchOptions, - version: '1', - }); - } catch (e) { - return e; - } - }, - async getTransformStats( - transformId: TransformId - ): Promise { - try { - return await http.get(addInternalBasePath(`transforms/${transformId}/_stats`), { - version: '1', - }); - } catch (e) { - return e; - } - }, - async getTransformsStats( - fetchOptions: FetchOptions = {} - ): Promise { - try { - return await http.get(addInternalBasePath(`transforms/_stats`), { - ...fetchOptions, - version: '1', - }); - } catch (e) { - return e; - } - }, - async createTransform( - transformId: TransformId, - transformConfig: PutTransformsRequestSchema - ): Promise { - try { - return await http.put(addInternalBasePath(`transforms/${transformId}`), { - body: JSON.stringify(transformConfig), - version: '1', - }); - } catch (e) { - return e; - } - }, - async updateTransform( - transformId: TransformId, - transformConfig: PostTransformsUpdateRequestSchema - ): Promise { - try { - return await http.post(addInternalBasePath(`transforms/${transformId}/_update`), { - body: JSON.stringify(transformConfig), - version: '1', - }); - } catch (e) { - return e; - } - }, - async deleteTransforms( - reqBody: DeleteTransformsRequestSchema - ): Promise { - try { - return await http.post(addInternalBasePath(`delete_transforms`), { - body: JSON.stringify(reqBody), - version: '1', - }); - } catch (e) { - return e; - } - }, - async getTransformsPreview( - obj: PostTransformsPreviewRequestSchema - ): Promise { - try { - return await http.post(addInternalBasePath(`transforms/_preview`), { - body: JSON.stringify(obj), - version: '1', - }); - } catch (e) { - return e; - } - }, - async reauthorizeTransforms( - reqBody: ReauthorizeTransformsRequestSchema - ): Promise { - try { - return await http.post(addInternalBasePath(`reauthorize_transforms`), { - body: JSON.stringify(reqBody), - version: '1', - }); - } catch (e) { - return e; - } - }, - - async resetTransforms( - reqBody: ResetTransformsRequestSchema - ): Promise { - try { - return await http.post(addInternalBasePath(`reset_transforms`), { - body: JSON.stringify(reqBody), - version: '1', - }); - } catch (e) { - return e; - } - }, - async startTransforms( - reqBody: StartTransformsRequestSchema - ): Promise { - try { - return await http.post(addInternalBasePath(`start_transforms`), { - body: JSON.stringify(reqBody), - version: '1', - }); - } catch (e) { - return e; - } - }, - async stopTransforms( - transformsInfo: StopTransformsRequestSchema - ): Promise { - try { - return await http.post(addInternalBasePath(`stop_transforms`), { - body: JSON.stringify(transformsInfo), - version: '1', - }); - } catch (e) { - return e; - } - }, - async scheduleNowTransforms( - transformsInfo: ScheduleNowTransformsRequestSchema - ): Promise { - try { - return await http.post(addInternalBasePath(`schedule_now_transforms`), { - body: JSON.stringify(transformsInfo), - version: '1', - }); - } catch (e) { - return e; - } - }, - async getTransformAuditMessages( - transformId: TransformId, - sortField: string, - sortDirection: 'asc' | 'desc' - ): Promise< - { messages: GetTransformsAuditMessagesResponseSchema; total: number } | IHttpFetchError - > { - try { - return await http.get(addInternalBasePath(`transforms/${transformId}/messages`), { - query: { - sortField, - sortDirection, - }, - version: '1', - }); - } catch (e) { - return e; - } - }, - async getEsIndices(): Promise { - try { - return await http.get(`/api/index_management/indices`, { version: '1' }); - } catch (e) { - return e; - } - }, - async getEsIngestPipelines(): Promise { - try { - return await http.get('/api/ingest_pipelines', { version: '1' }); - } catch (e) { - return e; - } - }, - async getHistogramsForFields( - dataViewTitle: string, - fields: FieldHistogramRequestConfig[], - query: string | SavedSearchQuery, - runtimeMappings?: FieldHistogramsRequestSchema['runtimeMappings'], - samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE - ): Promise { - try { - return await http.post(addInternalBasePath(`field_histograms/${dataViewTitle}`), { - body: JSON.stringify({ - query, - fields, - samplerShardSize, - ...(runtimeMappings !== undefined ? { runtimeMappings } : {}), - }), - version: '1', - }); - } catch (e) { - return e; - } - }, - }), - [http] - ); -}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_create_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_create_transform.tsx new file mode 100644 index 0000000000000..272e815c42eb4 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_create_transform.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useMutation } from '@tanstack/react-query'; + +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '@kbn/react-kibana-mount'; + +import type { + PutTransformsRequestSchema, + PutTransformsResponseSchema, +} from '../../../common/api_schemas/transforms'; +import { addInternalBasePath } from '../../../common/constants'; +import type { TransformId } from '../../../common/types/transform'; +import { getErrorMessage } from '../../../common/utils/errors'; + +import { useAppDependencies, useToastNotifications } from '../app_dependencies'; +import { ToastNotificationText } from '../components'; + +import { useRefreshTransformList } from './use_refresh_transform_list'; + +interface CreateTransformArgs { + transformId: TransformId; + transformConfig: PutTransformsRequestSchema; +} + +export const useCreateTransform = () => { + const { http, i18n: i18nStart, theme } = useAppDependencies(); + const refreshTransformList = useRefreshTransformList(); + const toastNotifications = useToastNotifications(); + + function errorToast(error: unknown, { transformId }: CreateTransformArgs) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepCreateForm.createTransformErrorMessage', { + defaultMessage: 'An error occurred creating the transform {transformId}:', + values: { transformId }, + }), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }); + } + + const mutation = useMutation({ + mutationFn: ({ transformId, transformConfig }: CreateTransformArgs) => { + return http.put( + addInternalBasePath(`transforms/${transformId}`), + { + body: JSON.stringify(transformConfig), + version: '1', + } + ); + }, + onError: errorToast, + onSuccess: (resp, options) => { + if (resp.errors.length > 0) { + errorToast(resp.errors.length === 1 ? resp.errors[0] : resp.errors, options); + } + + refreshTransformList(); + }, + }); + + return mutation.mutate; +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_data_search.ts b/x-pack/plugins/transform/public/app/hooks/use_data_search.ts index af4bb440f9e24..f691cb7a0d8b6 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_data_search.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_data_search.ts @@ -5,37 +5,37 @@ * 2.0. */ -import { useCallback } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { lastValueFrom } from 'rxjs'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + import type { IKibanaSearchRequest } from '@kbn/data-plugin/common'; +import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; + import { useAppDependencies } from '../app_dependencies'; -export const useDataSearch = () => { +export const useDataSearch = ( + esSearchRequestParams: IKibanaSearchRequest['params'], + enabled?: boolean +) => { const { data } = useAppDependencies(); - return useCallback( - async (esSearchRequestParams: IKibanaSearchRequest['params'], abortSignal?: AbortSignal) => { - try { - const { rawResponse: resp } = await lastValueFrom( - data.search.search( - { - params: esSearchRequestParams, - }, - { abortSignal } - ) - ); - - return resp; - } catch (error) { - if (error.name === 'AbortError') { - // ignore abort errors - } else { - return error; - } - } + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.DATA_SEARCH, esSearchRequestParams], + async ({ signal }) => { + const { rawResponse: resp } = await lastValueFrom( + data.search.search( + { + params: esSearchRequestParams, + }, + { abortSignal: signal } + ) + ); + + return resp; }, - [data] + { enabled } ); }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts b/x-pack/plugins/transform/public/app/hooks/use_data_view_exists.ts new file mode 100644 index 0000000000000..d74fa9c909a5d --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_data_view_exists.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 { useQuery } from '@tanstack/react-query'; + +import type { ErrorType } from '@kbn/ml-error-utils'; + +import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; + +import { useAppDependencies } from '../app_dependencies'; +import type { TransformListRow } from '../common'; + +export const useDataViewExists = (items: TransformListRow[]) => { + const { + data: { dataViews: dataViewsContract }, + } = useAppDependencies(); + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.DATA_VIEW_EXISTS, items], + async () => { + if (items.length !== 1) { + return false; + } + const config = items[0].config; + const indexName = Array.isArray(config.dest.index) ? config.dest.index[0] : config.dest.index; + + if (indexName === undefined) { + return false; + } + + return (await dataViewsContract.find(indexName)).some(({ title }) => title === indexName); + } + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx index 25318fc9e2903..a7ed779c47cc7 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -6,34 +6,40 @@ */ import React, { useCallback, useEffect, useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; + import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { extractErrorMessage } from '@kbn/ml-error-utils'; + +import { addInternalBasePath } from '../../../common/constants'; import type { - DeleteTransformStatus, DeleteTransformsRequestSchema, + DeleteTransformsResponseSchema, } from '../../../common/api_schemas/delete_transforms'; -import { isDeleteTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; import { getErrorMessage } from '../../../common/utils/errors'; + import { useAppDependencies, useToastNotifications } from '../app_dependencies'; -import { REFRESH_TRANSFORM_LIST_STATE, refreshTransformList$, TransformListRow } from '../common'; +import { type TransformListRow } from '../common'; import { ToastNotificationText } from '../components'; -import { useApi } from './use_api'; -import { indexService } from '../services/es_index_service'; + +import { useTransformCapabilities } from './use_transform_capabilities'; +import { useDataViewExists } from './use_data_view_exists'; +import { useRefreshTransformList } from './use_refresh_transform_list'; export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { const { - http, - data: { dataViews: dataViewsContract }, application: { capabilities }, } = useAppDependencies(); const toastNotifications = useToastNotifications(); + const { canDeleteIndex: userCanDeleteIndex } = useTransformCapabilities(); + + const userCanDeleteDataView = + capabilities.savedObjectsManagement?.delete === true || + capabilities.indexPatterns?.save === true; const [deleteDestIndex, setDeleteDestIndex] = useState(true); - const [deleteDataView, setDeleteDataView] = useState(true); - const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false); - const [dataViewExists, setDataViewExists] = useState(false); - const [userCanDeleteDataView, setUserCanDeleteDataView] = useState(false); + const [deleteDataView, setDeleteDataView] = useState(userCanDeleteDataView); const toggleDeleteIndex = useCallback( () => setDeleteDestIndex(!deleteDestIndex), @@ -43,67 +49,31 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { () => setDeleteDataView(!deleteDataView), [deleteDataView] ); - const checkDataViewExists = useCallback( - async (indexName: string) => { - try { - const dvExists = await indexService.dataViewExists(dataViewsContract, indexName); - setDataViewExists(dvExists); - } catch (e) { - const error = extractErrorMessage(e); - toastNotifications.addDanger( - i18n.translate( - 'xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage', - { - defaultMessage: 'An error occurred checking if data view {dataView} exists: {error}', - values: { dataView: indexName, error }, - } - ) - ); - } - }, - [dataViewsContract, toastNotifications] - ); + const { error: dataViewExistsError, data: dataViewExists = items.length !== 1 } = + useDataViewExists(items); + + useEffect(() => { + if (dataViewExistsError !== null && items.length === 1) { + const config = items[0].config; + const indexName = Array.isArray(config.dest.index) ? config.dest.index[0] : config.dest.index; - const checkUserIndexPermission = useCallback(async () => { - try { - const userCanDelete = await indexService.canDeleteIndex(http); - if (userCanDelete) { - setUserCanDeleteIndex(true); - } - const canDeleteDataView = - capabilities.savedObjectsManagement.delete === true || - capabilities.indexPatterns.save === true; - setUserCanDeleteDataView(canDeleteDataView); - if (canDeleteDataView === false) { - setDeleteDataView(false); - } - } catch (e) { toastNotifications.addDanger( i18n.translate( - 'xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage', + 'xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage', { - defaultMessage: 'An error occurred checking if user can delete destination index', + defaultMessage: 'An error occurred checking if data view {dataView} exists: {error}', + values: { + dataView: indexName, + error: extractErrorMessage(dataViewExistsError), + }, } ) ); } - }, [http, toastNotifications, capabilities]); - - useEffect(() => { - checkUserIndexPermission(); - - // if user only deleting one transform - if (items.length === 1) { - const config = items[0].config; - const destinationIndex = Array.isArray(config.dest.index) - ? config.dest.index[0] - : config.dest.index; - checkDataViewExists(destinationIndex); - } else { - setDataViewExists(true); - } - }, [checkDataViewExists, checkUserIndexPermission, items]); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataViewExistsError]); return { userCanDeleteIndex, @@ -116,183 +86,87 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { }; }; -type SuccessCountField = keyof Omit; - export const useDeleteTransforms = () => { - const { overlays, theme } = useAppDependencies(); + const { http, i18n: i18nStart, theme } = useAppDependencies(); + const refreshTransformList = useRefreshTransformList(); const toastNotifications = useToastNotifications(); - const api = useApi(); - return async (reqBody: DeleteTransformsRequestSchema) => { - const results = await api.deleteTransforms(reqBody); - - if (!isDeleteTransformsResponseSchema(results)) { + const mutation = useMutation({ + mutationFn: (reqBody: DeleteTransformsRequestSchema) => + http.post(addInternalBasePath('delete_transforms'), { + body: JSON.stringify(reqBody), + version: '1', + }), + onError: (error) => toastNotifications.addDanger({ title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', { defaultMessage: 'An error occurred calling the API endpoint to delete transforms.', }), text: toMountPoint( - , - { theme$: theme.theme$ } + , + { theme, i18n: i18nStart } ), - }); - return; - } - - const isBulk = Object.keys(results).length > 1; - const successCount: Record = { - transformDeleted: 0, - destIndexDeleted: 0, - destDataViewDeleted: 0, - }; - for (const transformId in results) { - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (results.hasOwnProperty(transformId)) { - const status = results[transformId]; - const destinationIndex = status.destinationIndex; - - // if we are only deleting one transform, show the success toast messages - if (!isBulk && status.transformDeleted) { - if (status.transformDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', { - defaultMessage: 'Request to delete transform {transformId} acknowledged.', + }), + onSuccess: (results) => { + for (const transformId in results) { + // hasOwnProperty check to ensure only properties on object itself, and not its prototypes + if (results.hasOwnProperty(transformId)) { + const status = results[transformId]; + const destinationIndex = status.destinationIndex; + + if (status.transformDeleted?.error) { + const error = status.transformDeleted.error.reason; + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', { + defaultMessage: 'An error occurred deleting the transform {transformId}', values: { transformId }, - }) - ); + }), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }); } - if (status.destIndexDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage', + + if (status.destIndexDeleted?.error) { + const error = status.destIndexDeleted.error.reason; + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage', { - defaultMessage: - 'Request to delete destination index {destinationIndex} acknowledged.', + defaultMessage: 'An error occurred deleting destination index {destinationIndex}', values: { destinationIndex }, } - ) - ); + ), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }); } - if (status.destDataViewDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithDataViewSuccessMessage', + + if (status.destDataViewDeleted?.error) { + const error = status.destDataViewDeleted.error.reason; + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithDataViewErrorMessage', { - defaultMessage: 'Request to delete data view {destinationIndex} acknowledged.', + defaultMessage: 'An error occurred deleting data view {destinationIndex}', values: { destinationIndex }, } - ) - ); + ), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }); } - } else { - (Object.keys(successCount) as SuccessCountField[]).forEach((key) => { - if (status[key]?.success) { - successCount[key] = successCount[key] + 1; - } - }); - } - if (status.transformDeleted?.error) { - const error = status.transformDeleted.error.reason; - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', { - defaultMessage: 'An error occurred deleting the transform {transformId}', - values: { transformId }, - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - } - - if (status.destIndexDeleted?.error) { - const error = status.destIndexDeleted.error.reason; - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage', - { - defaultMessage: 'An error occurred deleting destination index {destinationIndex}', - values: { destinationIndex }, - } - ), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - } - - if (status.destDataViewDeleted?.error) { - const error = status.destDataViewDeleted.error.reason; - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithDataViewErrorMessage', - { - defaultMessage: 'An error occurred deleting data view {destinationIndex}', - values: { destinationIndex }, - } - ), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); } } - } - // if we are deleting multiple transforms, combine the success messages - if (isBulk) { - if (successCount.transformDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', { - defaultMessage: - 'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.', - values: { count: successCount.transformDeleted }, - }) - ); - } - - if (successCount.destIndexDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', { - defaultMessage: - 'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.', - values: { count: successCount.destIndexDeleted }, - }) - ); - } - if (successCount.destDataViewDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.bulkDeleteDestDataViewSuccessMessage', { - defaultMessage: - 'Successfully deleted {count} destination data {count, plural, one {view} other {views}}.', - values: { count: successCount.destDataViewDeleted }, - }) - ); - } - } + refreshTransformList(); + }, + }); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - }; + return mutation.mutate; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_data_view_titles.ts b/x-pack/plugins/transform/public/app/hooks/use_get_data_view_titles.ts new file mode 100644 index 0000000000000..449ac30b9f513 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_data_view_titles.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; + +import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useGetDataViewTitles = () => { + const { data } = useAppDependencies(); + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_DATA_VIEW_TITLES], + () => data.dataViews.getTitles() + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_es_indices.ts b/x-pack/plugins/transform/public/app/hooks/use_get_es_indices.ts new file mode 100644 index 0000000000000..7da50c155d1be --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_es_indices.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; + +import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; +import type { EsIndex } from '../../../common/types/es_index'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useGetEsIndices = () => { + const { http } = useAppDependencies(); + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_ES_INDICES], + ({ signal }) => + http.get('/api/index_management/indices', { + version: '1', + signal, + }) + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_es_ingest_pipelines.ts b/x-pack/plugins/transform/public/app/hooks/use_get_es_ingest_pipelines.ts new file mode 100644 index 0000000000000..3f9784c64b652 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_es_ingest_pipelines.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; + +import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; +import type { EsIngestPipeline } from '../../../common/types/es_ingest_pipeline'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useGetEsIngestPipelines = () => { + const { http } = useAppDependencies(); + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_ES_INGEST_PIPELINES], + ({ signal }) => + http.get('/api/ingest_pipelines', { + version: '1', + signal, + }) + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_histograms_for_fields.ts b/x-pack/plugins/transform/public/app/hooks/use_get_histograms_for_fields.ts new file mode 100644 index 0000000000000..63ce23dba6214 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_histograms_for_fields.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; +import { DEFAULT_SAMPLER_SHARD_SIZE } from '@kbn/ml-agg-utils'; + +import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; +import type { + FieldHistogramsRequestSchema, + FieldHistogramsResponseSchema, +} from '../../../common/api_schemas/field_histograms'; + +import { useAppDependencies } from '../app_dependencies'; + +import type { SavedSearchQuery } from './use_search_items'; + +export interface FieldHistogramRequestConfig { + fieldName: string; + type?: KBN_FIELD_TYPES; +} + +export const useGetHistogramsForFields = ( + dataViewTitle: string, + fields: FieldHistogramRequestConfig[], + query: string | SavedSearchQuery, + runtimeMappings?: FieldHistogramsRequestSchema['runtimeMappings'], + enabled?: boolean, + samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE +) => { + const { http } = useAppDependencies(); + + return useQuery( + [ + TRANSFORM_REACT_QUERY_KEYS.GET_HISTOGRAMS_FOR_FIELDS, + { + dataViewTitle, + fields, + query, + runtimeMappings, + samplerShardSize, + }, + ], + ({ signal }) => + http.post( + addInternalBasePath(`field_histograms/${dataViewTitle}`), + { + body: JSON.stringify({ + query, + fields, + samplerShardSize, + ...(runtimeMappings !== undefined ? { runtimeMappings } : {}), + }), + version: '1', + signal, + } + ), + { enabled } + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_get_transform.tsx new file mode 100644 index 0000000000000..b9e3d977c71bd --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transform.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; + +import type { GetTransformsResponseSchema } from '../../../common/api_schemas/transforms'; +import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; +import type { TransformId } from '../../../common/types/transform'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useGetTransform = (transformId: TransformId, enabled?: boolean) => { + const { http } = useAppDependencies(); + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM, transformId], + ({ signal }) => + http.get(addInternalBasePath(`transforms/${transformId}`), { + version: '1', + signal, + }), + { enabled } + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transform_audit_messages.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transform_audit_messages.ts new file mode 100644 index 0000000000000..3f7559a251275 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transform_audit_messages.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; + +import type { GetTransformsAuditMessagesResponseSchema } from '../../../common/api_schemas/audit_messages'; +import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; +import { TransformMessage } from '../../../common/types/messages'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useGetTransformAuditMessages = ( + transformId: string, + sortField: keyof TransformMessage, + sortDirection: 'asc' | 'desc' +) => { + const { http } = useAppDependencies(); + + const query = { sortField, sortDirection }; + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_AUDIT_MESSAGES, transformId, query], + ({ signal }) => + http.get( + addInternalBasePath(`transforms/${transformId}/messages`), + { + query, + version: '1', + signal, + } + ) + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transform_nodes.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transform_nodes.ts new file mode 100644 index 0000000000000..2d3d7cfa9defd --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transform_nodes.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; + +import type { GetTransformNodesResponseSchema } from '../../../common/api_schemas/transforms'; +import { + addInternalBasePath, + DEFAULT_REFRESH_INTERVAL_MS, + TRANSFORM_REACT_QUERY_KEYS, +} from '../../../common/constants'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useGetTransformNodes = () => { + const { http } = useAppDependencies(); + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_NODES], + async ({ signal }) => { + const transformNodes = await http.get( + addInternalBasePath('transforms/_nodes'), + { + version: '1', + signal, + } + ); + + return transformNodes.count; + }, + { + refetchInterval: DEFAULT_REFRESH_INTERVAL_MS, + } + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transform_stats.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transform_stats.ts new file mode 100644 index 0000000000000..d2b9d32f25853 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transform_stats.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; + +import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats'; +import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; +import type { TransformId } from '../../../common/types/transform'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useGetTransformStats = ( + transformId: TransformId, + enabled?: boolean, + refetchInterval?: number | false +) => { + const { http } = useAppDependencies(); + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_STATS, transformId], + ({ signal }) => + http.get( + addInternalBasePath(`transforms/${transformId}/_stats`), + { + version: '1', + signal, + } + ), + { enabled, refetchInterval } + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts index 55e3bc40360cf..f74f7a5774ded 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts @@ -5,75 +5,64 @@ * 2.0. */ +import { useQuery } from '@tanstack/react-query'; + import type { IHttpFetchError } from '@kbn/core-http-browser'; import { isDefined } from '@kbn/ml-is-defined'; -import { - isGetTransformNodesResponseSchema, - isGetTransformsResponseSchema, - isGetTransformsStatsResponseSchema, -} from '../../../common/api_schemas/type_guards'; -import { TRANSFORM_MODE } from '../../../common/constants'; -import { isTransformStats } from '../../../common/types/transform_stats'; +import type { GetTransformsResponseSchema } from '../../../common/api_schemas/transforms'; +import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats'; import { - type TransformListRow, - refreshTransformList$, - REFRESH_TRANSFORM_LIST_STATE, -} from '../common'; + addInternalBasePath, + DEFAULT_REFRESH_INTERVAL_MS, + TRANSFORM_REACT_QUERY_KEYS, + TRANSFORM_MODE, +} from '../../../common/constants'; +import { isTransformStats } from '../../../common/types/transform_stats'; -import { useApi } from './use_api'; +import { type TransformListRow } from '../common'; +import { useAppDependencies } from '../app_dependencies'; import { TRANSFORM_ERROR_TYPE } from '../common/transform'; -export type GetTransforms = (forceRefresh?: boolean) => void; - -export const useGetTransforms = ( - setTransforms: React.Dispatch>, - setTransformNodes: React.Dispatch>, - setErrorMessage: React.Dispatch>, - setTransformIdsWithoutConfig: React.Dispatch>, - setIsInitialized: React.Dispatch>, - blockRefresh: boolean -): GetTransforms => { - const api = useApi(); - - let concurrentLoads = 0; - - const getTransforms = async (forceRefresh = false) => { - if (forceRefresh === true || blockRefresh === false) { - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.LOADING); - concurrentLoads++; - - if (concurrentLoads > 1) { - return; - } - - const fetchOptions = { asSystemRequest: true }; - const transformNodes = await api.getTransformNodes(); - const transformConfigs = await api.getTransforms(fetchOptions); - const transformStats = await api.getTransformsStats(fetchOptions); - - if ( - !isGetTransformsResponseSchema(transformConfigs) || - !isGetTransformsStatsResponseSchema(transformStats) || - !isGetTransformNodesResponseSchema(transformNodes) - ) { - // An error is followed immediately by setting the state to idle. - // This way we're able to treat ERROR as a one-time-event like REFRESH. - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.ERROR); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); - setTransformNodes(0); - setTransforms([]); - - setIsInitialized(true); - - if (!isGetTransformsResponseSchema(transformConfigs)) { - setErrorMessage(transformConfigs); - } else if (!isGetTransformsStatsResponseSchema(transformStats)) { - setErrorMessage(transformStats); +interface UseGetTransformsResponse { + transforms: TransformListRow[]; + transformIds: string[]; + transformIdsWithoutConfig?: string[]; +} + +const getInitialData = (): UseGetTransformsResponse => ({ + transforms: [], + transformIds: [], +}); + +interface UseGetTransformsOptions { + enabled?: boolean; +} + +export const useGetTransforms = ({ enabled }: UseGetTransformsOptions = {}) => { + const { http } = useAppDependencies(); + + const { data = getInitialData(), ...rest } = useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORMS], + async ({ signal }) => { + const update = getInitialData(); + + const transformConfigs = await http.get( + addInternalBasePath('transforms'), + { + version: '1', + asSystemRequest: true, + signal, } - - return; - } + ); + const transformStats = await http.get( + addInternalBasePath(`transforms/_stats`), + { + version: '1', + asSystemRequest: true, + signal, + } + ); // There might be some errors with fetching certain transforms // For example, when task exists and is running but the config is deleted @@ -87,17 +76,12 @@ export const useGetTransforms = ( }) .filter(isDefined); - setTransformIdsWithoutConfig( - danglingTaskIdMatches.length > 0 ? danglingTaskIdMatches : undefined - ); - } else { - setTransformIdsWithoutConfig(undefined); + update.transformIdsWithoutConfig = + danglingTaskIdMatches.length > 0 ? danglingTaskIdMatches : undefined; } - const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => { - const stats = isGetTransformsStatsResponseSchema(transformStats) - ? transformStats.transforms.find((d) => config.id === d.id) - : undefined; + update.transforms = transformConfigs.transforms.reduce((reducedtableRows, config) => { + const stats = transformStats.transforms.find((d) => config.id === d.id); // A newly created transform might not have corresponding stats yet. // If that's the case we just skip the transform and don't add it to the transform list yet. @@ -117,21 +101,15 @@ export const useGetTransforms = ( return reducedtableRows; }, [] as TransformListRow[]); - setTransformNodes(transformNodes.count); - setTransforms(tableRows); - setErrorMessage(undefined); - setIsInitialized(true); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); - - concurrentLoads--; + update.transformIds = update.transforms.map(({ id }) => id); - if (concurrentLoads > 0) { - concurrentLoads = 0; - getTransforms(true); - return; - } + return update; + }, + { + enabled, + refetchInterval: DEFAULT_REFRESH_INTERVAL_MS, } - }; + ); - return getTransforms; + return { data, ...rest }; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transforms_preview.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transforms_preview.ts new file mode 100644 index 0000000000000..ae671912b9267 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transforms_preview.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 { useQuery } from '@tanstack/react-query'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; + +import type { + PostTransformsPreviewRequestSchema, + PostTransformsPreviewResponseSchema, +} from '../../../common/api_schemas/transforms'; +import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useGetTransformsPreview = ( + obj: PostTransformsPreviewRequestSchema, + enabled?: boolean +) => { + const { http } = useAppDependencies(); + + return useQuery( + [TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORMS_PREVIEW, obj], + ({ signal }) => + http.post(addInternalBasePath('transforms/_preview'), { + body: JSON.stringify(obj), + version: '1', + signal, + }), + { enabled } + ); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index 9da9fa8e5e782..ced29e2f8c17c 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import React, { FC } from 'react'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; - +import React, { type FC } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import '@testing-library/jest-dom/extend-expect'; import { render, screen, waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { CoreSetup } from '@kbn/core/public'; import { DataGrid, type UseIndexDataReturnType } from '@kbn/ml-data-grid'; import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; @@ -25,7 +25,6 @@ import { useIndexData } from './use_index_data'; jest.mock('../../shared_imports'); jest.mock('../app_dependencies'); -jest.mock('./use_api'); import { MlSharedContext } from '../__mocks__/shared_context'; @@ -45,13 +44,17 @@ const runtimeMappings: RuntimeMappings = { }, }; +const queryClient = new QueryClient(); + describe('Transform: useIndexData()', () => { test('dataView set triggers loading', async () => { const mlShared = await getMlSharedImports(); const wrapper: FC = ({ children }) => ( - - {children} - + + + {children} + + ); const { result, waitForNextUpdate } = renderHook( @@ -102,11 +105,13 @@ describe('Transform: with useIndexData()', () => { }; render( - - - - - + + + + + + + ); // Act @@ -142,11 +147,13 @@ describe('Transform: with useIndexData()', () => { }; render( - - - - - + + + + + + + ); // Act diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 7b6911636c600..4534552f6b405 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { EuiDataGridColumn } from '@elastic/eui'; @@ -28,10 +28,6 @@ import { } from '@kbn/ml-data-grid'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; -import { - isEsSearchResponse, - isFieldHistogramsResponseSchema, -} from '../../../common/api_schemas/type_guards'; import { hasKeywordDuplicate, isKeywordDuplicate, @@ -44,14 +40,15 @@ import { useToastNotifications, useAppDependencies } from '../app_dependencies'; import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common'; import { SearchItems } from './use_search_items'; -import { useApi } from './use_api'; +import { useGetHistogramsForFields } from './use_get_histograms_for_fields'; import { useDataSearch } from './use_data_search'; export const useIndexData = ( dataView: SearchItems['dataView'], query: TransformConfigQuery, combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'], - timeRangeMs?: TimeRangeMs + timeRangeMs?: TimeRangeMs, + populatedFields?: string[] ): UseIndexDataReturnType => { const { analytics } = useAppDependencies(); @@ -60,13 +57,8 @@ export const useIndexData = ( const loadIndexDataStartTime = useRef(window.performance.now()); const indexPattern = useMemo(() => dataView.getIndexPattern(), [dataView]); - - const api = useApi(); - const dataSearch = useDataSearch(); const toastNotifications = useToastNotifications(); - const [dataViewFields, setDataViewFields] = useState(); - const baseFilterCriteria = buildBaseFilterCriteria( dataView.timeFieldName, timeRangeMs?.from, @@ -85,67 +77,73 @@ export const useIndexData = ( }, }; - useEffect(() => { - if (dataView.timeFieldName !== undefined && timeRangeMs === undefined) { - return; - } - const abortController = new AbortController(); - - // Fetch 500 random documents to determine populated fields. - // This is a workaround to avoid passing potentially thousands of unpopulated fields - // (for example, as part of filebeat/metricbeat/ECS based indices) - // to the data grid component which would significantly slow down the page. - const fetchDataGridSampleDocuments = async function () { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - const esSearchRequest = { - index: indexPattern, - body: { - fields: ['*'], - _source: false, - query: { - function_score: { - query: defaultQuery, - random_score: {}, - }, + // Fetch 500 random documents to determine populated fields. + // This is a workaround to avoid passing potentially thousands of unpopulated fields + // (for example, as part of filebeat/metricbeat/ECS based indices) + // to the data grid component which would significantly slow down the page. + const { + error: dataViewFieldsError, + data: dataViewFieldsData, + isError: dataViewFieldsIsError, + isLoading: dataViewFieldsIsLoading, + } = useDataSearch( + { + index: indexPattern, + body: { + fields: ['*'], + _source: false, + query: { + function_score: { + query: defaultQuery, + random_score: {}, }, - size: 500, }, - }; - - const resp = await dataSearch(esSearchRequest, abortController.signal); - - if (!isEsSearchResponse(resp)) { - setErrorMessage(getErrorMessage(resp)); - setStatus(INDEX_STATUS.ERROR); - return; - } + size: 500, + }, + }, + // Check whether fetching should be enabled + // If populatedFields are not provided, make own request to calculate + !Array.isArray(populatedFields) && + !(dataView.timeFieldName !== undefined && timeRangeMs === undefined) + ); + useEffect(() => { + if (dataViewFieldsIsLoading && !dataViewFieldsIsError) { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + } else if (dataViewFieldsError !== null) { + setErrorMessage(getErrorMessage(dataViewFieldsError)); + setStatus(INDEX_STATUS.ERROR); + } else if ( + !dataViewFieldsIsLoading && + !dataViewFieldsIsError && + dataViewFieldsData !== undefined + ) { const isCrossClusterSearch = indexPattern.includes(':'); - const isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined'); - - const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); - - // Get all field names for each returned doc and flatten it - // to a list of unique field names used across all docs. - const allDataViewFields = getFieldsFromKibanaIndexPattern(dataView); - const populatedFields = [...new Set(docs.map(Object.keys).flat(1))] - .filter((d) => allDataViewFields.includes(d)) - .sort(); + const isMissingFields = dataViewFieldsData.hits.hits.every( + (d) => typeof d.fields === 'undefined' + ); setCcsWarning(isCrossClusterSearch && isMissingFields); setStatus(INDEX_STATUS.LOADED); - setDataViewFields(populatedFields); - }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataViewFieldsData, dataViewFieldsError, dataViewFieldsIsError, dataViewFieldsIsLoading]); + + const dataViewFields = useMemo(() => { + let allPopulatedFields = Array.isArray(populatedFields) ? populatedFields : []; - fetchDataGridSampleDocuments(); + if (populatedFields === undefined && dataViewFieldsData) { + // Get all field names for each returned doc and flatten it + // to a list of unique field names used across all docs. + const docs = dataViewFieldsData.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + allPopulatedFields = [...new Set(docs.map(Object.keys).flat(1))]; + } - return () => { - abortController.abort(); - }; + const allDataViewFields = getFieldsFromKibanaIndexPattern(dataView); + return allPopulatedFields.filter((d) => allDataViewFields.includes(d)).sort(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [timeRangeMs]); + }, [dataViewFieldsData, populatedFields]); const columns: EuiDataGridColumn[] = useMemo(() => { if (typeof dataViewFields === 'undefined') { @@ -199,132 +197,113 @@ export const useIndexData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify([query, timeRangeMs])]); - useEffect(() => { - if (typeof dataViewFields === 'undefined') { - return; - } - const abortController = new AbortController(); + const sort: EsSorting = sortingColumns.reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const { + error: dataGridDataError, + data: dataGridData, + isError: dataGridDataIsError, + isLoading: dataGridDataIsLoading, + } = useDataSearch( + { + index: indexPattern, + body: { + fields: ['*'], + _source: false, + query: isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria, + from: pagination.pageIndex * pagination.pageSize, + size: pagination.pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + ...(isRuntimeMappings(combinedRuntimeMappings) + ? { runtime_mappings: combinedRuntimeMappings } + : {}), + }, + }, + // Check whether fetching should be enabled + dataViewFields !== undefined + ); - const fetchDataGridData = async function () { + useEffect(() => { + if (dataGridDataIsLoading && !dataGridDataIsError) { setErrorMessage(''); setStatus(INDEX_STATUS.LOADING); - - const sort: EsSorting = sortingColumns.reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const esSearchRequest = { - index: indexPattern, - body: { - fields: ['*'], - _source: false, - query: isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria, - from: pagination.pageIndex * pagination.pageSize, - size: pagination.pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - ...(isRuntimeMappings(combinedRuntimeMappings) - ? { runtime_mappings: combinedRuntimeMappings } - : {}), - }, - }; - const resp = await dataSearch(esSearchRequest, abortController.signal); - - if (!isEsSearchResponse(resp)) { - setErrorMessage(getErrorMessage(resp)); - setStatus(INDEX_STATUS.ERROR); - return; - } - + } else if (dataGridDataError !== null) { + setErrorMessage(getErrorMessage(dataGridDataError)); + setStatus(INDEX_STATUS.ERROR); + } else if (!dataGridDataIsLoading && !dataGridDataIsError && dataGridData !== undefined) { const isCrossClusterSearch = indexPattern.includes(':'); - const isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined'); + const isMissingFields = dataGridData.hits.hits.every((d) => typeof d.fields === 'undefined'); - const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + const docs = dataGridData.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); setCcsWarning(isCrossClusterSearch && isMissingFields); setRowCountInfo({ - rowCount: typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total!.value, + rowCount: + typeof dataGridData.hits.total === 'number' + ? dataGridData.hits.total + : dataGridData.hits.total!.value, rowCountRelation: - typeof resp.hits.total === 'number' + typeof dataGridData.hits.total === 'number' ? ('eq' as estypes.SearchTotalHitsRelation) - : resp.hits.total!.relation, + : dataGridData.hits.total!.relation, }); setTableItems(docs); setStatus(INDEX_STATUS.LOADED); - }; - - fetchDataGridData(); - - return () => { - abortController.abort(); - }; - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - indexPattern, + } // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify([ - query, - pagination, - sortingColumns, - dataViewFields, + }, [dataGridDataError, dataGridDataIsError, dataGridDataIsLoading]); + + const allDataViewFieldNames = new Set(dataView.fields.map((f) => f.name)); + const { error: histogramsForFieldsError, data: histogramsForFieldsData } = + useGetHistogramsForFields( + indexPattern, + columns + .filter((cT) => dataGrid.visibleColumns.includes(cT.id)) + .map((cT) => { + // If a column field name has a corresponding keyword field, + // fetch the keyword field instead to be able to do aggregations. + const fieldName = cT.id; + return hasKeywordDuplicate(fieldName, allDataViewFieldNames) + ? { + fieldName: `${fieldName}.keyword`, + type: getFieldType(undefined), + } + : { + fieldName, + type: getFieldType(cT.schema), + }; + }), + isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria, combinedRuntimeMappings, - timeRangeMs, - ]), - ]); + chartsVisible + ); useEffect(() => { - const fetchColumnChartsData = async function () { - const allDataViewFieldNames = new Set(dataView.fields.map((f) => f.name)); - const columnChartsData = await api.getHistogramsForFields( - indexPattern, - columns - .filter((cT) => dataGrid.visibleColumns.includes(cT.id)) - .map((cT) => { - // If a column field name has a corresponding keyword field, - // fetch the keyword field instead to be able to do aggregations. - const fieldName = cT.id; - return hasKeywordDuplicate(fieldName, allDataViewFieldNames) - ? { - fieldName: `${fieldName}.keyword`, - type: getFieldType(undefined), - } - : { - fieldName, - type: getFieldType(cT.schema), - }; - }), - isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria, - combinedRuntimeMappings - ); - - if (!isFieldHistogramsResponseSchema(columnChartsData)) { - showDataGridColumnChartErrorMessageToast(columnChartsData, toastNotifications); - return; - } + if (histogramsForFieldsError !== null) { + showDataGridColumnChartErrorMessageToast(histogramsForFieldsError, toastNotifications); + } + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [histogramsForFieldsError]); + useEffect(() => { + if (histogramsForFieldsData) { setColumnCharts( // revert field names with `.keyword` used to do aggregations to their original column name - columnChartsData.map((d) => ({ + histogramsForFieldsData.map((d) => ({ ...d, ...(isKeywordDuplicate(d.id, allDataViewFieldNames) ? { id: removeKeywordPostfix(d.id) } : {}), })) ); - }; - - if (chartsVisible) { - fetchColumnChartsData(); } // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - chartsVisible, - indexPattern, - // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings, timeRangeMs]), - ]); + }, [histogramsForFieldsData]); const renderCellValue = useRenderCellValue(dataView, pagination, tableItems); diff --git a/x-pack/plugins/transform/public/app/hooks/use_reauthorize_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_reauthorize_transform.tsx index af6018c35cecc..9ecd1b8717243 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_reauthorize_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_reauthorize_transform.tsx @@ -6,31 +6,39 @@ */ import React from 'react'; +import { useMutation } from '@tanstack/react-query'; import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; - -import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms'; -import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { addInternalBasePath } from '../../../common/constants'; import { getErrorMessage } from '../../../common/utils/errors'; +import type { + ReauthorizeTransformsRequestSchema, + ReauthorizeTransformsResponseSchema, +} from '../../../common/api_schemas/reauthorize_transforms'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; -import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { ToastNotificationText } from '../components'; -import { useApi } from './use_api'; +import { useRefreshTransformList } from './use_refresh_transform_list'; export const useReauthorizeTransforms = () => { - const { overlays, theme } = useAppDependencies(); + const { http, i18n: i18nStart, theme } = useAppDependencies(); + const refreshTransformList = useRefreshTransformList(); const toastNotifications = useToastNotifications(); - const api = useApi(); - - return async (transformsInfo: StartTransformsRequestSchema) => { - const results = await api.reauthorizeTransforms(transformsInfo); - if (!isStartTransformsResponseSchema(results)) { + const mutation = useMutation({ + mutationFn: (reqBody: ReauthorizeTransformsRequestSchema) => + http.post( + addInternalBasePath('reauthorize_transforms'), + { + body: JSON.stringify(reqBody), + version: '1', + } + ), + onError: (error) => toastNotifications.addDanger({ title: i18n.translate( 'xpack.transform.stepCreateForm.reauthorizeTransformResponseSchemaErrorMessage', @@ -38,44 +46,37 @@ export const useReauthorizeTransforms = () => { defaultMessage: 'An error occurred calling the reauthorize transforms request.', } ), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - return; - } - - for (const transformId in results) { - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (results.hasOwnProperty(transformId)) { - const result = results[transformId]; - if (result.success === true) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.reauthorizeTransformSuccessMessage', { - defaultMessage: 'Request to reauthorize transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - } else { - toastNotifications.addError(new Error(JSON.stringify(result.error!.caused_by, null, 2)), { - title: i18n.translate( - 'xpack.transform.transformList.reauthorizeTransformErrorMessage', + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }), + onSuccess: (results) => { + for (const transformId in results) { + // hasOwnProperty check to ensure only properties on object itself, and not its prototypes + if (results.hasOwnProperty(transformId)) { + const result = results[transformId]; + if (!result.success) { + toastNotifications.addError( + new Error(JSON.stringify(result.error!.caused_by, null, 2)), { - defaultMessage: 'An error occurred reauthorizing the transform {transformId}', - values: { transformId }, + title: i18n.translate( + 'xpack.transform.transformList.reauthorizeTransformErrorMessage', + { + defaultMessage: 'An error occurred reauthorizing the transform {transformId}', + values: { transformId }, + } + ), + toastMessage: result.error!.reason, } - ), - toastMessage: result.error!.reason, - }); + ); + } } } - } - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - }; + refreshTransformList(); + }, + }); + + return mutation.mutate; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_refresh_transform_list.ts b/x-pack/plugins/transform/public/app/hooks/use_refresh_transform_list.ts new file mode 100644 index 0000000000000..651886ba76f7b --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_refresh_transform_list.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 { useQueryClient } from '@tanstack/react-query'; + +import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants'; + +export const useRefreshTransformList = () => { + const queryClient = useQueryClient(); + + return () => { + queryClient.invalidateQueries([TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_NODES]); + queryClient.invalidateQueries([TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORMS]); + queryClient.invalidateQueries([TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_AUDIT_MESSAGES]); + }; +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx index c33cec3f5b93f..1f415eae1ad20 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx @@ -6,107 +6,72 @@ */ import React from 'react'; +import { useMutation } from '@tanstack/react-query'; + import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; + import type { - ResetTransformStatus, ResetTransformsRequestSchema, + ResetTransformsResponseSchema, } from '../../../common/api_schemas/reset_transforms'; -import { isResetTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; +import { addInternalBasePath } from '../../../common/constants'; import { getErrorMessage } from '../../../common/utils/errors'; + import { useAppDependencies, useToastNotifications } from '../app_dependencies'; -import { REFRESH_TRANSFORM_LIST_STATE, refreshTransformList$ } from '../common'; import { ToastNotificationText } from '../components'; -import { useApi } from './use_api'; -type SuccessCountField = keyof Omit; +import { useRefreshTransformList } from './use_refresh_transform_list'; export const useResetTransforms = () => { - const { overlays, theme } = useAppDependencies(); + const { http, i18n: i18nStart, theme } = useAppDependencies(); + const refreshTransformList = useRefreshTransformList(); const toastNotifications = useToastNotifications(); - const api = useApi(); - - return async (reqBody: ResetTransformsRequestSchema) => { - const results = await api.resetTransforms(reqBody); - if (!isResetTransformsResponseSchema(results)) { + const mutation = useMutation({ + mutationFn: (reqBody: ResetTransformsRequestSchema) => + http.post(addInternalBasePath('reset_transforms'), { + body: JSON.stringify(reqBody), + version: '1', + }), + onError: (error) => toastNotifications.addDanger({ title: i18n.translate('xpack.transform.transformList.resetTransformGenericErrorMessage', { defaultMessage: 'An error occurred calling the API endpoint to reset transforms.', }), text: toMountPoint( - , - { theme$: theme.theme$ } + , + { + theme, + i18n: i18nStart, + } ), - }); - return; - } - - const isBulk = Object.keys(results).length > 1; - const successCount: Record = { - transformReset: 0, - }; - for (const transformId in results) { - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (results.hasOwnProperty(transformId)) { - const status = results[transformId]; + }), + onSuccess: (results) => { + for (const transformId in results) { + // hasOwnProperty check to ensure only properties on object itself, and not its prototypes + if (results.hasOwnProperty(transformId)) { + const status = results[transformId]; - // if we are only resetting one transform, show the success toast messages - if (!isBulk && status.transformReset) { - if (status.transformReset?.success) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.resetTransformSuccessMessage', { - defaultMessage: 'Request to reset transform {transformId} acknowledged.', + if (status.transformReset?.error) { + const error = status.transformReset.error.reason; + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.transformList.resetTransformErrorMessage', { + defaultMessage: 'An error occurred resetting the transform {transformId}', values: { transformId }, - }) - ); + }), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }); } - } else { - (Object.keys(successCount) as SuccessCountField[]).forEach((key) => { - if (status[key]?.success) { - successCount[key] = successCount[key] + 1; - } - }); - } - if (status.transformReset?.error) { - const error = status.transformReset.error.reason; - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.transformList.resetTransformErrorMessage', { - defaultMessage: 'An error occurred resetting the transform {transformId}', - values: { transformId }, - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); } } - } - // if we are deleting multiple transforms, combine the success messages - if (isBulk) { - if (successCount.transformReset > 0) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.bulkResetTransformSuccessMessage', { - defaultMessage: - 'Successfully reset {count} {count, plural, one {transform} other {transforms}}.', - values: { count: successCount.transformReset }, - }) - ); - } - } + refreshTransformList(); + }, + }); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - }; + return mutation.mutate; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_schedule_now_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_schedule_now_transform.tsx index 8454580867391..bb568673a5758 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_schedule_now_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_schedule_now_transform.tsx @@ -6,31 +6,38 @@ */ import React from 'react'; +import { useMutation } from '@tanstack/react-query'; import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '@kbn/react-kibana-mount'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; - -import type { ScheduleNowTransformsRequestSchema } from '../../../common/api_schemas/schedule_now_transforms'; -import { isScheduleNowTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; - +import { addInternalBasePath } from '../../../common/constants'; +import type { + ScheduleNowTransformsRequestSchema, + ScheduleNowTransformsResponseSchema, +} from '../../../common/api_schemas/schedule_now_transforms'; import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; -import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { ToastNotificationText } from '../components'; -import { useApi } from './use_api'; +import { useRefreshTransformList } from './use_refresh_transform_list'; export const useScheduleNowTransforms = () => { - const { overlays, theme } = useAppDependencies(); + const { http, i18n: i18nStart, theme } = useAppDependencies(); + const refreshTransformList = useRefreshTransformList(); const toastNotifications = useToastNotifications(); - const api = useApi(); - return async (transformsInfo: ScheduleNowTransformsRequestSchema) => { - const results = await api.scheduleNowTransforms(transformsInfo); - - if (!isScheduleNowTransformsResponseSchema(results)) { + const mutation = useMutation({ + mutationFn: (reqBody: ScheduleNowTransformsRequestSchema) => + http.post( + addInternalBasePath('schedule_now_transforms'), + { + body: JSON.stringify(reqBody), + version: '1', + } + ), + onError: (error) => toastNotifications.addDanger({ title: i18n.translate( 'xpack.transform.stepCreateForm.scheduleNowTransformResponseSchemaErrorMessage', @@ -39,46 +46,38 @@ export const useScheduleNowTransforms = () => { 'An error occurred calling the request to schedule the transform to process data instantly.', } ), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - return; - } - - for (const transformId in results) { - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (results.hasOwnProperty(transformId)) { - const result = results[transformId]; - if (result.success === true) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.scheduleNowTransformSuccessMessage', { - defaultMessage: - 'Request to schedule transform {transformId} to process data instantly acknowledged.', - values: { transformId }, - }) - ); - } else { - toastNotifications.addError(new Error(JSON.stringify(result.error!.caused_by, null, 2)), { - title: i18n.translate( - 'xpack.transform.transformList.scheduleNowTransformErrorMessage', + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }), + onSuccess: (results) => { + for (const transformId in results) { + // hasOwnProperty check to ensure only properties on object itself, and not its prototypes + if (results.hasOwnProperty(transformId)) { + const result = results[transformId]; + if (!result.success) { + toastNotifications.addError( + new Error(JSON.stringify(result.error!.caused_by, null, 2)), { - defaultMessage: - 'An error occurred scheduling transform {transformId} to process data instantly.', - values: { transformId }, + title: i18n.translate( + 'xpack.transform.transformList.scheduleNowTransformErrorMessage', + { + defaultMessage: + 'An error occurred scheduling transform {transformId} to process data instantly.', + values: { transformId }, + } + ), + toastMessage: result.error!.reason, } - ), - toastMessage: result.error!.reason, - }); + ); + } } } - } - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - }; + refreshTransformList(); + }, + }); + + return mutation.mutate; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index c4ccff9944dd4..f2a59e0cb1a61 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -39,7 +39,10 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { } try { - fetchedSavedSearch = await appDeps.savedSearch.get(id); + // If data view already found, no need to get saved search + if (!fetchedDataView) { + fetchedSavedSearch = await appDeps.savedSearch.get(id); + } } catch (e) { // Just let fetchedSavedSearch stay undefined in case it doesn't exist. } diff --git a/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx index e4d9bdfe431a9..104c3145fc259 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx @@ -6,31 +6,35 @@ */ import React from 'react'; +import { useMutation } from '@tanstack/react-query'; import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '@kbn/react-kibana-mount'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; - -import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms'; -import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; - +import { addInternalBasePath } from '../../../common/constants'; +import type { + StartTransformsRequestSchema, + StartTransformsResponseSchema, +} from '../../../common/api_schemas/start_transforms'; import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; -import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { ToastNotificationText } from '../components'; -import { useApi } from './use_api'; +import { useRefreshTransformList } from './use_refresh_transform_list'; export const useStartTransforms = () => { - const { overlays, theme } = useAppDependencies(); + const { http, i18n: i18nStart, theme } = useAppDependencies(); + const refreshTransformList = useRefreshTransformList(); const toastNotifications = useToastNotifications(); - const api = useApi(); - return async (transformsInfo: StartTransformsRequestSchema) => { - const results = await api.startTransforms(transformsInfo); - - if (!isStartTransformsResponseSchema(results)) { + const mutation = useMutation({ + mutationFn: (reqBody: StartTransformsRequestSchema) => + http.post(addInternalBasePath('start_transforms'), { + body: JSON.stringify(reqBody), + version: '1', + }), + onError: (error) => toastNotifications.addDanger({ title: i18n.translate( 'xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage', @@ -38,41 +42,34 @@ export const useStartTransforms = () => { defaultMessage: 'An error occurred calling the start transforms request.', } ), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - return; - } - - for (const transformId in results) { - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (results.hasOwnProperty(transformId)) { - const result = results[transformId]; - if (result.success === true) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.startTransformSuccessMessage', { - defaultMessage: 'Request to start transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - } else { - toastNotifications.addError(new Error(JSON.stringify(result.error!.caused_by, null, 2)), { - title: i18n.translate('xpack.transform.transformList.startTransformErrorMessage', { - defaultMessage: 'An error occurred starting the transform {transformId}', - values: { transformId }, - }), - toastMessage: result.error!.reason, - }); + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }), + onSuccess: (results) => { + for (const transformId in results) { + // hasOwnProperty check to ensure only properties on object itself, and not its prototypes + if (results.hasOwnProperty(transformId)) { + const result = results[transformId]; + if (!result.success) { + toastNotifications.addError( + new Error(JSON.stringify(result.error!.caused_by, null, 2)), + { + title: i18n.translate('xpack.transform.transformList.startTransformErrorMessage', { + defaultMessage: 'An error occurred starting the transform {transformId}', + values: { transformId }, + }), + toastMessage: result.error!.reason, + } + ); + } } } - } - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - }; + refreshTransformList(); + }, + }); + + return mutation.mutate; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx index 39d009e471180..564b17feac3f9 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx @@ -6,31 +6,36 @@ */ import React from 'react'; +import { useMutation } from '@tanstack/react-query'; import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; - -import type { StopTransformsRequestSchema } from '../../../common/api_schemas/stop_transforms'; -import { isStopTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { addInternalBasePath } from '../../../common/constants'; +import type { + StopTransformsRequestSchema, + StopTransformsResponseSchema, +} from '../../../common/api_schemas/stop_transforms'; import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; -import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { ToastNotificationText } from '../components'; -import { useApi } from './use_api'; +import { useRefreshTransformList } from './use_refresh_transform_list'; export const useStopTransforms = () => { - const { overlays, theme } = useAppDependencies(); + const { http, i18n: i18nStart, theme } = useAppDependencies(); + const refreshTransformList = useRefreshTransformList(); const toastNotifications = useToastNotifications(); - const api = useApi(); - - return async (transformsInfo: StopTransformsRequestSchema) => { - const results = await api.stopTransforms(transformsInfo); - if (!isStopTransformsResponseSchema(results)) { + const mutation = useMutation({ + mutationFn: (reqBody: StopTransformsRequestSchema) => + http.post(addInternalBasePath('stop_transforms'), { + body: JSON.stringify(reqBody), + version: '1', + }), + onError: (error) => toastNotifications.addDanger({ title: i18n.translate( 'xpack.transform.transformList.stopTransformResponseSchemaErrorMessage', @@ -38,39 +43,29 @@ export const useStopTransforms = () => { defaultMessage: 'An error occurred called the stop transforms request.', } ), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - return; - } - - for (const transformId in results) { - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (results.hasOwnProperty(transformId)) { - if (results[transformId].success === true) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.stopTransformSuccessMessage', { - defaultMessage: 'Request to stop data frame transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - } else { - toastNotifications.addDanger( - i18n.translate('xpack.transform.transformList.stopTransformErrorMessage', { - defaultMessage: 'An error occurred stopping the data frame transform {transformId}', - values: { transformId }, - }) - ); + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }), + onSuccess: (results) => { + for (const transformId in results) { + // hasOwnProperty check to ensure only properties on object itself, and not its prototypes + if (results.hasOwnProperty(transformId)) { + if (!results[transformId].success) { + toastNotifications.addDanger( + i18n.translate('xpack.transform.transformList.stopTransformErrorMessage', { + defaultMessage: 'An error occurred stopping the data frame transform {transformId}', + values: { transformId }, + }) + ); + } } } - } - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - }; + refreshTransformList(); + }, + }); + + return mutation.mutate; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_transform_capabilities.ts b/x-pack/plugins/transform/public/app/hooks/use_transform_capabilities.ts new file mode 100644 index 0000000000000..f497da3bd51de --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_transform_capabilities.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getInitialTransformCapabilities, + isTransformCapabilities, +} from '../../../common/types/capabilities'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useTransformCapabilities = () => { + const { application } = useAppDependencies(); + + if (isTransformCapabilities(application?.capabilities?.transform)) { + return application.capabilities.transform; + } + + return getInitialTransformCapabilities(); +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts index f96a0f72194c6..0871dd7877c14 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts @@ -29,14 +29,13 @@ import { } from '@kbn/ml-data-grid'; import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms'; -import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards'; import { getErrorMessage } from '../../../common/utils/errors'; import { getPreviewTransformRequestBody, type TransformConfigQuery } from '../common'; import { SearchItems } from './use_search_items'; -import { useApi } from './use_api'; +import { useGetTransformsPreview } from './use_get_transforms_preview'; import { StepDefineExposedState } from '../sections/create_transform/components/step_define'; import { isLatestPartialRequest, @@ -111,7 +110,6 @@ export const useTransformConfigData = ( ): UseIndexDataReturnType => { const [previewMappingsProperties, setPreviewMappingsProperties] = useState({}); - const api = useApi(); // Filters mapping properties of type `object`, which get returned for nested field parents. const columnKeys = Object.keys(previewMappingsProperties).filter( @@ -147,84 +145,100 @@ export const useTransformConfigData = ( tableItems, } = dataGrid; - const getPreviewData = async () => { - if (!validationStatus.isValid) { + const previewRequest = useMemo( + () => + getPreviewTransformRequestBody( + dataView, + query, + requestPayload, + combinedRuntimeMappings, + timeRangeMs + ), + [dataView, query, requestPayload, combinedRuntimeMappings, timeRangeMs] + ); + + const { + error: previewError, + data: previewData, + isError, + isLoading, + } = useGetTransformsPreview(previewRequest, validationStatus.isValid); + + useEffect(() => { + if (isLoading) { + setErrorMessage(''); + setNoDataMessage(''); + setStatus(INDEX_STATUS.LOADING); + } else if (isError) { + setErrorMessage(getErrorMessage(previewError)); setTableItems([]); setRowCountInfo({ rowCount: 0, rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ, }); - setNoDataMessage(validationStatus.errorMessage!); - return; + setPreviewMappingsProperties({}); + setStatus(INDEX_STATUS.ERROR); + } else if (!isLoading && !isError && previewData !== undefined) { + // To improve UI performance with a latest configuration for indices with a large number + // of fields, we reduce the number of available columns to those populated with values. + + // 1. Flatten the returned object structure object documents to match mapping properties + const docs = previewData.preview.map(getFlattenedObject); + + // 2. Get all field names for each returned doc and flatten it + // to a list of unique field names used across all docs. + const populatedFields = [...new Set(docs.map(Object.keys).flat(1))]; + + // 3. Filter mapping properties by populated fields + let populatedProperties: PreviewMappingsProperties = Object.entries( + previewData.generated_dest_index.mappings.properties + ) + .filter(([key]) => populatedFields.includes(key)) + .reduce( + (p, [key, value]) => ({ + ...p, + [key]: value, + }), + {} + ); + + populatedProperties = getCombinedProperties(populatedProperties, docs); + + setTableItems(docs); + setRowCountInfo({ + rowCount: docs.length, + rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ, + }); + setPreviewMappingsProperties(populatedProperties); + setStatus(INDEX_STATUS.LOADED); + + if (docs.length === 0) { + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + }) + ); + } else { + setNoDataMessage(''); + } } + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isError, isLoading, previewData]); - setErrorMessage(''); - setNoDataMessage(''); - setStatus(INDEX_STATUS.LOADING); - - const previewRequest = getPreviewTransformRequestBody( - dataView, - query, - requestPayload, - combinedRuntimeMappings, - timeRangeMs - ); - const resp = await api.getTransformsPreview(previewRequest); - - if (!isPostTransformsPreviewResponseSchema(resp)) { - setErrorMessage(getErrorMessage(resp)); + useEffect(() => { + if (!validationStatus.isValid) { setTableItems([]); setRowCountInfo({ rowCount: 0, rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ, }); - setPreviewMappingsProperties({}); - setStatus(INDEX_STATUS.ERROR); - return; - } - - // To improve UI performance with a latest configuration for indices with a large number - // of fields, we reduce the number of available columns to those populated with values. - - // 1. Flatten the returned object structure object documents to match mapping properties - const docs = resp.preview.map(getFlattenedObject); - - // 2. Get all field names for each returned doc and flatten it - // to a list of unique field names used across all docs. - const populatedFields = [...new Set(docs.map(Object.keys).flat(1))]; - - // 3. Filter mapping properties by populated fields - let populatedProperties: PreviewMappingsProperties = Object.entries( - resp.generated_dest_index.mappings.properties - ) - .filter(([key]) => populatedFields.includes(key)) - .reduce( - (p, [key, value]) => ({ - ...p, - [key]: value, - }), - {} - ); - - populatedProperties = getCombinedProperties(populatedProperties, docs); - - setTableItems(docs); - setRowCountInfo({ - rowCount: docs.length, - rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ, - }); - setPreviewMappingsProperties(populatedProperties); - setStatus(INDEX_STATUS.LOADED); - - if (docs.length === 0) { - setNoDataMessage( - i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { - defaultMessage: - 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', - }) - ); + setNoDataMessage(validationStatus.errorMessage!); } - }; + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [validationStatus.isValid]); useEffect(() => { resetPagination(); @@ -232,15 +246,6 @@ export const useTransformConfigData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(query)]); - useEffect(() => { - getPreviewData(); - // custom comparison - /* eslint-disable react-hooks/exhaustive-deps */ - }, [ - dataView.getIndexPattern(), - JSON.stringify([requestPayload, query, combinedRuntimeMappings, timeRangeMs]), - ]); - if (sortingColumns.length > 0) { const sortingColumnsWithTypes = sortingColumns.map((c) => { // Since items might contain undefined/null values, we want to accurate find the data type @@ -291,13 +296,7 @@ export const useTransformConfigData = ( return cellValue; }; - }, [ - pageData, - pagination.pageIndex, - pagination.pageSize, - previewMappingsProperties, - formatHumanReadableDateTimeSeconds, - ]); + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappingsProperties]); return { ...dataGrid, diff --git a/x-pack/plugins/transform/public/app/hooks/use_update_transform.ts b/x-pack/plugins/transform/public/app/hooks/use_update_transform.ts new file mode 100644 index 0000000000000..3859f9a8353f0 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_update_transform.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation } from '@tanstack/react-query'; + +import type { + PostTransformsUpdateRequestSchema, + PostTransformsUpdateResponseSchema, +} from '../../../common/api_schemas/update_transforms'; +import { addInternalBasePath } from '../../../common/constants'; +import type { TransformId } from '../../../common/types/transform'; + +import { useAppDependencies } from '../app_dependencies'; + +import { useRefreshTransformList } from './use_refresh_transform_list'; + +export const useUpdateTransform = ( + transformId: TransformId, + transformConfig: PostTransformsUpdateRequestSchema +) => { + const { http } = useAppDependencies(); + const refreshTransformList = useRefreshTransformList(); + + const mutation = useMutation({ + mutationFn: () => + http.post( + addInternalBasePath(`transforms/${transformId}/_update`), + { + body: JSON.stringify(transformConfig), + version: '1', + } + ), + onSuccess: () => refreshTransformList(), + }); + + return mutation.mutate; +}; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx deleted file mode 100644 index 02bbe4e40a969..0000000000000 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { createContext } from 'react'; -import { useQuery } from '@tanstack/react-query'; - -import type { IHttpFetchError } from '@kbn/core-http-browser'; - -import type { Privileges } from '../../../../../common/types/privileges'; - -import { - type PrivilegesAndCapabilities, - type TransformCapabilities, - INITIAL_CAPABILITIES, -} from '../../../../../common/privilege/has_privilege_factory'; - -import { useAppDependencies } from '../../../app_dependencies'; - -interface Authorization { - isLoading: boolean; - apiError: Error | null; - privileges: Privileges; - capabilities: TransformCapabilities; -} - -const initialValue: Authorization = { - isLoading: true, - apiError: null, - privileges: { - hasAllPrivileges: false, - missingPrivileges: {}, - }, - capabilities: INITIAL_CAPABILITIES, -}; - -export const AuthorizationContext = createContext({ ...initialValue }); - -interface Props { - privilegesEndpoint: { path: string; version: string }; - children: React.ReactNode; -} - -export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) => { - const { http } = useAppDependencies(); - - const { path, version } = privilegesEndpoint; - - const { - isLoading, - error, - data: privilegesData, - } = useQuery( - ['transform-privileges-and-capabilities'], - async ({ signal }) => { - return await http.fetch(path, { - version, - method: 'GET', - signal, - }); - } - ); - - const value = { - isLoading, - privileges: - isLoading || privilegesData === undefined - ? { ...initialValue.privileges } - : privilegesData.privileges, - capabilities: - isLoading || privilegesData === undefined - ? { ...INITIAL_CAPABILITIES } - : privilegesData.capabilities, - apiError: error ? error : null, - }; - - return ( - {children} - ); -}; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/index.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/index.ts deleted file mode 100644 index cb0f248efc165..0000000000000 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { createCapabilityFailureMessage } from '../../../../../common/privilege/has_privilege_factory'; -export { AuthorizationProvider, AuthorizationContext } from './authorization_provider'; -export { PrivilegesWrapper } from './with_privileges'; -export { NotAuthorizedSection } from './not_authorized_section'; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/not_authorized_section.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/not_authorized_section.tsx deleted file mode 100644 index 4b0942276c028..0000000000000 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/not_authorized_section.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiEmptyPrompt } from '@elastic/eui'; - -interface Props { - title: React.ReactNode; - message: React.ReactNode | string; -} - -export const NotAuthorizedSection = ({ title, message }: Props) => ( - {title}} body={

{message}

} /> -); diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx deleted file mode 100644 index 2117591142b26..0000000000000 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useContext, FC } from 'react'; - -import { - EuiFlexItem, - EuiFlexGroup, - EuiPageContent_Deprecated as EuiPageContent, -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n-react'; - -import { MissingPrivileges } from '../../../../../common/types/privileges'; - -import { SectionLoading } from '../../../components'; - -import { AuthorizationContext } from './authorization_provider'; -import { NotAuthorizedSection } from './not_authorized_section'; -import { - hasPrivilegeFactory, - toArray, - Privilege, -} from '../../../../../common/privilege/has_privilege_factory'; - -interface Props { - /** - * Each required privilege must have the format "section.privilege". - * To indicate that *all* privileges from a section are required, we can use the asterix - * e.g. "index.*" - */ - privileges: string | string[]; - children: (childrenProps: { - isLoading: boolean; - hasPrivileges: boolean; - privilegesMissing: MissingPrivileges; - }) => JSX.Element; -} - -export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { - const { isLoading, privileges } = useContext(AuthorizationContext); - - const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map((p) => { - const [section, privilege] = p.split('.'); - if (!privilege) { - // Oh! we forgot to use the dot "." notation. - throw new Error('Required privilege must have the format "section.privilege"'); - } - return [section, privilege]; - }); - - const hasPrivilege = hasPrivilegeFactory(privileges); - const hasPrivileges = isLoading ? false : privilegesToArray.every(hasPrivilege); - - const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => { - if (privilege === '*') { - acc[section] = privileges.missingPrivileges[section] || []; - } else if ( - privileges.missingPrivileges[section] && - privileges.missingPrivileges[section]!.includes(privilege) - ) { - const missing: string[] = acc[section] || []; - acc[section] = [...missing, privilege]; - } - - return acc; - }, {} as MissingPrivileges); - - return children({ isLoading, hasPrivileges, privilegesMissing }); -}; - -interface MissingClusterPrivilegesProps { - missingPrivileges: string; - privilegesCount: number; -} - -const MissingClusterPrivileges: FC = ({ - missingPrivileges, - privilegesCount, -}) => ( - - - - - } - message={ - - } - /> - - - -); - -export const PrivilegesWrapper: FC<{ privileges: string | string[] }> = ({ - children, - privileges, -}) => ( - - {({ isLoading, hasPrivileges, privilegesMissing }) => { - if (isLoading) { - return ( - - - - ); - } - - if (!hasPrivileges) { - return ( - - ); - } - - return <>{children}; - }} - -); diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 2131ae616553d..47fa44a53d182 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -5,30 +5,23 @@ * 2.0. */ -import React, { useEffect, useState, FC } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { parse } from 'query-string'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiCallOut, - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, - EuiSpacer, -} from '@elastic/eui'; -import { isHttpFetchError } from '@kbn/core-http-browser'; -import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; +import { EuiButtonEmpty, EuiCallOut, EuiPageTemplate, EuiSpacer } from '@elastic/eui'; + import { TransformConfigUnion } from '../../../../common/types/transform'; -import { useApi } from '../../hooks/use_api'; +import { useGetTransform } from '../../hooks'; import { useDocumentationLinks } from '../../hooks/use_documentation_links'; import { useSearchItems } from '../../hooks/use_search_items'; -import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { PrivilegesWrapper } from '../../lib/authorization'; +import { BREADCRUMB_SECTION, breadcrumbService, docTitleService } from '../../services/navigation'; +import { CapabilitiesWrapper } from '../../components/capabilities_wrapper'; import { Wizard } from '../create_transform/components/wizard'; import { overrideTransformForCloning } from '../../common/transform'; @@ -45,8 +38,6 @@ export const CloneTransformSection: FC = ({ match, location }) => { docTitleService.setTitle('createTransform'); }, []); - const api = useApi(); - const { esTransform } = useDocumentationLinks(); const transformId = match.params.transformId; @@ -56,52 +47,55 @@ export const CloneTransformSection: FC = ({ match, location }) => { const [isInitialized, setIsInitialized] = useState(false); const { error: searchItemsError, searchItems, setSavedObjectId } = useSearchItems(undefined); - const fetchTransformConfig = async () => { + useEffect(() => { + if (dataViewId === undefined) { + setErrorMessage( + i18n.translate('xpack.transform.clone.fetchErrorPromptText', { + defaultMessage: 'Could not fetch the Kibana data view ID.', + }) + ); + } else { + setSavedObjectId(dataViewId); + } + }, [dataViewId, setSavedObjectId]); + + useEffect(() => { if (searchItemsError !== undefined) { setTransformConfig(undefined); setErrorMessage(searchItemsError); setIsInitialized(true); - return; } + }, [searchItemsError]); + + const { data: transformConfigs, error } = useGetTransform( + transformId, + searchItemsError === undefined + ); - const transformConfigs = await api.getTransform(transformId); - if (isHttpFetchError(transformConfigs)) { + useEffect(() => { + if (error !== null && error.message !== errorMessage) { setTransformConfig(undefined); - setErrorMessage(transformConfigs.message); + setErrorMessage(error.message); setIsInitialized(true); return; } - try { - if (dataViewId === undefined) { - throw new Error( - i18n.translate('xpack.transform.clone.fetchErrorPromptText', { - defaultMessage: 'Could not fetch the Kibana data view ID.', - }) - ); - } - - setSavedObjectId(dataViewId); - - setTransformConfig(overrideTransformForCloning(transformConfigs.transforms[0])); - setErrorMessage(undefined); - setIsInitialized(true); - } catch (e) { - setTransformConfig(undefined); - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e, null, 2)); + if (transformConfigs !== undefined) { + try { + setTransformConfig(overrideTransformForCloning(transformConfigs.transforms[0])); + setErrorMessage(undefined); + setIsInitialized(true); + } catch (e) { + setTransformConfig(undefined); + if (e.message !== undefined) { + setErrorMessage(e.message); + } else { + setErrorMessage(JSON.stringify(e, null, 2)); + } + setIsInitialized(true); } - setIsInitialized(true); } - }; - - useEffect(() => { - fetchTransformConfig(); - // The effect should only be called once. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [error, errorMessage, transformConfigs]); const docsLink = ( = ({ match, location }) => { ); return ( - - + = ({ match, location }) => { } rightSideItems={[docsLink]} bottomBorder + paddingSize={'none'} /> - - {typeof errorMessage !== 'undefined' && ( + + {typeof errorMessage !== 'undefined' ? ( <> = ({ match, location }) => { - )} + ) : null} + {searchItems !== undefined && isInitialized === true && transformConfig !== undefined && ( )} - - + + ); }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/advanced_pivot_editor_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/advanced_pivot_editor_switch.tsx index 900af603266b8..a125da52b0cae 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/advanced_pivot_editor_switch.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/advanced_pivot_editor_switch.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/advanced_query_editor_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/advanced_query_editor_switch.tsx index 43c6684a5a2bc..cb21e7026f27b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/advanced_query_editor_switch.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/advanced_query_editor_switch.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiSwitch } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx index 2ee8bc9995df6..c53c5dae0b4ad 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { EuiSwitch } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SwitchModal } from './switch_modal'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/switch_modal.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/switch_modal.tsx index ff08ab37bb3e6..37dd6dd01f98e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/switch_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/switch_modal.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx index db617690efc5f..8b76b8d7d7e1a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { EuiButton, EuiButtonIcon, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx index 665c9986e6e69..4fea2f43bd5db 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap deleted file mode 100644 index 09056b8529f16..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap +++ /dev/null @@ -1,72 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Date histogram aggregation 1`] = ` - - - - - the-group-by-agg-name - - - - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} - id="transformFormPopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - - - - - - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_form.test.tsx.snap deleted file mode 100644 index 89b54e6d0a22f..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_form.test.tsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Minimal initialization 1`] = ` - - - - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_summary.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_summary.test.tsx.snap deleted file mode 100644 index f08d7fab7c829..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_summary.test.tsx.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Minimal initialization 1`] = ` - - -
- the-agg -
-
- -
-`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/popover_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/popover_form.test.tsx.snap deleted file mode 100644 index f7b4e836ee784..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/popover_form.test.tsx.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Aggregation Minimal initialization 1`] = ` - - - - - - - Apply - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx index fc9f91f96b3ff..b036016314754 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { AggName } from '../../../../../../common/types/aggregations'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -31,8 +31,8 @@ describe('Transform: ', () => { onChange() {}, }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toBe('the-group-by-agg-name'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx index 1525aa1a3320b..205fb93479339 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -29,8 +29,8 @@ describe('Transform: ', () => { onChange() {}, }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toBe('the-group-by-agg-name'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx index 71d728798c8fb..d1078f4d5a87a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -26,8 +26,8 @@ describe('Transform: ', () => { list: { 'the-agg': item }, }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toBe('the-agg'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx index 0587d4f0e1c46..88cdad407bd62 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { shallow } from 'enzyme'; import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { AggName } from '../../../../../../common/types/aggregations'; @@ -29,7 +28,7 @@ describe('Transform: Aggregation ', () => { const otherAggNames: AggName[] = []; const onChange = (item: PivotAggsConfig) => {}; - const wrapper = shallow( + const { getByTestId } = render( ', () => { /> ); - expect(wrapper).toMatchSnapshot(); + const input = getByTestId('transformAggName'); + expect(input).toHaveValue('the-group-by-agg-name'); }); test('preserves the field for unsupported aggs', async () => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx index a57d83b75aa10..2f5672d3ec592 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiSwitch } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap deleted file mode 100644 index 0bb11827655e0..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap +++ /dev/null @@ -1,234 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Date histogram aggregation 1`] = ` - - - - the-group-by-agg-name - - - - - 1m - - - - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} - id="transformIntervalFormPopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - - - - - - - -`; - -exports[`Transform: Histogram aggregation 1`] = ` - - - - the-group-by-agg-name - - - - - 100 - - - - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} - id="transformIntervalFormPopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - - - - - - - -`; - -exports[`Transform: Terms aggregation 1`] = ` - - - - the-group-by-agg-name - - - - - } - closePopover={[Function]} - display="inline-block" - hasArrow={true} - id="transformIntervalFormPopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - - - - - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap deleted file mode 100644 index b1d3afeff412c..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Date histogram aggregation 1`] = ` - - - - the-options-data-id - - - - - 1m - - - -`; - -exports[`Transform: Histogram aggregation 1`] = ` - - - - the-options-data-id - - - - - 100 - - - -`; - -exports[`Transform: Terms aggregation 1`] = ` - - - - the-options-data-id - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_form.test.tsx.snap deleted file mode 100644 index da1e9a79680ad..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_form.test.tsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Minimal initialization 1`] = ` - - - - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_summary.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_summary.test.tsx.snap deleted file mode 100644 index 724433a80d3a8..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_summary.test.tsx.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Minimal initialization 1`] = ` - - - - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/popover_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/popover_form.test.tsx.snap deleted file mode 100644 index 9c9fb59eea4b1..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/popover_form.test.tsx.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Group By Minimal initialization 1`] = ` - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.test.tsx index 09ec34f90d751..2edcfc7a9bd2c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common'; @@ -29,9 +29,9 @@ describe('Transform: ', () => { onChange() {}, }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-group-by-agg-name'); }); test('Histogram aggregation', () => { @@ -50,9 +50,9 @@ describe('Transform: ', () => { onChange() {}, }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-group-by-agg-name'); }); test('Terms aggregation', () => { @@ -70,8 +70,8 @@ describe('Transform: ', () => { onChange() {}, }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-group-by-agg-name'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.test.tsx index 689c27f5f81be..5c03d3bc8f320 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common'; @@ -26,9 +26,9 @@ describe('Transform: ', () => { optionsDataId: 'the-options-data-id', }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-options-data-id'); }); test('Histogram aggregation', () => { @@ -44,9 +44,9 @@ describe('Transform: ', () => { optionsDataId: 'the-options-data-id', }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-options-data-id'); }); test('Terms aggregation', () => { @@ -61,8 +61,8 @@ describe('Transform: ', () => { optionsDataId: 'the-options-data-id', }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-options-data-id'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.test.tsx index 61a11a8b551b3..eb942cad63b01 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common'; @@ -27,8 +27,8 @@ describe('Transform: ', () => { onChange() {}, }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-group-by-agg-name'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.test.tsx index 201b36a41b09c..eadddd8356e42 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common'; @@ -24,8 +24,8 @@ describe('Transform: ', () => { list: { 'the-options-data-id': item }, }; - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-options-data-id'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx index d5b1139e6cdae..4ef6cbbaf51be 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; @@ -101,7 +101,7 @@ describe('Transform: Group By ', () => { appName: 'the-test-app', }; - const wrapper = shallow( + const { getByDisplayValue } = render( ', () => { ); - expect(wrapper.find(PopoverForm)).toMatchSnapshot(); + expect(getByDisplayValue('the-agg-name')).toBeInTheDocument(); + expect(getByDisplayValue('1m')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx index e0a52978c0b4a..42b9d556f9dc2 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiCode, EuiInputPopover } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx index 2e785c2d6680b..e2fa8912fc932 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { StepCreateForm, StepCreateFormProps } from './step_create_form'; @@ -16,6 +17,7 @@ jest.mock('../../../../app_dependencies'); describe('Transform: ', () => { test('Minimal initialization', () => { // Arrange + const queryClient = new QueryClient(); const props: StepCreateFormProps = { createDataView: false, transformId: 'the-transform-id', @@ -35,7 +37,11 @@ describe('Transform: ', () => { onChange() {}, }; - const { getByText } = render(); + const { getByText } = render( + + + + ); // Act // Assert diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index d61b18632cef4..3c78757a6f257 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -24,25 +24,19 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; import { DuplicateDataViewError } from '@kbn/data-plugin/public'; import type { RuntimeField } from '@kbn/data-views-plugin/common'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms'; -import { - isGetTransformsStatsResponseSchema, - isPutTransformsResponseSchema, - isStartTransformsResponseSchema, -} from '../../../../../../common/api_schemas/type_guards'; import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; import { getErrorMessage } from '../../../../../../common/utils/errors'; import { getTransformProgress } from '../../../../common'; -import { useApi } from '../../../../hooks/use_api'; +import { useCreateTransform, useGetTransformStats, useStartTransforms } from '../../../../hooks'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { RedirectToTransformManagement } from '../../../../common/navigation'; import { ToastNotificationText } from '../../../../components'; @@ -92,11 +86,10 @@ export const StepCreateForm: FC = React.memo( ); const [discoverLink, setDiscoverLink] = useState(); - const deps = useAppDependencies(); - const { share } = deps; - const dataViews = deps.data.dataViews; const toastNotifications = useToastNotifications(); - const isDiscoverAvailable = deps.application.capabilities.discover?.show ?? false; + const { application, data, i18n: i18nStart, share, theme } = useAppDependencies(); + const dataViews = data.dataViews; + const isDiscoverAvailable = application.capabilities.discover?.show ?? false; useEffect(() => { let unmounted = false; @@ -128,104 +121,38 @@ export const StepCreateForm: FC = React.memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, [created, started, dataViewId]); - const { overlays, theme } = useAppDependencies(); - const api = useApi(); + const startTransforms = useStartTransforms(); + const createTransform = useCreateTransform(); - async function createTransform() { + function createTransformHandler(startAfterCreation = false) { setLoading(true); - const resp = await api.createTransform(transformId, transformConfig); - - if (!isPutTransformsResponseSchema(resp) || resp.errors.length > 0) { - let respErrors: - | PutTransformsResponseSchema['errors'] - | PutTransformsResponseSchema['errors'][number] - | undefined; - - if (isPutTransformsResponseSchema(resp) && resp.errors.length > 0) { - respErrors = resp.errors.length === 1 ? resp.errors[0] : resp.errors; + createTransform( + { transformId, transformConfig }, + { + onError: () => setCreated(false), + onSuccess: () => { + setCreated(true); + if (createDataView) { + createKibanaDataView(); + } + if (startAfterCreation) { + startTransform(); + } + }, + onSettled: () => setLoading(false), } - - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepCreateForm.createTransformErrorMessage', { - defaultMessage: 'An error occurred creating the transform {transformId}:', - values: { transformId }, - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - setCreated(false); - setLoading(false); - return false; - } - - toastNotifications.addSuccess( - i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', { - defaultMessage: 'Request to create transform {transformId} acknowledged.', - values: { transformId }, - }) ); - setCreated(true); - setLoading(false); - - if (createDataView) { - createKibanaDataView(); - } - - return true; } - async function startTransform() { + function startTransform() { setLoading(true); - const resp = await api.startTransforms([{ id: transformId }]); - - if (isStartTransformsResponseSchema(resp) && resp[transformId]?.success === true) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', { - defaultMessage: 'Request to start transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - setStarted(true); - setLoading(false); - return; - } - - const errorMessage = - isStartTransformsResponseSchema(resp) && resp[transformId]?.success === false - ? resp[transformId].error - : resp; - - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', { - defaultMessage: 'An error occurred starting the transform {transformId}:', - values: { transformId }, - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), + startTransforms([{ id: transformId }], { + onError: () => setStarted(false), + onSuccess: (resp) => setStarted(resp[transformId]?.success === true), + onSettled: () => setLoading(false), }); - setStarted(false); - setLoading(false); - } - - async function createAndStartTransform() { - const acknowledged = await createTransform(); - if (acknowledged) { - await startTransform(); - } } const createKibanaDataView = async () => { @@ -250,13 +177,6 @@ export const StepCreateForm: FC = React.memo( true ); - toastNotifications.addSuccess( - i18n.translate('xpack.transform.stepCreateForm.createDataViewSuccessMessage', { - defaultMessage: 'Kibana data view {dataViewName} created successfully.', - values: { dataViewName }, - }) - ); - setDataViewId(newDataView.id); setLoading(false); return true; @@ -275,10 +195,10 @@ export const StepCreateForm: FC = React.memo( defaultMessage: 'An error occurred creating the Kibana data view {dataViewName}:', values: { dataViewName }, }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), }); setLoading(false); return false; @@ -288,57 +208,59 @@ export const StepCreateForm: FC = React.memo( const isBatchTransform = typeof transformConfig.sync === 'undefined'; - if ( - loading === false && - started === true && - progressPercentComplete === undefined && - isBatchTransform - ) { - // wrapping in function so we can keep the interval id in local scope - function startProgressBar() { - const interval = setInterval(async () => { - const stats = await api.getTransformStats(transformId); - - if ( - isGetTransformsStatsResponseSchema(stats) && - Array.isArray(stats.transforms) && - stats.transforms.length > 0 - ) { - const percent = - getTransformProgress({ - id: transformId, - config: { - ...transformConfig, - id: transformId, - }, - stats: stats.transforms[0], - }) || 0; - setProgressPercentComplete(percent); - if (percent >= 100) { - clearInterval(interval); - } - } else { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', { - defaultMessage: 'An error occurred getting the progress percentage:', - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - clearInterval(interval); - } - }, PROGRESS_REFRESH_INTERVAL_MS); + useEffect(() => { + if ( + loading === false && + started === true && + progressPercentComplete === undefined && + isBatchTransform + ) { setProgressPercentComplete(0); } + }, [loading, started, progressPercentComplete, isBatchTransform]); + + const progressBarRefetchEnabled = + isBatchTransform && + typeof progressPercentComplete === 'number' && + progressPercentComplete < 100; + const progressBarRefetchInterval = progressBarRefetchEnabled + ? PROGRESS_REFRESH_INTERVAL_MS + : false; + + const { data: stats } = useGetTransformStats( + transformId, + progressBarRefetchEnabled, + progressBarRefetchInterval + ); + + useEffect(() => { + if (stats === undefined) { + return; + } - startProgressBar(); - } + if (stats && Array.isArray(stats.transforms) && stats.transforms.length > 0) { + const percent = + getTransformProgress({ + id: transformId, + config: { + ...transformConfig, + id: transformId, + }, + stats: stats.transforms[0], + }) || 0; + setProgressPercentComplete(percent); + } else { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', { + defaultMessage: 'An error occurred getting the progress percentage:', + }), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }); + } + }, [i18nStart, stats, theme, toastNotifications, transformConfig, transformId]); function getTransformConfigDevConsoleStatement() { return `PUT _transform/${transformId}\n${JSON.stringify(transformConfig, null, 2)}\n\n`; @@ -362,7 +284,7 @@ export const StepCreateForm: FC = React.memo( createTransformHandler(true)} data-test-subj="transformWizardCreateAndStartButton" > {i18n.translate('xpack.transform.stepCreateForm.createAndStartTransformButton', { @@ -436,7 +358,7 @@ export const StepCreateForm: FC = React.memo( createTransformHandler()} data-test-subj="transformWizardCreateButton" > {i18n.translate('xpack.transform.stepCreateForm.createTransformButton', { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx index b7704044fe0b3..2be067c777a1e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; export const StepCreateSummary: FC = React.memo(() => { return null; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx index 0c6fae30fd1f0..c35cca74c72ac 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx @@ -6,7 +6,7 @@ */ import { debounce } from 'lodash'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -18,7 +18,6 @@ import { i18n } from '@kbn/i18n'; import { isMultiBucketAggregate } from '@kbn/ml-agg-utils'; import { useDataSearch } from '../../../../../../../hooks/use_data_search'; -import { isEsSearchResponseWithAggregations } from '../../../../../../../../../common/api_schemas/type_guards'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; import { useToastNotifications } from '../../../../../../../app_dependencies'; @@ -33,16 +32,22 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm selectedField, }) => { const { dataView, runtimeMappings } = useContext(CreateTransformWizardContext); - const dataSearch = useDataSearch(); const toastNotifications = useToastNotifications(); - const [options, setOptions] = useState([]); - const [isLoading, setIsLoading] = useState(true); const [searchValue, setSearchValue] = useState(''); + const debouncedOnSearchChange = useMemo( + () => debounce((d: string) => setSearchValue(d), 600), + [] + ); - const onSearchChange = (newSearchValue: string) => { - setSearchValue(newSearchValue); - }; + useEffect(() => { + // Simulate initial load. + debouncedOnSearchChange(''); + // Cancel debouncing when unmounting + return () => debouncedOnSearchChange.cancel(); + // Only call on mount + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, []); const updateConfig = useCallback( (update) => { @@ -56,80 +61,53 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm [config, onChange] ); - useEffect(() => { - const abortController = new AbortController(); - - const fetchOptions = debounce(async () => { - if (selectedField === undefined) return; - - setIsLoading(true); - setOptions([]); - - const esSearchRequest = { - index: dataView!.title, - body: { - ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), - query: { - wildcard: { - [selectedField!]: { - value: `*${searchValue}*`, - }, + const { data, isError, isLoading } = useDataSearch( + { + index: dataView!.title, + body: { + ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), + query: { + wildcard: { + [selectedField!]: { + value: `*${searchValue}*`, }, }, - aggs: { - field_values: { - terms: { - field: selectedField, - size: 10, - }, + }, + aggs: { + field_values: { + terms: { + field: selectedField, + size: 10, }, }, - size: 0, }, - }; - - const response = await dataSearch(esSearchRequest, abortController.signal); - - setIsLoading(false); - - if ( - !( - isEsSearchResponseWithAggregations(response) && - isMultiBucketAggregate( - response.aggregations.field_values - ) - ) - ) { - toastNotifications.addWarning( - i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', { - defaultMessage: 'Unable to fetch suggestions', - }) - ); - return; - } + size: 0, + }, + }, + // Check whether fetching should be enabled + selectedField !== undefined + ); - setOptions( - ( - response.aggregations.field_values - .buckets as estypes.AggregationsSignificantLongTermsBucket[] - ).map((value) => ({ label: value.key + '' })) + useEffect(() => { + if (isError) { + toastNotifications.addWarning( + i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', { + defaultMessage: 'Unable to fetch suggestions', + }) ); - }, 600); - - fetchOptions(); - - return () => { - // make sure the ongoing request is canceled - fetchOptions.cancel(); - abortController.abort(); - }; + } /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [selectedField]); - - useEffect(() => { - // Simulate initial load. - onSearchChange(''); - }, []); + }, [isError]); + + const options: EuiComboBoxOptionOption[] = + isMultiBucketAggregate( + data?.aggregations?.field_values + ) + ? ( + data?.aggregations?.field_values + .buckets as estypes.AggregationsSignificantLongTermsBucket[] + ).map((value) => ({ label: value.key + '' })) + : []; useUpdateEffect(() => { // Reset value control on field change @@ -168,7 +146,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm onCreateOption={(value) => { updateConfig({ value }); }} - onSearchChange={onSearchChange} + onSearchChange={debouncedOnSearchChange} data-test-subj="transformFilterTermValueSelector" />
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx index a83c7c7a5871c..ba0d9e93e2cce 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonIcon, EuiCallOut, EuiComboBox, EuiCopy, EuiFormRow } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx index efa28de596a18..861aeb778af74 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiButton, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 6d5d6d0ea6fc9..3470cf5706a2e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { I18nProvider } from '@kbn/i18n-react'; import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; @@ -66,6 +67,7 @@ const createMockStorage = () => ({ describe('Transform: ', () => { test('Minimal initialization', async () => { // Arrange + const queryClient = new QueryClient(); const mlSharedImports = await getMlSharedImports(); const searchItems = { @@ -87,13 +89,15 @@ describe('Transform: ', () => { const { getByText } = render( - - - - - - - + + + + + + + + + ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 9796d9f01de65..246460d11d3ee 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -58,7 +58,7 @@ import { import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { useIndexData } from '../../../../hooks/use_index_data'; import { useTransformConfigData } from '../../../../hooks/use_transform_config_data'; -import { useToastNotifications } from '../../../../app_dependencies'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { SearchItems } from '../../../../hooks/use_search_items'; import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs'; @@ -78,6 +78,9 @@ const ALLOW_TIME_RANGE_ON_TRANSFORM_CONFIG = false; const advancedEditorsSidebarWidth = '220px'; +type PopulatedFields = Set; +const isPopulatedFields = (arg: unknown): arg is PopulatedFields => arg instanceof Set; + export const ConfigSectionTitle: FC<{ title: string }> = ({ title }) => ( <> @@ -120,8 +123,22 @@ export const StepDefineForm: FC = React.memo((props) => { const { transformConfigQuery } = stepDefineForm.searchBar.state; const { runtimeMappings } = stepDefineForm.runtimeMappingsEditor.state; + const appDependencies = useAppDependencies(); + const { + ml: { useFieldStatsFlyoutContext }, + } = appDependencies; + + const fieldStatsContext = useFieldStatsFlyoutContext(); const indexPreviewProps = { - ...useIndexData(dataView, transformConfigQuery, runtimeMappings, timeRangeMs), + ...useIndexData( + dataView, + transformConfigQuery, + runtimeMappings, + timeRangeMs, + isPopulatedFields(fieldStatsContext?.populatedFields) + ? [...fieldStatsContext.populatedFields] + : [] + ), dataTestSubj: 'transformIndexPreview', toastNotifications, }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index 85cef1fb2958e..4f378d4394da0 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -30,6 +31,7 @@ describe('Transform: ', () => { // Using the async/await wait()/done() pattern to avoid act() errors. test('Minimal initialization', async () => { // Arrange + const queryClient = new QueryClient(); const mlSharedImports = await getMlSharedImports(); const searchItems = { @@ -78,9 +80,11 @@ describe('Transform: ', () => { }; const { queryByText } = render( - - - + + + + + ); // Act diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/transform_function_selector.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/transform_function_selector.tsx index 3aec137a3adf8..441e18ebb43d2 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/transform_function_selector.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/transform_function_selector.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { i18n } from '@kbn/i18n'; import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; import { TRANSFORM_FUNCTION, TransformFunction } from '../../../../../../common/constants'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 4e8104a7645ee..da9ebbf9b2ef7 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -25,15 +25,9 @@ import { } from '@elastic/eui'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; -import { isHttpFetchError } from '@kbn/core-http-browser'; import { retentionPolicyMaxAgeInvalidErrorMessage } from '../../../../common/constants/validation_messages'; -import { - isEsIndices, - isEsIngestPipelines, - isPostTransformsPreviewResponseSchema, -} from '../../../../../../common/api_schemas/type_guards'; import { DEFAULT_TRANSFORM_FREQUENCY } from '../../../../../../common/constants'; import { TransformId } from '../../../../../../common/types/transform'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; @@ -42,16 +36,22 @@ import { getErrorMessage } from '../../../../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { ToastNotificationText } from '../../../../components'; -import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; +import { + useDocumentationLinks, + useGetDataViewTitles, + useGetEsIndices, + useGetEsIngestPipelines, + useGetTransforms, + useGetTransformsPreview, +} from '../../../../hooks'; import { SearchItems } from '../../../../hooks/use_search_items'; -import { useApi } from '../../../../hooks/use_api'; import { StepDetailsTimeField } from './step_details_time_field'; import { getTransformConfigQuery, getPreviewTransformRequestBody, isTransformIdValid, } from '../../../../common'; -import { EsIndexName, DataViewTitle } from './common'; +import { EsIndexName } from './common'; import { continuousModeDelayValidator, integerRangeMinus1To100Validator, @@ -73,8 +73,8 @@ interface StepDetailsFormProps { export const StepDetailsForm: FC = React.memo( ({ overrides = {}, onChange, searchItems, stepDefineState }) => { - const deps = useAppDependencies(); - const { capabilities } = deps.application; + const { application, i18n: i18nStart, theme } = useAppDependencies(); + const { capabilities } = application; const toastNotifications = useToastNotifications(); const { esIndicesCreateIndex } = useDocumentationLinks(); @@ -90,19 +90,15 @@ export const StepDetailsForm: FC = React.memo( const [destinationIngestPipeline, setDestinationIngestPipeline] = useState( defaults.destinationIngestPipeline ); - const [transformIds, setTransformIds] = useState([]); - const [indexNames, setIndexNames] = useState([]); - const [ingestPipelineNames, setIngestPipelineNames] = useState([]); const canCreateDataView = useMemo( () => - capabilities.savedObjectsManagement.edit === true || - capabilities.indexPatterns.save === true, + capabilities.savedObjectsManagement?.edit === true || + capabilities.indexPatterns?.save === true, [capabilities] ); // Index pattern state - const [dataViewTitles, setDataViewTitles] = useState([]); const [createDataView, setCreateDataView] = useState( canCreateDataView === false ? false : defaults.createDataView ); @@ -125,126 +121,122 @@ export const StepDetailsForm: FC = React.memo( [setDataViewTimeField, dataViewAvailableTimeFields] ); - const { overlays, theme } = useAppDependencies(); - const api = useApi(); + const { + error: transformsError, + data: { transformIds }, + } = useGetTransforms(); + + useEffect(() => { + if (transformsError !== null) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', { + defaultMessage: 'An error occurred getting the existing transform IDs:', + }), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }); + } + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [transformsError]); + + const previewRequest = useMemo(() => { + const { searchQuery, previewRequest: partialPreviewRequest } = stepDefineState; + const transformConfigQuery = getTransformConfigQuery(searchQuery); + return getPreviewTransformRequestBody( + searchItems.dataView, + transformConfigQuery, + partialPreviewRequest, + stepDefineState.runtimeMappings + ); + }, [searchItems.dataView, stepDefineState]); + const { error: transformsPreviewError, data: transformPreview } = + useGetTransformsPreview(previewRequest); - // fetch existing transform IDs and indices once for form validation useEffect(() => { - // use an IIFE to avoid returning a Promise to useEffect. - (async function () { - const { searchQuery, previewRequest: partialPreviewRequest } = stepDefineState; - const transformConfigQuery = getTransformConfigQuery(searchQuery); - const previewRequest = getPreviewTransformRequestBody( - searchItems.dataView, - transformConfigQuery, - partialPreviewRequest, - stepDefineState.runtimeMappings + if (transformPreview) { + const properties = transformPreview.generated_dest_index.mappings.properties; + const timeFields: string[] = Object.keys(properties).filter( + (col) => properties[col].type === 'date' ); - const transformPreview = await api.getTransformsPreview(previewRequest); - - if (isPostTransformsPreviewResponseSchema(transformPreview)) { - const properties = transformPreview.generated_dest_index.mappings.properties; - const timeFields: string[] = Object.keys(properties).filter( - (col) => properties[col].type === 'date' - ); - - setDataViewAvailableTimeFields(timeFields); - setDataViewTimeField(timeFields[0]); - } else { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', { - defaultMessage: 'An error occurred fetching the transform preview', - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - } + setDataViewAvailableTimeFields(timeFields); + setDataViewTimeField(timeFields[0]); + } + }, [transformPreview]); - const resp = await api.getTransforms(); - - if (isHttpFetchError(resp)) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', { - defaultMessage: 'An error occurred getting the existing transform IDs:', - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - } else { - setTransformIds(resp.transforms.map((transform) => transform.id)); - } + useEffect(() => { + if (transformsPreviewError !== null) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', { + defaultMessage: 'An error occurred fetching the transform preview', + }), + text: toMountPoint( + , + { theme, i18n: i18nStart } + ), + }); + } + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [transformsPreviewError]); - const [indices, ingestPipelines] = await Promise.all([ - api.getEsIndices(), - api.getEsIngestPipelines(), - ]); - - if (isEsIndices(indices)) { - setIndexNames(indices.map((index) => index.name)); - } else { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', { - defaultMessage: 'An error occurred getting the existing index names:', - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - } + const { error: esIndicesError, data: esIndicesData } = useGetEsIndices(); + const indexNames = esIndicesData?.map((index) => index.name) ?? []; - if (isEsIngestPipelines(ingestPipelines)) { - setIngestPipelineNames(ingestPipelines.map(({ name }) => name)); - } else { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIngestPipelines', { - defaultMessage: 'An error occurred getting the existing ingest pipeline names:', - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - } + useEffect(() => { + if (esIndicesError !== null) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', { + defaultMessage: 'An error occurred getting the existing index names:', + }), + text: toMountPoint(, { + theme, + i18n: i18nStart, + }), + }); + } + // custom comparison + /* eslint-disable react-hooks/exhaustive-deps */ + }, [esIndicesError]); - try { - setDataViewTitles(await deps.data.dataViews.getTitles()); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingDataViewTitles', { - defaultMessage: 'An error occurred getting the existing data view titles:', - }), - text: toMountPoint( - , - { theme$: theme.theme$ } - ), - }); - } - })(); - // run once - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const { error: esIngestPipelinesError, data: esIngestPipelinesData } = + useGetEsIngestPipelines(); + const ingestPipelineNames = esIngestPipelinesData?.map(({ name }) => name) ?? []; + + useEffect(() => { + if (esIngestPipelinesError !== null) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIngestPipelines', { + defaultMessage: 'An error occurred getting the existing ingest pipeline names:', + }), + text: toMountPoint( + , + { theme, i18n: i18nStart } + ), + }); + } + // custom comparison + /* eslint-disable react-hooks/exhaustive-deps */ + }, [esIngestPipelinesError]); + + const { error: dataViewTitlesError, data: dataViewTitles } = useGetDataViewTitles(); + + useEffect(() => { + if (dataViewTitlesError !== null) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingDataViewTitles', { + defaultMessage: 'An error occurred getting the existing data view titles:', + }), + text: toMountPoint( + , + { theme, i18n: i18nStart } + ), + }); + } + }, [dataViewTitlesError]); const dateFieldNames = searchItems.dataView.fields .filter((f) => f.type === KBN_FIELD_TYPES.DATE) @@ -284,7 +276,6 @@ export const StepDetailsForm: FC = React.memo( ); setRetentionPolicyMaxAge(''); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRetentionPolicyEnabled]); const transformIdExists = transformIds.some((id) => transformId === id); @@ -294,7 +285,7 @@ export const StepDetailsForm: FC = React.memo( const indexNameExists = indexNames.some((name) => destinationIndex === name); const indexNameEmpty = destinationIndex === ''; const indexNameValid = isValidIndexName(destinationIndex); - const dataViewTitleExists = dataViewTitles.some((name) => destinationIndex === name); + const dataViewTitleExists = dataViewTitles?.some((name) => destinationIndex === name) ?? false; const [transformFrequency, setTransformFrequency] = useState(defaults.transformFrequency); const isTransformFrequencyValid = transformFrequencyValidator(transformFrequency); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx index 80203af34e105..b3b11a6e4764e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_time_field.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_time_field.tsx index d750bf6c7e1fd..76ce803bdb415 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_time_field.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_time_field.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/switch_modal.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/switch_modal.tsx index 2d2a5b1fcad93..cbc53383541ed 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/switch_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/switch_modal.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { i18n } from '@kbn/i18n'; import { EuiConfirmModal } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx index 103b8378cef7f..2ffb8437d4861 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index a5c674a70745c..6ee4d5c812f73 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -5,25 +5,16 @@ * 2.0. */ -import React, { useEffect, FC } from 'react'; +import React, { FC, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; - import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiButtonEmpty, - EuiCallOut, - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, - EuiSpacer, -} from '@elastic/eui'; - -import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; +import { EuiButtonEmpty, EuiCallOut, EuiPageTemplate, EuiSpacer } from '@elastic/eui'; import { useDocumentationLinks } from '../../hooks/use_documentation_links'; import { useSearchItems } from '../../hooks/use_search_items'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { PrivilegesWrapper } from '../../lib/authorization'; +import { CapabilitiesWrapper } from '../../components/capabilities_wrapper'; import { Wizard } from './components/wizard'; @@ -54,8 +45,15 @@ export const CreateTransformSection: FC = ({ match }) => { ); return ( - - + = ({ match }) => { } rightSideItems={[docsLink]} bottomBorder + paddingSize={'none'} /> - + {searchItemsError !== undefined && ( <> @@ -76,7 +75,7 @@ export const CreateTransformSection: FC = ({ match }) => { )} {searchItems !== undefined && } - - + + ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap deleted file mode 100644 index 8348f93b32140..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Minimal initialization 1`] = ` - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_action_name.tsx index e6f3eeb4a9064..21c33361e398b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_action_name.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { createCapabilityFailureMessage } from '../../../../lib/authorization'; +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; export const cloneActionNameText = i18n.translate( 'xpack.transform.transformList.cloneActionNameText', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx index 35d5e9f05c315..d78211e19bdb4 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import React, { useCallback, useContext, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { AuthorizationContext } from '../../../../lib/authorization'; import { TransformListAction, TransformListRow } from '../../../../common'; import { SECTION_SLUG } from '../../../../common/constants'; -import { useSearchItems } from '../../../../hooks/use_search_items'; +import { useTransformCapabilities, useSearchItems } from '../../../../hooks'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { cloneActionNameText, CloneActionName } from './clone_action_name'; @@ -26,7 +25,7 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) => const { getDataViewIdByTitle, loadDataViews } = useSearchItems(undefined); - const { canCreateTransform } = useContext(AuthorizationContext).capabilities; + const { canCreateTransform } = useTransformCapabilities(); const clickHandler = useCallback( async (item: TransformListRow) => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/create_alert_rule_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/create_alert_rule_action_name.tsx index c8d67a86d579a..fec35288d80b5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/create_alert_rule_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/create_alert_rule_action_name.tsx @@ -5,11 +5,13 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { createCapabilityFailureMessage } from '../../../../lib/authorization'; + +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; interface CreateAlertRuleActionProps { disabled: boolean; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/use_create_alert_rule_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/use_create_alert_rule_action.tsx index 070f1eb08ac60..af75222fa85e7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/use_create_alert_rule_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/use_create_alert_rule_action.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useCallback, useContext, useMemo } from 'react'; -import { AuthorizationContext } from '../../../../lib/authorization'; +import React, { useCallback, useMemo } from 'react'; +import { useTransformCapabilities } from '../../../../hooks'; import { TransformListAction, TransformListRow } from '../../../../common'; import { crateAlertRuleActionNameText, @@ -17,7 +17,7 @@ import { isContinuousTransform } from '../../../../../../common/types/transform' export type CreateAlertRuleAction = ReturnType; export const useCreateAlertRuleAction = (forceDisable: boolean) => { - const { canCreateTransformAlerts } = useContext(AuthorizationContext).capabilities; + const { canCreateTransformAlerts } = useTransformCapabilities(); const { setCreateAlertRule } = useAlertRuleFlyout(); const clickHandler = useCallback( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_action_name.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_action_name.test.tsx.snap deleted file mode 100644 index fe1e813ca9843..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_action_name.test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Actions Minimal initialization 1`] = ` - - Delete - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.test.tsx index 1fb0d69682cda..37daeeb75e138 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { DeleteActionName, DeleteActionNameProps } from './delete_action_name'; @@ -21,7 +21,7 @@ describe('Transform: Transform List Actions ', () => { isBulkAction: false, }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + const { container } = render(); + expect(container.textContent).toBe('Delete'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx index fa1142938efe7..93e346e6c7a1d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx @@ -5,11 +5,15 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { i18n } from '@kbn/i18n'; + import { EuiToolTip } from '@elastic/eui'; + import { TransformState, TRANSFORM_STATE } from '../../../../../../common/constants'; -import { createCapabilityFailureMessage } from '../../../../lib/authorization'; +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; + import { TransformListRow } from '../../../../common'; export const deleteActionNameText = i18n.translate( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx index 357809b54746b..5996e271604e3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx @@ -5,13 +5,16 @@ * 2.0. */ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { TransformListAction, TransformListRow } from '../../../../common'; -import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks'; -import { AuthorizationContext } from '../../../../lib/authorization'; +import { + useDeleteIndexAndTargetIndex, + useDeleteTransforms, + useTransformCapabilities, +} from '../../../../hooks'; import { deleteActionNameText, @@ -21,7 +24,7 @@ import { export type DeleteAction = ReturnType; export const useDeleteAction = (forceDisable: boolean) => { - const { canDeleteTransform } = useContext(AuthorizationContext).capabilities; + const { canDeleteTransform } = useTransformCapabilities(); const deleteTransforms = useDeleteTransforms(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.tsx index f7cc72c2236b0..c3d359e648c28 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_action_name.tsx index 3bf5ee8e611f6..7fc16ebfff278 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_action_name.tsx @@ -5,16 +5,15 @@ * 2.0. */ -import React, { useContext, FC } from 'react'; +import React, { type FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { - createCapabilityFailureMessage, - AuthorizationContext, -} from '../../../../lib/authorization'; +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; + +import { useTransformCapabilities } from '../../../../hooks'; export const editActionNameText = i18n.translate( 'xpack.transform.transformList.editActionNameText', @@ -24,7 +23,7 @@ export const editActionNameText = i18n.translate( ); export const EditActionName: FC = () => { - const { canCreateTransform } = useContext(AuthorizationContext).capabilities; + const { canCreateTransform } = useTransformCapabilities(); if (!canCreateTransform) { return ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx index 19d60b8b64381..40e7334197b0c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import React, { useCallback, useContext, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { TransformListAction, TransformListRow } from '../../../../common'; -import { AuthorizationContext } from '../../../../lib/authorization'; +import { useTransformCapabilities } from '../../../../hooks'; import { editActionNameText, EditActionName } from './edit_action_name'; import { useSearchItems } from '../../../../hooks/use_search_items'; @@ -19,7 +19,7 @@ import { TransformConfigUnion } from '../../../../../../common/types/transform'; export type EditAction = ReturnType; export const useEditAction = (forceDisable: boolean, transformNodes: number) => { - const { canCreateTransform } = useContext(AuthorizationContext).capabilities; + const { canCreateTransform } = useTransformCapabilities(); const [config, setConfig] = useState(); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/reauthorize_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/reauthorize_action_modal.tsx index 9e848fdd8323e..edfde117abbd7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/reauthorize_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/reauthorize_action_modal.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { i18n } from '@kbn/i18n'; import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal } from '@elastic/eui'; import type { ReauthorizeAction } from './use_reauthorize_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/reauthorize_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/reauthorize_action_name.tsx index e07ae03ec46b0..ee883a3e7e77b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/reauthorize_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/reauthorize_action_name.tsx @@ -5,14 +5,16 @@ * 2.0. */ -import React, { FC, useContext } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { type FC } from 'react'; + import { EuiToolTip, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; + import { needsReauthorization } from '../../../../common/reauthorization_utils'; -import { - AuthorizationContext, - createCapabilityFailureMessage, -} from '../../../../lib/authorization'; +import { useTransformCapabilities } from '../../../../hooks'; import { TransformListRow } from '../../../../common'; export const reauthorizeActionNameText = i18n.translate( @@ -45,7 +47,7 @@ export const ReauthorizeActionName: FC = ({ forceDisable, transformNodes, }) => { - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const { canStartStopTransform } = useTransformCapabilities(); // Disable start for batch transforms which have completed. const someNeedsReauthorization = items.some(needsReauthorization); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/use_reauthorize_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/use_reauthorize_action.tsx index 086f5451d53be..67e618765e42e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/use_reauthorize_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reauthorize/use_reauthorize_action.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { sortTransformsToReauthorize } from './sort_transforms_to_reauthorize'; import { needsReauthorization } from '../../../../common/reauthorization_utils'; @@ -17,11 +17,11 @@ import { } from './reauthorize_action_name'; import { TransformListAction, TransformListRow } from '../../../../common'; -import { AuthorizationContext } from '../../../../lib/authorization'; +import { useTransformCapabilities } from '../../../../hooks'; export type ReauthorizeAction = ReturnType; export const useReauthorizeAction = (forceDisable: boolean, transformNodes: number) => { - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const { canStartStopTransform } = useTransformCapabilities(); const reauthorizeTransforms = useReauthorizeTransforms(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx index c6ea0f5f7270d..6d5f56d3e7297 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx @@ -5,11 +5,15 @@ * 2.0. */ -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { type FC } from 'react'; + import { EuiToolTip } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + import { TransformState, TRANSFORM_STATE } from '../../../../../../common/constants'; -import { createCapabilityFailureMessage } from '../../../../lib/authorization'; +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; + import { TransformListRow } from '../../../../common'; export const resetActionNameText = i18n.translate( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/use_reset_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/use_reset_action.tsx index 70164bc22a63c..23a399fdb9086 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/use_reset_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/use_reset_action.tsx @@ -5,19 +5,18 @@ * 2.0. */ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { TransformListAction, TransformListRow } from '../../../../common'; -import { useResetTransforms } from '../../../../hooks'; -import { AuthorizationContext } from '../../../../lib/authorization'; +import { useTransformCapabilities, useResetTransforms } from '../../../../hooks'; import { resetActionNameText, isResetActionDisabled, ResetActionName } from './reset_action_name'; export type ResetAction = ReturnType; export const useResetAction = (forceDisable: boolean) => { - const { canResetTransform } = useContext(AuthorizationContext).capabilities; + const { canResetTransform } = useTransformCapabilities(); const resetTransforms = useResetTransforms(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_schedule_now/schedule_now_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_schedule_now/schedule_now_action_name.tsx index 71b6a055c0d94..0c3be1cdad70b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_schedule_now/schedule_now_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_schedule_now/schedule_now_action_name.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import React, { FC, useContext } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { type FC } from 'react'; + import { EuiToolTip } from '@elastic/eui'; -import { - createCapabilityFailureMessage, - AuthorizationContext, -} from '../../../../lib/authorization'; +import { i18n } from '@kbn/i18n'; + +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; + +import { useTransformCapabilities } from '../../../../hooks'; import { TransformListRow, isCompletedBatchTransform } from '../../../../common'; export const scheduleNowActionNameText = i18n.translate( @@ -48,7 +49,7 @@ export const ScheduleNowActionName: FC = ({ forceDisable, transformNodes, }) => { - const { canScheduleNowTransform } = useContext(AuthorizationContext).capabilities; + const { canScheduleNowTransform } = useTransformCapabilities(); const isBulkAction = items.length > 1; // Disable schedule-now for batch transforms which have completed. diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_schedule_now/use_schedule_now_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_schedule_now/use_schedule_now_action.tsx index dda70deb225a6..a13d3da89f677 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_schedule_now/use_schedule_now_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_schedule_now/use_schedule_now_action.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React, { useContext, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; -import { AuthorizationContext } from '../../../../lib/authorization'; +import { useTransformCapabilities } from '../../../../hooks'; import { TransformListAction, TransformListRow } from '../../../../common'; import { useScheduleNowTransforms } from '../../../../hooks'; @@ -21,8 +21,7 @@ import { export type ScheduleNowAction = ReturnType; export const useScheduleNowAction = (forceDisable: boolean, transformNodes: number) => { - const { canScheduleNowTransform } = useContext(AuthorizationContext).capabilities; - + const { canScheduleNowTransform } = useTransformCapabilities(); const scheduleNowTransforms = useScheduleNowTransforms(); const action: TransformListAction = useMemo( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_action_name.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_action_name.test.tsx.snap deleted file mode 100644 index 20b0691b55bf9..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_action_name.test.tsx.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Actions Minimal initialization 1`] = ` - - Start - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx index 7ad6897034e10..ab98f36222957 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { i18n } from '@kbn/i18n'; import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { StartAction } from './use_start_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx index ba4619b022620..6ef0a995186c7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { TransformListRow } from '../../../../common'; import { StartActionName, StartActionNameProps } from './start_action_name'; @@ -16,6 +17,8 @@ import transformListRow from '../../../../common/__mocks__/transform_list_row.js jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); +const queryClient = new QueryClient(); + describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { // @ts-expect-error mock data is too loosely typed @@ -26,8 +29,11 @@ describe('Transform: Transform List Actions ', () => { transformNodes: 1, }; - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); + const { container } = render( + + + + ); + expect(container.textContent).toBe('Start'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx index 844d9755d7073..c50c83d25edc5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx @@ -5,16 +5,14 @@ * 2.0. */ -import React, { FC, useContext } from 'react'; +import React, { type FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; -import { - createCapabilityFailureMessage, - AuthorizationContext, -} from '../../../../lib/authorization'; +import { useTransformCapabilities } from '../../../../hooks'; import { TransformListRow, isCompletedBatchTransform } from '../../../../common'; export const startActionNameText = i18n.translate( @@ -55,7 +53,7 @@ export const StartActionName: FC = ({ forceDisable, transformNodes, }) => { - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const { canStartStopTransform } = useTransformCapabilities(); const isBulkAction = items.length > 1; // Disable start for batch transforms which have completed. diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx index 20910cf5fa0a5..168174730b706 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx @@ -5,19 +5,18 @@ * 2.0. */ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; -import { AuthorizationContext } from '../../../../lib/authorization'; import { TransformListAction, TransformListRow } from '../../../../common'; -import { useStartTransforms } from '../../../../hooks'; +import { useTransformCapabilities, useStartTransforms } from '../../../../hooks'; import { isStartActionDisabled, startActionNameText, StartActionName } from './start_action_name'; export type StartAction = ReturnType; export const useStartAction = (forceDisable: boolean, transformNodes: number) => { - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const { canStartStopTransform } = useTransformCapabilities(); const startTransforms = useStartTransforms(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_action_name.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_action_name.test.tsx.snap deleted file mode 100644 index fd97412fa1875..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_action_name.test.tsx.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Actions Minimal initialization 1`] = ` - - Stop - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.test.tsx index 9496dbd82c70d..e32bf043a2221 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.test.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { TransformListRow } from '../../../../common'; import { StopActionName, StopActionNameProps } from './stop_action_name'; @@ -16,6 +17,8 @@ import transformListRow from '../../../../common/__mocks__/transform_list_row.js jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); +const queryClient = new QueryClient(); + describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { // @ts-expect-error mock data is too loosely typed @@ -25,8 +28,11 @@ describe('Transform: Transform List Actions ', () => { items: [item], }; - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); + const { container } = render( + + + + ); + expect(container.textContent).toBe('Stop'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx index 1c729555f7df3..e5bc1425cdd99 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx @@ -5,17 +5,15 @@ * 2.0. */ -import React, { FC, useContext } from 'react'; +import React, { type FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; import { TransformListRow } from '../../../../common'; -import { - createCapabilityFailureMessage, - AuthorizationContext, -} from '../../../../lib/authorization'; +import { useTransformCapabilities } from '../../../../hooks'; export const stopActionNameText = i18n.translate( 'xpack.transform.transformList.stopActionNameText', @@ -43,7 +41,7 @@ export interface StopActionNameProps { } export const StopActionName: FC = ({ items, forceDisable }) => { const isBulkAction = items.length > 1; - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const { canStartStopTransform } = useTransformCapabilities(); // Disable stop action if one of the transforms is stopped already const stoppedTransform = items.some( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx index ac53ee83f6f65..e410704341177 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx @@ -5,32 +5,28 @@ * 2.0. */ -import React, { useCallback, useContext, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; -import { AuthorizationContext } from '../../../../lib/authorization'; import { TransformListAction, TransformListRow } from '../../../../common'; -import { useStopTransforms } from '../../../../hooks'; +import { useTransformCapabilities, useStopTransforms } from '../../../../hooks'; import { isStopActionDisabled, stopActionNameText, StopActionName } from './stop_action_name'; import { isManagedTransform } from '../../../../common/managed_transforms_utils'; export type StopAction = ReturnType; export const useStopAction = (forceDisable: boolean) => { - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; - + const { canStartStopTransform } = useTransformCapabilities(); const stopTransforms = useStopTransforms(); const [isModalVisible, setModalVisible] = useState(false); const [items, setItems] = useState([]); const closeModal = () => setModalVisible(false); - const openModal = (newItems: TransformListRow[]) => { if (Array.isArray(newItems)) { setItems(newItems); setModalVisible(true); } }; - const stopAndCloseModal = useCallback( (transformSelection: TransformListRow[]) => { setModalVisible(false); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/__snapshots__/create_transform_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/__snapshots__/create_transform_button.test.tsx.snap deleted file mode 100644 index a529ef04f9230..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/__snapshots__/create_transform_button.test.tsx.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Minimal initialization 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx index 0a7324fd09ffc..da84ef2faa8e3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx @@ -5,17 +5,23 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { CreateTransformButton } from './create_transform_button'; jest.mock('../../../../../shared_imports'); +const queryClient = new QueryClient(); + describe('Transform: Transform List ', () => { test('Minimal initialization', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); + const { container } = render( + + + + ); + expect(container.textContent).toBe('Create a transform'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx index 14697f9af4080..9c1fa4be8c101 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx @@ -5,16 +5,15 @@ * 2.0. */ -import React, { useContext, FC, MouseEventHandler } from 'react'; +import React, { type FC, type MouseEventHandler } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - createCapabilityFailureMessage, - AuthorizationContext, -} from '../../../../lib/authorization'; +import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message'; + +import { useTransformCapabilities } from '../../../../hooks'; interface CreateTransformButtonProps { onClick: MouseEventHandler; @@ -25,7 +24,7 @@ export const CreateTransformButton: FC = ({ onClick, transformNodes, }) => { - const { capabilities } = useContext(AuthorizationContext); + const capabilities = useTransformCapabilities(); const disabled = !capabilities.canCreateTransform || diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_callout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_callout.tsx index 75f333960fa54..cdaabb3a3b200 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_callout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index a337d73ca54b3..d9310762ef3e0 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiFieldText, EuiFormRow } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx index b5bb7f3fb258f..519bdc94011e1 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx @@ -5,15 +5,13 @@ * 2.0. */ -import React, { useEffect, useState, type FC } from 'react'; +import React, { type FC } from 'react'; import { useEuiTheme, EuiComboBox, EuiFormRow, EuiSkeletonRectangle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEsIngestPipelines } from '../../../../../../common/api_schemas/type_guards'; - -import { useApi } from '../../../../hooks/use_api'; +import { useGetEsIngestPipelines } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; import { useEditTransformFlyout } from './use_edit_transform_flyout'; @@ -30,35 +28,8 @@ export const EditTransformIngestPipeline: FC = () => { const { errorMessages, value } = useEditTransformFlyout('destinationIngestPipeline'); const { formField } = useEditTransformFlyout('actions'); - const api = useApi(); - - const [ingestPipelineNames, setIngestPipelineNames] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(function fetchPipelinesOnMount() { - let unmounted = false; - - async function getIngestPipelineNames() { - try { - const ingestPipelines = await api.getEsIngestPipelines(); - - if (!unmounted && isEsIngestPipelines(ingestPipelines)) { - setIngestPipelineNames(ingestPipelines.map(({ name }) => name)); - } - } finally { - if (!unmounted) { - setIsLoading(false); - } - } - } - - getIngestPipelineNames(); - - return () => { - unmounted = true; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const { data: esIngestPipelinesData, isLoading } = useGetEsIngestPipelines(); + const ingestPipelineNames = esIngestPipelinesData?.map(({ name }) => name) ?? []; return ( <> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index 06e30d584de46..b55b6f90a0aa3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -11,12 +11,9 @@ import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isPostTransformsUpdateResponseSchema } from '../../../../../../common/api_schemas/type_guards'; import { getErrorMessage } from '../../../../../../common/utils/errors'; -import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common'; -import { useToastNotifications } from '../../../../app_dependencies'; -import { useApi } from '../../../../hooks/use_api'; +import { useUpdateTransform } from '../../../../hooks'; import { useEditTransformFlyout } from './use_edit_transform_flyout'; @@ -25,33 +22,20 @@ interface EditTransformUpdateButtonProps { } export const EditTransformUpdateButton: FC = ({ closeFlyout }) => { - const api = useApi(); - const toastNotifications = useToastNotifications(); - const requestConfig = useEditTransformFlyout('requestConfig'); const isUpdateButtonDisabled = useEditTransformFlyout('isUpdateButtonDisabled'); const config = useEditTransformFlyout('config'); const { apiError } = useEditTransformFlyout('actions'); + const updateTransfrom = useUpdateTransform(config.id, requestConfig); + async function submitFormHandler() { apiError(undefined); - const transformId = config.id; - - const resp = await api.updateTransform(transformId, requestConfig); - - if (!isPostTransformsUpdateResponseSchema(resp)) { - apiError(getErrorMessage(resp)); - return; - } - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.editTransformSuccessMessage', { - defaultMessage: 'Transform {transformId} updated.', - values: { transformId }, - }) - ); - closeFlyout(); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); + updateTransfrom(undefined, { + onError: (error) => apiError(getErrorMessage(error)), + onSuccess: () => closeFlyout(), + }); } return ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx index d0ea13ec11df1..efd4fb1d500af 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; 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 5647060ee7a72..3f2bd7fb70c0c 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 @@ -8,7 +8,8 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; import { useAppDependencies } from '../../../../app_dependencies'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stat.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stat.tsx index 810d60f3b8fd7..cfd0d112ba90b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stat.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stat.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; export interface StatsBarStat { label: string; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stats_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stats_bar.tsx index 21674ae24301c..849c651b37ea0 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stats_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stats_bar.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; + import { Stat, StatsBarStat } from './stat'; interface Stats { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap deleted file mode 100644 index 39964399f66db..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap +++ /dev/null @@ -1,66 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Job List Expanded Row Minimal initialization 1`] = ` -
- - - -
- - - -
-`; - -exports[`Transform: Job List Expanded Row
Minimal initialization 1`] = ` - - - - the-section-title - - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap deleted file mode 100644 index 19a5055954d84..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap +++ /dev/null @@ -1,66 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Expanded Row Minimal initialization 1`] = ` -
- - - - - { - "id": "fq_date_histogram_1m_1441", - "source": { - "index": [ - "farequote-2019" - ], - "query": { - "match_all": {} - } - }, - "dest": { - "index": "fq_date_histogram_1m_1441" - }, - "pivot": { - "group_by": { - "@timestamp": { - "date_histogram": { - "field": "@timestamp", - "calendar_interval": "1m" - } - } - }, - "aggregations": { - "responsetime.avg": { - "avg": { - "field": "responsetime" - } - } - } - }, - "version": "8.0.0", - "create_time": 1564388146667 -} - - - -   - - -
-`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap deleted file mode 100644 index 2e80f1deb8b2e..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Minimal initialization 1`] = ` - - - - - - Create your first transform - , - ] - } - data-test-subj="transformNoTransformsFound" - title={ -

- No transforms found -

- } - /> -
-
-
-`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.test.tsx index a4c8a497d2202..ad375d47ce4cc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { ExpandedRowDetailsPane, Section, SectionConfig } from './expanded_row_details_pane'; @@ -23,16 +23,20 @@ const section: SectionConfig = { describe('Transform: Job List Expanded Row ', () => { test('Minimal initialization', () => { - const wrapper = shallow(); + const { container } = render(); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-section-title'); + expect(container.textContent).toContain('the-item-title'); + expect(container.textContent).toContain('the-item-description'); }); }); describe('Transform: Job List Expanded Row
', () => { test('Minimal initialization', () => { - const wrapper = shallow(
); + const { container } = render(
); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('the-section-title'); + expect(container.textContent).toContain('the-item-title'); + expect(container.textContent).toContain('the-item-description'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.test.tsx index 7b1a5de79fae5..ff69aab3aa3eb 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; @@ -14,8 +14,7 @@ import { ExpandedRowJsonPane } from './expanded_row_json_pane'; describe('Transform: Transform List Expanded Row ', () => { test('Minimal initialization', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); + const { container } = render(); + expect(container.textContent).toContain(JSON.stringify(transformListRow.config, null, 2)); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx index c68a57ea12109..48e17d9157226 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx index 4cf387ad2973b..2ec73e4485dd1 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx @@ -15,17 +15,15 @@ import { EuiToolTip, EuiButtonIcon, } from '@elastic/eui'; -import { euiLightVars as theme } from '@kbn/ui-theme'; +import { euiLightVars as theme } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { DEFAULT_MAX_AUDIT_MESSAGE_SIZE, TIME_FORMAT } from '../../../../../../common/constants'; -import { isGetTransformsAuditMessagesResponseSchema } from '../../../../../../common/api_schemas/type_guards'; import { TransformMessage } from '../../../../../../common/types/messages'; -import { useApi } from '../../../../hooks/use_api'; import { JobIcon } from '../../../../components/job_icon'; -import { useRefreshTransformList } from '../../../../common'; +import { useGetTransformAuditMessages, useRefreshTransformList } from '../../../../hooks'; interface ExpandedRowMessagesPaneProps { transformId: string; @@ -37,12 +35,6 @@ interface Sorting { } export const ExpandedRowMessagesPane: FC = ({ transformId }) => { - const [messages, setMessages] = useState([]); - const [msgCount, setMsgCount] = useState(0); - - const [isLoading, setIsLoading] = useState(false); - const [errorMessage, setErrorMessage] = useState(''); - const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [sorting, setSorting] = useState<{ sort: Sorting }>({ @@ -52,58 +44,24 @@ export const ExpandedRowMessagesPane: FC = ({ tran }, }); - const api = useApi(); - - const getMessagesFactory = ( - sortField: keyof TransformMessage = 'timestamp', - sortDirection: 'asc' | 'desc' = 'desc' - ) => { - let concurrentLoads = 0; - - return async function getMessages() { - concurrentLoads++; - - if (concurrentLoads > 1) { - return; - } - - setIsLoading(true); - const messagesResp = await api.getTransformAuditMessages( - transformId, - sortField, - sortDirection - ); - - if (!isGetTransformsAuditMessagesResponseSchema(messagesResp)) { - setIsLoading(false); - setErrorMessage( - i18n.translate( - 'xpack.transform.transformList.transformDetails.messagesPane.errorMessage', - { - defaultMessage: 'Messages could not be loaded', - } - ) - ); - return; - } - - setIsLoading(false); - setMessages(messagesResp.messages); - setMsgCount(messagesResp.total); - - concurrentLoads--; - - if (concurrentLoads > 0) { - concurrentLoads = 0; - getMessages(); - } - }; - }; - const { refresh: refreshMessage } = useRefreshTransformList({ onRefresh: getMessagesFactory() }); + const { + isLoading, + error, + data = { messages: [], total: 0 }, + } = useGetTransformAuditMessages(transformId, sorting.sort.field, sorting.sort.direction); + const { messages, total } = data; + const errorMessage = + error !== null + ? i18n.translate('xpack.transform.transformList.transformDetails.messagesPane.errorMessage', { + defaultMessage: 'Messages could not be loaded', + }) + : ''; + + const refreshTransformList = useRefreshTransformList(); const columns = [ { - name: refreshMessage ? ( + name: refreshTransformList ? ( = ({ tran // TODO: Replace this with ML's blurButtonOnClick when it's moved to a shared package onClick={(e: MouseEvent) => { (e.currentTarget as HTMLButtonElement).blur(); - refreshMessage(); + refreshTransformList(); }} iconType="refresh" aria-label={i18n.translate('xpack.transform.transformList.refreshAriaLabel', { @@ -162,7 +120,7 @@ export const ExpandedRowMessagesPane: FC = ({ tran const getPageOfMessages = ({ index, size }: { index: number; size: number }) => { let list = messages; - if (msgCount <= DEFAULT_MAX_AUDIT_MESSAGE_SIZE) { + if (total <= DEFAULT_MAX_AUDIT_MESSAGE_SIZE) { const sortField = sorting.sort.field ?? 'timestamp'; list = messages.sort((a: TransformMessage, b: TransformMessage) => { const prev = a[sortField] as any; @@ -192,12 +150,6 @@ export const ExpandedRowMessagesPane: FC = ({ tran setPageSize(size); if (sort) { setSorting({ sort }); - - // Since we only show 500 messages, if user wants oldest messages first - // we need to make sure we fetch them from elasticsearch - if (msgCount > DEFAULT_MAX_AUDIT_MESSAGE_SIZE) { - getMessagesFactory(sort.field, sort.direction)(); - } } }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx index 430fa9478f60a..fc4bf0e24d14d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx @@ -5,25 +5,48 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider, type UseQueryResult } from '@tanstack/react-query'; +import * as ReactQuery from '@tanstack/react-query'; + +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { TransformList } from './transform_list'; +const useQueryMock = jest.spyOn(ReactQuery, 'useQuery').mockImplementation((queryKey) => { + switch (queryKey[0]) { + case 'transform.data_view_exists': + return { error: null, data: true } as UseQueryResult; + } + + return { error: null, data: undefined } as UseQueryResult; +}); + +const queryClient = new QueryClient(); + jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); describe('Transform: Transform List ', () => { - test('Minimal initialization', () => { - const wrapper = shallow( - + test('Minimal initialization', async () => { + const { container } = render( + + + + + ); - expect(wrapper).toMatchSnapshot(); + await waitFor(() => { + expect(useQueryMock).toHaveBeenCalledTimes(4); + expect(container.textContent).toContain('Create your first transform'); + }); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index e6c279e56f3c3..2e0106afc0e92 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -5,25 +5,21 @@ * 2.0. */ -import React, { MouseEventHandler, FC, useContext, useState } from 'react'; +import React, { type FC, type MouseEventHandler, useState } from 'react'; import { i18n } from '@kbn/i18n'; - import { EuiButton, EuiButtonEmpty, EuiButtonIcon, - EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, - EuiPageContent_Deprecated as EuiPageContent, - EuiPopover, - EuiSpacer, - EuiTitle, EuiInMemoryTable, + EuiPageTemplate, + EuiPopover, EuiSearchBarProps, + EuiTitle, } from '@elastic/eui'; - import { isReauthorizeActionDisabled, ReauthorizeActionModal, @@ -32,43 +28,38 @@ import { } from '../action_reauthorize'; import type { TransformId } from '../../../../../../common/types/transform'; -import { - useRefreshTransformList, - TransformListRow, - TRANSFORM_LIST_COLUMN, -} from '../../../../common'; -import { AuthorizationContext } from '../../../../lib/authorization'; +import { type TransformListRow, TRANSFORM_LIST_COLUMN } from '../../../../common'; +import { useRefreshTransformList, useTransformCapabilities } from '../../../../hooks'; import { CreateTransformButton } from '../create_transform_button'; import { RefreshTransformListButton } from '../refresh_transform_list_button'; import { + DeleteActionModal, + DeleteActionName, isDeleteActionDisabled, useDeleteAction, - DeleteActionName, - DeleteActionModal, } from '../action_delete'; import { isResetActionDisabled, - useResetAction, - ResetActionName, ResetActionModal, + ResetActionName, + useResetAction, } from '../action_reset'; import { isStartActionDisabled, - useStartAction, - StartActionName, StartActionModal, + StartActionName, + useStartAction, } from '../action_start'; import { isScheduleNowActionDisabled, - useScheduleNowAction, ScheduleNowActionName, + useScheduleNowAction, } from '../action_schedule_now'; import { isStopActionDisabled, StopActionName, useStopAction } from '../action_stop'; - import { useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; -import { transformFilters, filterTransforms } from './transform_search_bar_filters'; +import { filterTransforms, transformFilters } from './transform_search_bar_filters'; import { useTableSettings } from './use_table_settings'; import { useAlertRuleFlyout } from '../../../../../alerting/transform_alerting_flyout'; import { TransformHealthAlertRule } from '../../../../../../common/types/alerting'; @@ -91,6 +82,7 @@ function getItemIdToExpandedRowMap( } interface TransformListProps { + isLoading: boolean; onCreateTransform: MouseEventHandler; transformNodes: number; transforms: TransformListRow[]; @@ -98,13 +90,13 @@ interface TransformListProps { } export const TransformList: FC = ({ + isLoading, onCreateTransform, transformNodes, transforms, transformsLoading, }) => { - const [isLoading, setIsLoading] = useState(false); - const { refresh } = useRefreshTransformList({ isLoading: setIsLoading }); + const refreshTransformList = useRefreshTransformList(); const { setEditAlertRule } = useAlertRuleFlyout(); const [query, setQuery] = useState>[0]>(); @@ -119,7 +111,7 @@ export const TransformList: FC = ({ const bulkStopAction = useStopAction(false); const bulkScheduleNowAction = useScheduleNowAction(false, transformNodes); - const { capabilities } = useContext(AuthorizationContext); + const capabilities = useTransformCapabilities(); const disabled = !capabilities.canCreateTransform || !capabilities.canPreviewTransform || @@ -142,42 +134,32 @@ export const TransformList: FC = ({ const filteredTransforms = clauses.length > 0 ? filterTransforms(transforms, clauses) : transforms; - if (transforms.length === 0 && transformNodes === 0) { - return null; - } - if (transforms.length === 0) { return ( - - - - - - {i18n.translate('xpack.transform.list.emptyPromptTitle', { - defaultMessage: 'No transforms found', - })} - - } - actions={[ - - {i18n.translate('xpack.transform.list.emptyPromptButtonText', { - defaultMessage: 'Create your first transform', - })} - , - ]} - data-test-subj="transformNoTransformsFound" - /> - - - + + {i18n.translate('xpack.transform.list.emptyPromptTitle', { + defaultMessage: 'No transforms found', + })} + + } + actions={[ + + {i18n.translate('xpack.transform.list.emptyPromptButtonText', { + defaultMessage: 'Create your first transform', + })} + , + ]} + data-test-subj="transformNoTransformsFound" + /> ); } @@ -319,7 +301,7 @@ export const TransformList: FC = ({ const toolsRight = ( - + diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx index 8ad53c64c2f1e..a041466eb5434 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; -import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -16,7 +16,7 @@ import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/consta import { TransformListRow } from '../../../../common'; -import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; +import { useDocumentationLinks, useRefreshTransformList } from '../../../../hooks'; import { StatsBar, TransformStatsBarStats } from '../stats_bar'; @@ -109,6 +109,7 @@ export const TransformStatsBar: FC = ({ transformNodes, transformsList, }) => { + const refreshTransformList = useRefreshTransformList(); const { esNodeRoles } = useDocumentationLinks(); const transformStats: TransformStatsBarStats = createTranformStats( @@ -118,10 +119,8 @@ export const TransformStatsBar: FC = ({ return ( <> - {transformNodes === 0 && ( <> - = ({ }} />

+ refreshTransformList()} size="s"> + +
+ )} + ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx index d620a60e7a861..34b69e45f087a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import React, { type FC } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react-hooks'; jest.mock('../../../../../shared_imports'); @@ -14,8 +16,13 @@ import { useActions } from './use_actions'; describe('Transform: Transform List Actions', () => { test('useActions()', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useActions({ forceDisable: false, transformNodes: 1 }) + const queryClient = new QueryClient(); + const wrapper: FC = ({ children }) => ( + {children} + ); + const { result, waitForNextUpdate } = renderHook( + () => useActions({ forceDisable: false, transformNodes: 1 }), + { wrapper } ); await waitForNextUpdate(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx index 521bd745e8692..87f550e433bc2 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import React, { type FC } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react-hooks'; import { useColumns } from './use_columns'; @@ -14,7 +16,13 @@ jest.mock('../../../../app_dependencies'); describe('Transform: Job List Columns', () => { test('useColumns()', async () => { - const { result, waitForNextUpdate } = renderHook(() => useColumns([], () => {}, 1, [])); + const queryClient = new QueryClient(); + const wrapper: FC = ({ children }) => ( + {children} + ); + const { result, waitForNextUpdate } = renderHook(() => useColumns([], () => {}, 1, []), { + wrapper, + }); await waitForNextUpdate(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index db77b3e306da2..2ae8edf30a1de 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useContext } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -24,7 +24,7 @@ import { EuiIcon, } from '@elastic/eui'; -import { AuthorizationContext } from '../../../../lib/authorization'; +import { useTransformCapabilities } from '../../../../hooks'; import { needsReauthorization } from '../../../../common/reauthorization_utils'; import { isLatestTransform, @@ -52,7 +52,7 @@ export const useColumns = ( transformNodes: number, transformSelection: TransformListRow[] ) => { - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const { canStartStopTransform } = useTransformCapabilities(); const { actions, modals } = useActions({ forceDisable: transformSelection.length > 0, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts deleted file mode 100644 index c1f4c3d4f7d5c..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.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 React, { useEffect } from 'react'; - -import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; - -import { useRefreshTransformList } from '../../../../common'; - -export const useRefreshInterval = ( - setBlockRefresh: React.Dispatch> -) => { - const { refresh } = useRefreshTransformList(); - useEffect(() => { - const interval = setInterval(refresh, DEFAULT_REFRESH_INTERVAL_MS); - - // useEffect cleanup - return () => { - clearInterval(interval); - }; - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // [] as comparator makes sure this only runs once -}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx index 3e4cee609c75b..3782bc9e9dfb3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx @@ -5,17 +5,25 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { TransformManagementSection } from './transform_management_section'; jest.mock('../../../shared_imports'); +jest.mock('../../services/navigation'); + +const queryClient = new QueryClient(); describe('Transform: ', () => { test('Minimal initialization', () => { - const wrapper = shallow(); + const { container } = render( + + + + ); - expect(wrapper).toMatchSnapshot(); + expect(container.textContent).toContain('Missing permission'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index 3c74cf450b4b9..8cfb7ebb5240c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -5,39 +5,37 @@ * 2.0. */ -import React, { FC, useContext, useEffect, useMemo, useState } from 'react'; - -import { FormattedMessage } from '@kbn/i18n-react'; +import React, { type FC, useEffect, useMemo, useState } from 'react'; import { + EuiButton, EuiButtonEmpty, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiSkeletonText, + EuiCallOut, EuiModal, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, + EuiPageTemplate, + EuiSkeletonText, EuiSpacer, - EuiCallOut, - EuiButton, } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; + import { needsReauthorization } from '../../common/reauthorization_utils'; -import { - APP_GET_TRANSFORM_CLUSTER_PRIVILEGES, - TRANSFORM_STATE, -} from '../../../../common/constants'; +import { TRANSFORM_STATE } from '../../../../common/constants'; -import { useRefreshTransformList, TransformListRow } from '../../common'; -import { useDocumentationLinks } from '../../hooks/use_documentation_links'; -import { useDeleteTransforms, useGetTransforms } from '../../hooks'; +import { + useDocumentationLinks, + useDeleteTransforms, + useTransformCapabilities, + useGetTransforms, + useGetTransformNodes, +} from '../../hooks'; import { RedirectToCreateTransform } from '../../common/navigation'; -import { AuthorizationContext, PrivilegesWrapper } from '../../lib/authorization'; +import { CapabilitiesWrapper } from '../../components/capabilities_wrapper'; +import { ToastNotificationText } from '../../components/toast_notification_text'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { useRefreshInterval } from './components/transform_list/use_refresh_interval'; import { SearchSelection } from './components/search_selection'; import { TransformList } from './components/transform_list'; import { TransformStatsBar } from './components/transform_list/transforms_stats_bar'; @@ -47,39 +45,52 @@ import { TransformAlertFlyoutWrapper, } from '../../../alerting/transform_alerting_flyout'; +const ErrorMessageCallout: FC<{ + text: JSX.Element; + errorMessage: IHttpFetchError | null; +}> = ({ text, errorMessage }) => { + return ( + <> + + + {text}{' '} + {errorMessage !== null && ( + + )} + + } + color="danger" + iconType="error" + /> + + ); +}; + export const TransformManagement: FC = () => { const { esTransform } = useDocumentationLinks(); - const [transformsLoading, setTransformsLoading] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [blockRefresh, setBlockRefresh] = useState(false); - const [transforms, setTransforms] = useState([]); - const [transformNodes, setTransformNodes] = useState(0); - const [errorMessage, setErrorMessage] = useState(undefined); - const [transformIdsWithoutConfig, setTransformIdsWithoutConfig] = useState< - string[] | undefined - >(); - const deleteTransforms = useDeleteTransforms(); - const getTransforms = useGetTransforms( - setTransforms, - setTransformNodes, - setErrorMessage, - setTransformIdsWithoutConfig, - setIsInitialized, - blockRefresh - ); + const { + isInitialLoading: transformNodesInitialLoading, + error: transformNodesErrorMessage, + data: transformNodesData = 0, + } = useGetTransformNodes(); + const transformNodes = transformNodesErrorMessage === null ? transformNodesData : 0; - // Subscribe to the refresh observable to trigger reloading the transform list. - useRefreshTransformList({ - isLoading: setTransformsLoading, - onRefresh: () => getTransforms(true), - }); - // Call useRefreshInterval() after the subscription above is set up. - useRefreshInterval(setBlockRefresh); + const { + isInitialLoading: transformsInitialLoading, + isLoading: transformsLoading, + error: transformsErrorMessage, + data: { transforms, transformIdsWithoutConfig }, + } = useGetTransforms({ enabled: !transformNodesInitialLoading && transformNodes > 0 }); - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const isInitialLoading = transformNodesInitialLoading || transformsInitialLoading; + + const { canStartStopTransform } = useTransformCapabilities(); const unauthorizedTransformsWarning = useMemo(() => { const unauthorizedCnt = transforms.filter((t) => needsReauthorization(t)).length; @@ -151,7 +162,7 @@ export const TransformManagement: FC = () => { return ( <> - { } rightSideItems={[docsLink]} bottomBorder + paddingSize={'none'} /> - - - - {!isInitialized && } - {isInitialized && ( + + {isInitialLoading && ( + <> + + + + )} + {!isInitialLoading && ( <> {unauthorizedTransformsWarning} + {transformNodesErrorMessage !== null && ( + + } + errorMessage={transformNodesErrorMessage} + /> + )} + {transformsErrorMessage !== null && ( + + } + errorMessage={transformsErrorMessage} + /> + )} + + - {typeof errorMessage !== 'undefined' && ( - - - - - - - - } - body={ -

-

{JSON.stringify(errorMessage)}
-

+ + + {transformIdsWithoutConfig ? ( + <> + +

+ +

+ + deleteTransforms( + // If transform task doesn't have any corresponding config + // we won't know what the destination index or data view would be + // and should be force deleted + { + transformsInfo: transformIdsWithoutConfig.map((id) => ({ + id, + state: TRANSFORM_STATE.FAILED, + })), + deleteDestIndex: false, + deleteDestDataView: false, + forceDelete: true, + } + ) } - actions={[]} - /> -
-
-
- )} - {typeof errorMessage === 'undefined' && ( - - {transformIdsWithoutConfig ? ( - <> - -

- -

- { - await deleteTransforms( - // If transform task doesn't have any corresponding config - // we won't know what the destination index or data view would be - // and should be force deleted - { - transformsInfo: transformIdsWithoutConfig.map((id) => ({ - id, - state: TRANSFORM_STATE.FAILED, - })), - deleteDestIndex: false, - deleteDestDataView: false, - forceDelete: true, - } - ); + > + - - -
- - - ) : null} + /> + +
+ + + ) : null} + {(transformNodes > 0 || transforms.length > 0) && ( - - - )} + )} + + )} - + {isSearchSelectionVisible && ( { }, []); return ( - + - + ); }; diff --git a/x-pack/plugins/transform/public/app/services/es_index_service.ts b/x-pack/plugins/transform/public/app/services/es_index_service.ts deleted file mode 100644 index d8d058b731a74..0000000000000 --- a/x-pack/plugins/transform/public/app/services/es_index_service.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpSetup } from '@kbn/core/public'; -import type { DataViewsContract } from '@kbn/data-views-plugin/public'; -import { addInternalBasePath } from '../../../common/constants'; - -export class IndexService { - async canDeleteIndex(http: HttpSetup) { - const privilege = await http.get<{ hasAllPrivileges: boolean }>( - addInternalBasePath(`privileges`), - { version: '1' } - ); - if (!privilege) { - return false; - } - return privilege.hasAllPrivileges; - } - - async dataViewExists(dataViewsContract: DataViewsContract, indexName: string) { - return (await dataViewsContract.find(indexName)).some(({ title }) => title === indexName); - } -} - -export const indexService = new IndexService(); diff --git a/x-pack/plugins/transform/common/privilege/has_privilege_factory.test.ts b/x-pack/plugins/transform/server/capabilities.test.ts similarity index 94% rename from x-pack/plugins/transform/common/privilege/has_privilege_factory.test.ts rename to x-pack/plugins/transform/server/capabilities.test.ts index e9fa6cd7bc6d7..5cf194d6e84e3 100644 --- a/x-pack/plugins/transform/common/privilege/has_privilege_factory.test.ts +++ b/x-pack/plugins/transform/server/capabilities.test.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { extractMissingPrivileges, getPrivilegesAndCapabilities } from './has_privilege_factory'; +import { type TransformCapabilities } from '../common/types/capabilities'; + +import { extractMissingPrivileges, getPrivilegesAndCapabilities } from './capabilities'; describe('has_privilege_factory', () => { const fullClusterPrivileges = { @@ -65,9 +67,10 @@ describe('has_privilege_factory', () => { describe('getPrivilegesAndCapabilities', () => { it('returns full capabilities if provided both monitor and admin cluster privileges', () => { - const fullCapabilities = { + const fullCapabilities: TransformCapabilities = { canCreateTransform: true, canCreateTransformAlerts: true, + canDeleteIndex: true, canDeleteTransform: true, canGetTransform: true, canPreviewTransform: true, @@ -91,9 +94,10 @@ describe('has_privilege_factory', () => { }); }); it('returns view only capabilities if provided only monitor cluster privileges', () => { - const viewOnlyCapabilities = { + const viewOnlyCapabilities: TransformCapabilities = { canCreateTransform: false, canCreateTransformAlerts: false, + canDeleteIndex: false, canDeleteTransform: false, canGetTransform: true, canPreviewTransform: false, @@ -119,9 +123,10 @@ describe('has_privilege_factory', () => { }); }); it('returns no capabilities and all the missing privileges if no cluster privileges', () => { - const noCapabilities = { + const noCapabilities: TransformCapabilities = { canCreateTransform: false, canCreateTransformAlerts: false, + canDeleteIndex: false, canDeleteTransform: false, canGetTransform: false, canPreviewTransform: false, diff --git a/x-pack/plugins/transform/server/capabilities.ts b/x-pack/plugins/transform/server/capabilities.ts index 73889a0808359..5fd542d428a70 100644 --- a/x-pack/plugins/transform/server/capabilities.ts +++ b/x-pack/plugins/transform/server/capabilities.ts @@ -7,22 +7,123 @@ import type { CoreSetup } from '@kbn/core-lifecycle-server'; import { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + import { - getPrivilegesAndCapabilities, - INITIAL_CAPABILITIES, -} from '../common/privilege/has_privilege_factory'; -import { APP_CLUSTER_PRIVILEGES } from '../common/constants'; + getInitialTransformCapabilities, + type Privilege, + type Privileges, + type PrivilegesAndCapabilities, +} from '../common/types/capabilities'; +import { APP_CLUSTER_PRIVILEGES, APP_INDEX_PRIVILEGES } from '../common/constants'; + import type { PluginStartDependencies } from './types'; export const TRANSFORM_PLUGIN_ID = 'transform' as const; +function isPrivileges(arg: unknown): arg is Privileges { + return ( + isPopulatedObject(arg, ['hasAllPrivileges', 'missingPrivileges']) && + typeof arg.hasAllPrivileges === 'boolean' && + typeof arg.missingPrivileges === 'object' && + arg.missingPrivileges !== null + ); +} + +export const hasPrivilegeFactory = + (privileges: Privileges | undefined | null) => (privilege: Privilege) => { + const [section, requiredPrivilege] = privilege; + if (isPrivileges(privileges) && !privileges.missingPrivileges[section]) { + // if the section does not exist in our missingPrivileges, everything is OK + return true; + } + if (isPrivileges(privileges) && privileges.missingPrivileges[section]!.length === 0) { + return true; + } + if (requiredPrivilege === '*') { + // If length > 0 and we require them all... KO + return false; + } + // If we require _some_ privilege, we make sure that the one + // we require is *not* in the missingPrivilege array + return ( + isPrivileges(privileges) && + !privileges.missingPrivileges[section]!.includes(requiredPrivilege) + ); + }; + +export const extractMissingPrivileges = ( + privilegesObject: { [key: string]: boolean } = {} +): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (!privilegesObject[privilegeName]) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export const getPrivilegesAndCapabilities = ( + clusterPrivileges: Record, + hasOneIndexWithAllPrivileges: boolean, + hasAllPrivileges: boolean +): PrivilegesAndCapabilities => { + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + index: [], + }, + }; + + // Find missing cluster privileges and set overall app privileges + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(clusterPrivileges); + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + if (!hasOneIndexWithAllPrivileges) { + privilegesResult.missingPrivileges.index = [...APP_INDEX_PRIVILEGES]; + } + + const hasPrivilege = hasPrivilegeFactory(privilegesResult); + + const capabilities = getInitialTransformCapabilities(); + + capabilities.canGetTransform = + hasPrivilege(['cluster', 'cluster:monitor/transform/get']) && + hasPrivilege(['cluster', 'cluster:monitor/transform/stats/get']); + + capabilities.canCreateTransform = hasPrivilege(['cluster', 'cluster:admin/transform/put']); + + capabilities.canDeleteTransform = hasPrivilege(['cluster', 'cluster:admin/transform/delete']); + + capabilities.canResetTransform = hasPrivilege(['cluster', 'cluster:admin/transform/reset']); + + capabilities.canPreviewTransform = hasPrivilege(['cluster', 'cluster:admin/transform/preview']); + + capabilities.canStartStopTransform = + hasPrivilege(['cluster', 'cluster:admin/transform/start']) && + hasPrivilege(['cluster', 'cluster:admin/transform/start_task']) && + hasPrivilege(['cluster', 'cluster:admin/transform/stop']); + + capabilities.canCreateTransformAlerts = capabilities.canCreateTransform; + + capabilities.canUseTransformAlerts = capabilities.canGetTransform; + + capabilities.canScheduleNowTransform = capabilities.canStartStopTransform; + + capabilities.canReauthorizeTransform = capabilities.canStartStopTransform; + + capabilities.canDeleteIndex = hasAllPrivileges; + + return { privileges: privilegesResult, capabilities }; +}; + export const setupCapabilities = ( core: Pick, 'capabilities' | 'getStartServices'>, securitySetup?: SecurityPluginSetup ) => { core.capabilities.registerProvider(() => { return { - transform: INITIAL_CAPABILITIES, + transform: getInitialTransformCapabilities(), }; }); @@ -38,10 +139,7 @@ export const setupCapabilities = ( // If security is not enabled or not available, transform should have full permission if (!isSecurityPluginEnabled || !securityStart) { return { - transform: Object.keys(INITIAL_CAPABILITIES).reduce>((acc, p) => { - acc[p] = true; - return acc; - }, {}), + transform: getInitialTransformCapabilities(true), }; } @@ -71,6 +169,8 @@ export const setupCapabilities = ( hasAllRequested ).capabilities; - return { transform: transformCapabilities }; + return { + transform: transformCapabilities as Record>, + }; }); }; diff --git a/x-pack/plugins/transform/server/routes/api/privileges.ts b/x-pack/plugins/transform/server/routes/api/privileges.ts deleted file mode 100644 index 0b93c8e503fc6..0000000000000 --- a/x-pack/plugins/transform/server/routes/api/privileges.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 type { IKibanaResponse, IScopedClusterClient } from '@kbn/core/server'; -import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; -import { - getPrivilegesAndCapabilities, - type PrivilegesAndCapabilities, -} from '../../../common/privilege/has_privilege_factory'; -import { - addInternalBasePath, - APP_CLUSTER_PRIVILEGES, - APP_INDEX_PRIVILEGES, -} from '../../../common/constants'; - -import type { RouteDependencies } from '../../types'; - -export function registerPrivilegesRoute({ router, license }: RouteDependencies) { - router.versioned - .get({ - path: addInternalBasePath('privileges'), - access: 'internal', - }) - .addVersion( - { - version: '1', - validate: false, - }, - license.guardApiRoute( - async (ctx, req, res): Promise> => { - if (license.getStatus().isSecurityEnabled === false) { - // If security isn't enabled, let the user use app. - return res.ok({ - body: getPrivilegesAndCapabilities({}, true, true), - }); - } - - const esClient: IScopedClusterClient = (await ctx.core).elasticsearch.client; - - const esClusterPrivilegesReq: Promise = - esClient.asCurrentUser.security.hasPrivileges({ - body: { - cluster: APP_CLUSTER_PRIVILEGES, - }, - }); - const [esClusterPrivileges, userPrivileges] = await Promise.all([ - // Get cluster privileges - esClusterPrivilegesReq, - // // Get all index privileges the user has - esClient.asCurrentUser.security.getUserPrivileges(), - ]); - - const { has_all_requested: hasAllPrivileges, cluster } = esClusterPrivileges; - const { indices } = userPrivileges; - - // Check if they have all the required index privileges for at least one index - const hasOneIndexWithAllPrivileges = - indices.find(({ privileges }: { privileges: string[] }) => { - if (privileges.includes('all')) { - return true; - } - - const indexHasAllPrivileges = APP_INDEX_PRIVILEGES.every((privilege) => - privileges.includes(privilege) - ); - - return indexHasAllPrivileges; - }) !== undefined; - - return res.ok({ - body: getPrivilegesAndCapabilities( - cluster, - hasOneIndexWithAllPrivileges, - hasAllPrivileges - ), - }); - } - ) - ); -} diff --git a/x-pack/plugins/transform/server/routes/index.ts b/x-pack/plugins/transform/server/routes/index.ts index 0a8539c7683de..aec4417f2a421 100644 --- a/x-pack/plugins/transform/server/routes/index.ts +++ b/x-pack/plugins/transform/server/routes/index.ts @@ -8,11 +8,9 @@ import { RouteDependencies } from '../types'; import { registerFieldHistogramsRoutes } from './api/field_histograms'; -import { registerPrivilegesRoute } from './api/privileges'; import { registerTransformsRoutes } from './api/transforms'; export function registerRoutes(dependencies: RouteDependencies) { registerFieldHistogramsRoutes(dependencies); - registerPrivilegesRoute(dependencies); registerTransformsRoutes(dependencies); } diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 9c9ef2299c60d..2d0cdf45c554d 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -67,7 +67,8 @@ "@kbn/saved-search-plugin", "@kbn/unified-field-list", "@kbn/ebt-tools", - "@kbn/content-management-plugin" + "@kbn/content-management-plugin", + "@kbn/react-kibana-mount" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index e1f61483a6b20..e320b5a24c8be 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2155,7 +2155,7 @@ "discover.advancedSettings.disableDocumentExplorerDescription": "Désactivez cette option pour utiliser le nouveau {documentExplorerDocs} au lieu de la vue classique. l'explorateur de documents offre un meilleur tri des données, des colonnes redimensionnables et une vue en plein écran.", "discover.advancedSettings.discover.showFieldStatisticsDescription": "Activez le {fieldStatisticsDocs} pour afficher des détails tels que les valeurs minimale et maximale d'un champ numérique ou une carte d'un champ géographique. Cette fonctionnalité est en version bêta et susceptible d'être modifiée.", "discover.advancedSettings.discover.showMultifieldsDescription": "Détermine si les {multiFields} doivent s'afficher dans la fenêtre de document étendue. Dans la plupart des cas, les champs multiples sont les mêmes que les champs d'origine. Cette option est uniquement disponible lorsque le paramètre ''searchFieldsFromSource'' est désactivé.", - "discover.advancedSettings.enableSQLDescription": "{technicalPreviewLabel} Cette fonctionnalité en préversion technique est à un stade hautement expérimental ; ne pas s'y fier pour les recherches enregistrées, ni pour les visualisations ou les tableaux de bord en production. Ce paramètre active SQL comme langage de requête à base de texte dans Discover et Lens. Si vous avez des commentaires sur cette expérience, contactez-nous via {link}", + "discover.advancedSettings.enableESQLDescription": "{technicalPreviewLabel} Cette fonctionnalité en préversion technique est à un stade hautement expérimental ; ne pas s'y fier pour les recherches enregistrées, ni pour les visualisations ou les tableaux de bord en production. Ce paramètre active SQL comme langage de requête à base de texte dans Discover et Lens. Si vous avez des commentaires sur cette expérience, contactez-nous via {link}", "discover.context.contextOfTitle": "Documents relatifs à #{anchorId}", "discover.context.newerDocumentsWarning": "Seuls {docCount} documents plus récents que le document ancré ont été trouvés.", "discover.context.olderDocumentsWarning": "Seuls {docCount} documents plus anciens que le document ancré ont été trouvés.", @@ -2178,11 +2178,6 @@ "discover.dscTour.stepAddFields.description": "Cliquez sur {plusIcon} pour ajouter les champs qui vous intéressent.", "discover.dscTour.stepExpand.description": "Cliquez sur {expandIcon} pour afficher, comparer et filtrer les documents.", "discover.errorCalloutFormattedTitle": "{title} : {errorMessage}", - "discover.grid.copyClipboardButtonTitle": "Copier la valeur de {column}", - "discover.grid.copyColumnValuesToClipboard.toastTitle": "Valeurs de la colonne \"{column}\" copiées dans le presse-papiers", - "discover.grid.filterForAria": "Filtrer sur cette {value}", - "discover.grid.filterOutAria": "Exclure cette {value}", - "discover.gridSampleSize.limitDescription": "Les résultats de recherche sont limités à {sampleSize} documents Ajoutez d'autres termes pour affiner votre recherche.", "discover.howToSeeOtherMatchingDocumentsDescription": "Voici les {sampleSize} premiers documents correspondant à votre recherche. Veuillez affiner cette dernière pour en voir davantage.", "discover.noMatchRoute.bannerText": "L'application Discover ne reconnaît pas cet itinéraire : {route}", "discover.noResults.kqlExamples.kqlDescription": "En savoir plus sur {kqlLink}", @@ -2192,9 +2187,6 @@ "discover.pageTitleWithSavedSearch": "Discover –{savedSearchTitle}", "discover.savedSearchAliasMatchRedirect.objectNoun": "Recherche {savedSearch}", "discover.savedSearchURLConflictCallout.objectNoun": "Recherche {savedSearch}", - "discover.searchGenerationWithDescription": "Tableau généré par la recherche {searchTitle}", - "discover.searchGenerationWithDescriptionGrid": "Tableau généré par la recherche {searchTitle} ({searchDescription})", - "discover.selectedDocumentsNumber": "{nr} documents sélectionnés", "discover.showingDefaultDataViewWarningDescription": "Affichage de la vue de données par défaut : \"{loadedDataViewTitle}\" ({loadedDataViewId})", "discover.showingSavedDataViewWarningDescription": "Affichage de la vue de données enregistrée : \"{ownDataViewTitle}\" ({ownDataViewId})", "discover.singleDocRoute.errorMessage": "Aucune vue de données correspondante pour l'ID {dataViewId}", @@ -2220,8 +2212,8 @@ "discover.advancedSettings.docTableHideTimeColumnText": "Permet de masquer la colonne ''Time'' dans Discover et dans toutes les recherches enregistrées des tableaux de bord.", "discover.advancedSettings.docTableHideTimeColumnTitle": "Masquer la colonne ''Time''", "discover.advancedSettings.documentExplorerLinkText": "Explorateur de documents", - "discover.advancedSettings.enableSQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", - "discover.advancedSettings.enableSQLTitle": "Activer SQL", + "discover.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", + "discover.advancedSettings.enableESQLTitle": "Activer SQL", "discover.advancedSettings.fieldsPopularLimitText": "Les N champs les plus populaires à afficher", "discover.advancedSettings.fieldsPopularLimitTitle": "Limite de champs populaires", "discover.advancedSettings.maxDocFieldsDisplayedText": "Le nombre maximal de champs renvoyés dans le résumé du document", @@ -2247,7 +2239,6 @@ "discover.backToTopLinkText": "Revenir en haut de la page.", "discover.badge.readOnly.text": "Lecture seule", "discover.badge.readOnly.tooltip": "Impossible d’enregistrer les recherches", - "discover.clearSelection": "Effacer la sélection", "discover.confirmDataViewSave.cancel": "Annuler", "discover.confirmDataViewSave.message": "L'action que vous avez choisie requiert une vue de données enregistrée.", "discover.confirmDataViewSave.saveAndContinue": "Enregistrer et continuer", @@ -2268,8 +2259,6 @@ "discover.context.unableToLoadAnchorDocumentDescription": "Impossible de charger le document ancré", "discover.context.unableToLoadDocumentDescription": "Impossible de charger les documents", "discover.contextViewRoute.errorTitle": "Une erreur s'est produite", - "discover.controlColumnHeader": "Colonne de commande", - "discover.copyToClipboardJSON": "Copier les documents dans le presse-papiers (JSON)", "discover.discoverBreadcrumbTitle": "Discover", "discover.discoverDefaultSearchSessionName": "Discover", "discover.discoverDescription": "Explorez vos données de manière interactive en interrogeant et en filtrant des documents bruts.", @@ -2310,35 +2299,36 @@ "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "Afficher les documents alentour", "discover.documentsAriaLabel": "Documents", "discover.documentsErrorTitle": "Erreur lors de la recherche", - "discover.docView.table.actions.label": "Actions", - "discover.docView.table.actions.open": "Actions ouvertes", - "discover.docView.table.ignored.multiAboveTooltip": "Une ou plusieurs valeurs dans ce champ sont trop longues et ne peuvent pas être recherchées ni filtrées.", - "discover.docView.table.ignored.multiMalformedTooltip": "Ce champ comporte une ou plusieurs valeurs mal formées qui ne peuvent pas être recherchées ni filtrées.", - "discover.docView.table.ignored.multiUnknownTooltip": "Une ou plusieurs valeurs dans ce champ ont été ignorées par Elasticsearch et ne peuvent pas être recherchées ni filtrées.", - "discover.docView.table.ignored.singleAboveTooltip": "La valeur dans ce champ est trop longue et ne peut pas être recherchée ni filtrée.", - "discover.docView.table.ignored.singleMalformedTooltip": "La valeur dans ce champ est mal formée et ne peut pas être recherchée ni filtrée.", - "discover.docView.table.ignored.singleUnknownTooltip": "La valeur dans ce champ a été ignorée par Elasticsearch et ne peut pas être recherchée ni filtrée.", - "discover.docView.table.searchPlaceHolder": "Rechercher les noms de champs", - "discover.docViews.json.jsonTitle": "JSON", - "discover.docViews.table.filterForFieldPresentButtonAriaLabel": "Filtrer sur le champ", - "discover.docViews.table.filterForFieldPresentButtonTooltip": "Filtrer sur le champ", - "discover.docViews.table.filterForValueButtonAriaLabel": "Filtrer sur la valeur", - "discover.docViews.table.filterForValueButtonTooltip": "Filtrer sur la valeur", - "discover.docViews.table.filterOutValueButtonAriaLabel": "Exclure la valeur", - "discover.docViews.table.filterOutValueButtonTooltip": "Exclure la valeur", - "discover.docViews.table.ignored.multiValueLabel": "Contient des valeurs ignorées", - "discover.docViews.table.ignored.singleValueLabel": "Valeur ignorée", - "discover.docViews.table.pinFieldAriaLabel": "Épingler le champ", - "discover.docViews.table.pinFieldLabel": "Épingler le champ", + "unifiedDocViewer.docView.table.actions.label": "Actions", + "unifiedDocViewer.docView.table.actions.open": "Actions ouvertes", + "unifiedDocViewer.docView.table.ignored.multiAboveTooltip": "Une ou plusieurs valeurs dans ce champ sont trop longues et ne peuvent pas être recherchées ni filtrées.", + "unifiedDocViewer.docView.table.ignored.multiMalformedTooltip": "Ce champ comporte une ou plusieurs valeurs mal formées qui ne peuvent pas être recherchées ni filtrées.", + "unifiedDocViewer.docView.table.ignored.multiUnknownTooltip": "Une ou plusieurs valeurs dans ce champ ont été ignorées par Elasticsearch et ne peuvent pas être recherchées ni filtrées.", + "unifiedDocViewer.docView.table.ignored.singleAboveTooltip": "La valeur dans ce champ est trop longue et ne peut pas être recherchée ni filtrée.", + "unifiedDocViewer.docView.table.ignored.singleMalformedTooltip": "La valeur dans ce champ est mal formée et ne peut pas être recherchée ni filtrée.", + "unifiedDocViewer.docView.table.ignored.singleUnknownTooltip": "La valeur dans ce champ a été ignorée par Elasticsearch et ne peut pas être recherchée ni filtrée.", + "unifiedDocViewer.docView.table.searchPlaceHolder": "Rechercher les noms de champs", + "unifiedDocViewer.docViews.json.jsonTitle": "JSON", + "unifiedDocViewer.docViews.table.filterForFieldPresentButtonAriaLabel": "Filtrer sur le champ", + "unifiedDocViewer.docViews.table.filterForFieldPresentButtonTooltip": "Filtrer sur le champ", + "unifiedDocViewer.docViews.table.filterForValueButtonAriaLabel": "Filtrer sur la valeur", + "unifiedDocViewer.docViews.table.filterForValueButtonTooltip": "Filtrer sur la valeur", + "unifiedDocViewer.docViews.table.filterOutValueButtonAriaLabel": "Exclure la valeur", + "unifiedDocViewer.docViews.table.filterOutValueButtonTooltip": "Exclure la valeur", + "unifiedDocViewer.docViews.table.ignored.multiValueLabel": "Contient des valeurs ignorées", + "unifiedDocViewer.docViews.table.ignored.singleValueLabel": "Valeur ignorée", + "unifiedDocViewer.docViews.table.pinFieldAriaLabel": "Épingler le champ", + "unifiedDocViewer.docViews.table.pinFieldLabel": "Épingler le champ", "discover.docViews.table.scoreSortWarningTooltip": "Filtrez sur _score pour pouvoir récupérer les valeurs correspondantes.", - "discover.docViews.table.tableTitle": "Tableau", - "discover.docViews.table.toggleColumnInTableButtonAriaLabel": "Afficher/Masquer la colonne dans le tableau", - "discover.docViews.table.toggleColumnInTableButtonTooltip": "Afficher/Masquer la colonne dans le tableau", - "discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "Impossible de filtrer sur les champs méta", - "discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "Impossible de filtrer sur les champs scriptés", - "discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "Les champs non indexés ou les valeurs ignorées ne peuvent pas être recherchés", - "discover.docViews.table.unpinFieldAriaLabel": "Désépingler le champ", - "discover.docViews.table.unpinFieldLabel": "Désépingler le champ", + "unifiedDocViewer.docViews.table.tableTitle": "Tableau", + "unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel": "Afficher/Masquer la colonne dans le tableau", + "unifiedDocViewer.docViews.table.toggleColumnInTableButtonTooltip": "Afficher/Masquer la colonne dans le tableau", + "unifiedDocViewer.fieldChooser.discoverField.name": "Afficher/Masquer les détails du champ", + "unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "Impossible de filtrer sur les champs méta", + "unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "Impossible de filtrer sur les champs scriptés", + "unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "Les champs non indexés ou les valeurs ignorées ne peuvent pas être recherchés", + "unifiedDocViewer.docViews.table.unpinFieldAriaLabel": "Désépingler le champ", + "unifiedDocViewer.docViews.table.unpinFieldLabel": "Désépingler le champ", "discover.dropZoneTableLabel": "Abandonner la zone pour ajouter un champ en tant que colonne dans la table", "discover.dscTour.stepAddFields.imageAltText": "Dans la liste Champs disponibles, cliquez sur l'icône Plus pour afficher/masquer un champ dans le tableau de documents.", "discover.dscTour.stepAddFields.title": "Ajouter des champs dans le tableau", @@ -2359,47 +2349,32 @@ "discover.embeddable.search.displayName": "rechercher", "discover.errorCalloutShowErrorMessage": "Afficher les détails", "discover.fieldChooser.availableFieldsTooltip": "Champs disponibles pour l'affichage dans le tableau.", - "discover.fieldChooser.discoverField.actions": "Actions", + "unifiedDocViewer.fieldChooser.discoverField.actions": "Actions", "discover.fieldChooser.discoverField.addFieldTooltip": "Ajouter le champ en tant que colonne", - "discover.fieldChooser.discoverField.multiField": "champ multiple", - "discover.fieldChooser.discoverField.multiFieldTooltipContent": "Les champs multiples peuvent avoir plusieurs valeurs.", - "discover.fieldChooser.discoverField.name": "Champ", + "unifiedDocViewer.fieldChooser.discoverField.multiField": "champ multiple", + "unifiedDocViewer.fieldChooser.discoverField.multiFieldTooltipContent": "Les champs multiples peuvent avoir plusieurs valeurs.", + "unifiedDocViewer.fieldChooser.discoverField.name": "Champ", "discover.fieldChooser.discoverField.removeFieldTooltip": "Supprimer le champ du tableau", - "discover.fieldChooser.discoverField.value": "Valeur", + "unifiedDocViewer.fieldChooser.discoverField.value": "Valeur", "discover.goToDiscoverButtonText": "Aller à Discover", - "discover.grid.closePopover": "Fermer la fenêtre contextuelle", - "discover.grid.copyCellValueButton": "Copier la valeur", - "discover.grid.copyColumnNameToClipboard.toastTitle": "Copié dans le presse-papiers", - "discover.grid.copyColumnNameToClipBoardButton": "Copier le nom", - "discover.grid.copyColumnValuesToClipBoardButton": "Copier la colonne", - "discover.grid.copyEscapedValueWithFormulasToClipboardWarningText": "Les valeurs peuvent contenir des formules avec échappement.", - "discover.grid.copyFailedErrorText": "Impossible de copier dans le presse-papiers avec ce navigateur", - "discover.grid.copyValueToClipboard.toastTitle": "Copié dans le presse-papiers", - "discover.grid.documentHeader": "Document", - "discover.grid.editFieldButton": "Modifier le champ de la vue de données", - "discover.grid.filterFor": "Filtrer sur", - "discover.grid.filterOut": "Exclure", "discover.grid.flyout.documentNavigation": "Navigation dans le document", "discover.grid.flyout.toastColumnAdded": "La colonne \"{columnName}\" a été ajoutée.", "discover.grid.flyout.toastColumnRemoved": "La colonne \"{columnName}\" a été supprimée.", - "discover.grid.selectDoc": "Sélectionner le document \"{rowNumber}\"", "discover.grid.tableRow.detailHeading": "Document développé", "discover.grid.tableRow.textBasedDetailHeading": "Ligne développée", "discover.grid.tableRow.viewSingleDocumentLinkTextSimple": "Document unique", "discover.grid.tableRow.viewSurroundingDocumentsHover": "Inspectez des documents qui ont été créés avant et après ce document. Seuls les filtres épinglés restent actifs dans la vue Documents relatifs.", "discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple": "Documents relatifs", "discover.grid.tableRow.viewText": "Afficher :", - "discover.grid.viewDoc": "Afficher/Masquer les détails de la boîte de dialogue", "discover.helpMenu.appName": "Découverte", "discover.inspectorRequestDataTitleDocuments": "Documents", "discover.inspectorRequestDescriptionDocument": "Cette requête interroge Elasticsearch afin de récupérer les documents.", "discover.invalidFiltersWarnToast.description": "Les références d'ID de la vue de données dans certains filtres appliqués diffèrent de la vue de données actuelle.", "discover.invalidFiltersWarnToast.title": "Références d'index différentes", - "discover.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", - "discover.json.copyToClipboardLabel": "Copier dans le presse-papiers", + "unifiedDocViewer.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", + "unifiedDocViewer.json.copyToClipboardLabel": "Copier dans le presse-papiers", "discover.loadingDocuments": "Chargement des documents", - "discover.loadingJSON": "Chargement de JSON", - "discover.loadingResults": "Chargement des résultats", + "unifiedDocViewer.loadingJSON": "Chargement de JSON", "discover.localMenu.alertsDescription": "Alertes", "discover.localMenu.fallbackReportTitle": "Recherche Discover sans titre", "discover.localMenu.inspectTitle": "Inspecter", @@ -2442,28 +2417,23 @@ "discover.noResults.suggestion.syntaxPopoverExampleHeader": "Exemple", "discover.noResults.suggestion.tryText": "Voici quelques solutions à essayer :", "discover.noResults.suggestion.viewAllMatchesButtonText": "Afficher toutes les correspondances", - "discover.noResultsFound": "Résultat introuvable", "discover.notifications.invalidTimeRangeText": "La plage temporelle spécifiée n'est pas valide (de : \"{from}\" à \"{to}\").", "discover.notifications.invalidTimeRangeTitle": "Plage temporelle non valide", "discover.notifications.notSavedSearchTitle": "La recherche \"{savedSearchTitle}\" n'a pas été enregistrée.", "discover.notifications.savedSearchTitle": "La recherche \"{savedSearchTitle}\" a été enregistrée.", "discover.pageTitleWithoutSavedSearch": "Discover - Recherche non encore enregistrée", "discover.reloadSavedSearchButton": "Réinitialiser la recherche", - "discover.removeColumnLabel": "Supprimer la colonne", "discover.rootBreadcrumb": "Découverte", "discover.sampleData.viewLinkLabel": "Découverte", "discover.savedSearch.savedObjectName": "Recherche enregistrée", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "Ouvrir dans Discover", "discover.searchingTitle": "Recherche", - "discover.selectColumnHeader": "Sélectionner la colonne", "discover.serverLocatorExtension.titleFromLocatorUnknown": "Recherche inconnue", - "discover.showAllDocuments": "Afficher tous les documents", - "discover.showSelectedDocumentsOnly": "Afficher uniquement les documents sélectionnés", "discover.singleDocRoute.errorTitle": "Une erreur s'est produite", "discover.skipToBottomButtonLabel": "Atteindre la fin du tableau", - "discover.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.", - "discover.sourceViewer.errorMessageTitle": "Une erreur s'est produite.", - "discover.sourceViewer.refresh": "Actualiser", + "unifiedDocViewer.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.", + "unifiedDocViewer.sourceViewer.errorMessageTitle": "Une erreur s'est produite.", + "unifiedDocViewer.sourceViewer.refresh": "Actualiser", "discover.toggleSidebarAriaLabel": "Activer/Désactiver la barre latérale", "discover.topNav.openSearchPanel.manageSearchesButtonLabel": "Gérer les recherches", "discover.topNav.openSearchPanel.noSearchesFoundDescription": "Aucune recherche correspondante trouvée.", @@ -2480,6 +2450,25 @@ "discover.viewAlert.searchSourceErrorTitle": "Erreur lors de la récupération de la source de recherche", "discover.viewModes.document.label": "Documents", "discover.viewModes.fieldStatistics.label": "Statistiques de champ", + "unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel": "{timeFieldName} – Ce champ représente l'heure à laquelle les événements se sont produits.", + "unifiedDataTable.searchGenerationWithDescription": "Tableau généré par la recherche {searchTitle}", + "unifiedDataTable.searchGenerationWithDescriptionGrid": "Tableau généré par la recherche {searchTitle} ({searchDescription})", + "unifiedDataTable.selectedDocumentsNumber": "{nr} documents sélectionnés", + "unifiedDataTable.clearSelection": "Effacer la sélection", + "unifiedDataTable.controlColumnHeader": "Colonne de commande", + "unifiedDataTable.copyToClipboardJSON": "Copier les documents dans le presse-papiers (JSON)", + "unifiedDataTable.tableHeader.timeFieldIconTooltip": "Ce champ représente l'heure à laquelle les événements se sont produits.", + "unifiedDataTable.grid.copyColumnNameToClipBoardButton": "Copier le nom", + "unifiedDataTable.grid.copyColumnValuesToClipBoardButton": "Copier la colonne", + "unifiedDataTable.grid.documentHeader": "Document", + "unifiedDataTable.grid.editFieldButton": "Modifier le champ de la vue de données", + "unifiedDataTable.grid.selectDoc": "Sélectionner le document \"{rowNumber}\"", + "unifiedDataTable.loadingResults": "Chargement des résultats", + "unifiedDataTable.noResultsFound": "Résultat introuvable", + "unifiedDataTable.removeColumnLabel": "Supprimer la colonne", + "unifiedDataTable.selectColumnHeader": "Sélectionner la colonne", + "unifiedDataTable.showAllDocuments": "Afficher tous les documents", + "unifiedDataTable.showSelectedDocumentsOnly": "Afficher uniquement les documents sélectionnés", "domDragDrop.announce.cancelled": "Mouvement annulé. {label} revenu à sa position initiale", "domDragDrop.announce.cancelledItem": "Mouvement annulé. {label} revenu au groupe {groupLabel} à la position {position}", "domDragDrop.announce.dropped.combineCompatible": "{label} combiné dans le groupe {groupLabel} en {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}", @@ -5696,34 +5685,34 @@ "unifiedFieldList.fieldNameDescription.textField": "Texte intégral tel que le corps d'un e-mail ou la description d'un produit.", "unifiedFieldList.fieldNameDescription.unknownField": "Champ inconnu", "unifiedFieldList.fieldNameDescription.versionField": "Versions des logiciels. Prend en charge les règles de priorité de la Gestion sémantique des versions.", - "unifiedFieldList.fieldNameIcons.binaryAriaLabel": "Binaire", - "unifiedFieldList.fieldNameIcons.booleanAriaLabel": "Booléen", - "unifiedFieldList.fieldNameIcons.conflictFieldAriaLabel": "Conflit", - "unifiedFieldList.fieldNameIcons.counterFieldAriaLabel": "Indicateur de compteur", - "unifiedFieldList.fieldNameIcons.dateFieldAriaLabel": "Date", - "unifiedFieldList.fieldNameIcons.dateRangeFieldAriaLabel": "Plage de dates", - "unifiedFieldList.fieldNameIcons.denseVectorFieldAriaLabel": "Vecteur dense", - "unifiedFieldList.fieldNameIcons.flattenedFieldAriaLabel": "Lissé", - "unifiedFieldList.fieldNameIcons.gaugeFieldAriaLabel": "Indicateur de jauge", - "unifiedFieldList.fieldNameIcons.geoPointFieldAriaLabel": "Point géographique", - "unifiedFieldList.fieldNameIcons.geoShapeFieldAriaLabel": "Forme géométrique", - "unifiedFieldList.fieldNameIcons.histogramFieldAriaLabel": "Histogramme", - "unifiedFieldList.fieldNameIcons.ipAddressFieldAriaLabel": "Adresse IP", - "unifiedFieldList.fieldNameIcons.ipRangeFieldAriaLabel": "Plage d'IP", - "unifiedFieldList.fieldNameIcons.keywordFieldAriaLabel": "Mot-clé", - "unifiedFieldList.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3", - "unifiedFieldList.fieldNameIcons.nestedFieldAriaLabel": "Imbriqué", - "unifiedFieldList.fieldNameIcons.numberFieldAriaLabel": "Nombre", - "unifiedFieldList.fieldNameIcons.pointFieldAriaLabel": "Point", - "unifiedFieldList.fieldNameIcons.rankFeatureFieldAriaLabel": "Fonctionnalité de rang", - "unifiedFieldList.fieldNameIcons.rankFeaturesFieldAriaLabel": "Fonctionnalités de rang", - "unifiedFieldList.fieldNameIcons.recordAriaLabel": "Enregistrements", - "unifiedFieldList.fieldNameIcons.shapeFieldAriaLabel": "Forme", - "unifiedFieldList.fieldNameIcons.sourceFieldAriaLabel": "Champ source", - "unifiedFieldList.fieldNameIcons.stringFieldAriaLabel": "Chaîne", - "unifiedFieldList.fieldNameIcons.textFieldAriaLabel": "Texte", - "unifiedFieldList.fieldNameIcons.unknownFieldAriaLabel": "Champ inconnu", - "unifiedFieldList.fieldNameIcons.versionFieldAriaLabel": "Version", + "discover.fieldNameIcons.binaryAriaLabel": "Binaire", + "discover.fieldNameIcons.booleanAriaLabel": "Booléen", + "discover.fieldNameIcons.conflictFieldAriaLabel": "Conflit", + "discover.fieldNameIcons.counterFieldAriaLabel": "Indicateur de compteur", + "discover.fieldNameIcons.dateFieldAriaLabel": "Date", + "discover.fieldNameIcons.dateRangeFieldAriaLabel": "Plage de dates", + "discover.fieldNameIcons.denseVectorFieldAriaLabel": "Vecteur dense", + "discover.fieldNameIcons.flattenedFieldAriaLabel": "Lissé", + "discover.fieldNameIcons.gaugeFieldAriaLabel": "Indicateur de jauge", + "discover.fieldNameIcons.geoPointFieldAriaLabel": "Point géographique", + "discover.fieldNameIcons.geoShapeFieldAriaLabel": "Forme géométrique", + "discover.fieldNameIcons.histogramFieldAriaLabel": "Histogramme", + "discover.fieldNameIcons.ipAddressFieldAriaLabel": "Adresse IP", + "discover.fieldNameIcons.ipRangeFieldAriaLabel": "Plage d'IP", + "discover.fieldNameIcons.keywordFieldAriaLabel": "Mot-clé", + "discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3", + "discover.fieldNameIcons.nestedFieldAriaLabel": "Imbriqué", + "discover.fieldNameIcons.numberFieldAriaLabel": "Nombre", + "discover.fieldNameIcons.pointFieldAriaLabel": "Point", + "discover.fieldNameIcons.rankFeatureFieldAriaLabel": "Fonctionnalité de rang", + "discover.fieldNameIcons.rankFeaturesFieldAriaLabel": "Fonctionnalités de rang", + "discover.fieldNameIcons.recordAriaLabel": "Enregistrements", + "discover.fieldNameIcons.shapeFieldAriaLabel": "Forme", + "discover.fieldNameIcons.sourceFieldAriaLabel": "Champ source", + "discover.fieldNameIcons.stringFieldAriaLabel": "Chaîne", + "discover.fieldNameIcons.textFieldAriaLabel": "Texte", + "discover.fieldNameIcons.unknownFieldAriaLabel": "Champ inconnu", + "discover.fieldNameIcons.versionFieldAriaLabel": "Version", "unifiedFieldList.fieldNameSearch.filterByNameLabel": "Rechercher les noms de champs", "unifiedFieldList.fieldPopover.addExistsFilterLabel": "Filtrer sur le champ", "unifiedFieldList.fieldPopover.deleteFieldLabel": "Supprimer le champ de la vue de données", @@ -5924,9 +5913,6 @@ "unifiedSearch.query.queryBar.indexPattern.findFilterSet": "Trouver une requête", "unifiedSearch.query.queryBar.indexPattern.manageFieldButton": "Gérer cette vue de données", "unifiedSearch.query.queryBar.indexPattern.temporaryDataviewLabel": "Temporaire", - "unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning": "Un changement de vue de données supprime la requête SQL en cours. Sauvegardez cette recherche pour ne pas perdre de travail.", - "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesLabel": "Langages de requête à base de texte", - "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody": "Un changement de vue de données supprime la requête SQL en cours. Sauvegardez cette recherche pour ne pas perdre de travail.", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton": "Basculer sans sauvegarder", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalDismissButton": "Ne plus afficher cet avertissement", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalSaveButton": "Sauvegarder et basculer", @@ -27639,8 +27625,6 @@ "xpack.observability.slo.sloEdit.sliType.customKql.goodQuery.tooltip": "Cette requête KQL doit renvoyer un sous-ensemble d'événements considérés comme \"bons\" ou \"réussis\" aux fins du calcul du SLO. La requête doit filtrer les événements en fonction de certains critères pertinents, tels que les codes de statut, les messages d'erreur ou d'autres champs pertinents.", "xpack.observability.slo.sloEdit.sliType.customKql.goodQueryPlaceholder": "Définir les bons événements", "xpack.observability.slo.sloEdit.sliType.customKql.queryFilter": "Filtre de requête", - "xpack.observability.slo.sloEdit.sliType.customKql.timestampField.label": "Champ d'horodatage", - "xpack.observability.slo.sloEdit.sliType.customKql.timestampField.placeholder": "Sélectionner un champ d'horodatage", "xpack.observability.slo.sloEdit.sliType.customKql.totalQuery": "Total de la requête", "xpack.observability.slo.sloEdit.sliType.customKql.totalQuery.tooltip": "Cette requête KQL doit renvoyer tous les événements pertinents pour le calcul du SLO, y compris les bons et les mauvais événements.", "xpack.observability.slo.sloEdit.sliType.customKql.totalQueryPlaceholder": "Définir le total d'événements", @@ -27654,8 +27638,6 @@ "xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder": "Sélectionner un champ d’indicateur", "xpack.observability.slo.sloEdit.sliType.customMetric.queryFilter": "Filtre de requête", "xpack.observability.slo.sloEdit.sliType.customMetric.sumLabel": "Somme de", - "xpack.observability.slo.sloEdit.sliType.customMetric.timestampField.label": "Champ d'horodatage", - "xpack.observability.slo.sloEdit.sliType.customMetric.timestampField.placeholder": "Sélectionner un champ d'horodatage", "xpack.observability.slo.sloEdit.tags.label": "Balises", "xpack.observability.slo.sloEdit.tags.placeholder": "Ajouter des balises", "xpack.observability.slo.sloEdit.targetSlo.label": "Cible/SLO (%)", @@ -33186,43 +33168,6 @@ "xpack.securitySolution.expandedValue.showTopN.showTopValues": "Afficher les valeurs les plus élevées", "xpack.securitySolution.explore.landing.pageTitle": "Explorer", "xpack.securitySolution.featureCatalogueDescription": "Prévenez, collectez, détectez et traitez les menaces pour une protection unifiée dans toute votre infrastructure.", - "xpack.securitySolution.featureRegistry.deleteSubFeatureDetails": "Supprimer les cas et les commentaires", - "xpack.securitySolution.featureRegistry.deleteSubFeatureName": "Supprimer", - "xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle": "Cas", - "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "Sécurité", - "xpack.securitySolution.featureRegistry.subFeatures.blockList": "Liste noire", - "xpack.securitySolution.featureRegistry.subFeatures.blockList.description": "Étendez la protection d'Elastic Defend contre les processus malveillants et protégez-vous des applications potentiellement nuisibles.", - "xpack.securitySolution.featureRegistry.subFeatures.blockList.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès à la liste noire.", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList": "Liste de points de terminaison", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList.description": "Affiche tous les hôtes exécutant Elastic Defend et leurs détails d'intégration associés.", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès à la liste de points de terminaison.", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters": "Filtres d'événements", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters.description": "Excluez les événements de point de terminaison dont vous n'avez pas besoin ou que vous ne souhaitez pas stocker dans Elasticsearch.", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès aux filtres d'événements.", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations": "Exécuter les opérations", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations.description": "Effectuez l'exécution de script sur le point de terminaison.", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès aux opérations d'exécution.", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations": "Opérations de fichier", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations.description": "Effectuez les actions de réponse liées aux fichiers dans la console de réponse.", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès aux opérations de fichier.", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation": "Isolation de l'hôte", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.description": "Effectuez les actions de réponse \"isoler\" et \"libérer\".", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès à l'isolation de l'hôte.", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions": "Exceptions d'isolation de l'hôte", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.description": "Ajoutez des adresses IP spécifiques avec lesquelles les hôtes isolés sont toujours autorisés à communiquer, même lorsqu'ils sont isolés du reste du réseau.", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès aux exceptions d'isolation de l'hôte.", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement": "Gestion des politiques Elastic Defend", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement.description": "Accédez à la politique d'intégration Elastic Defend pour configurer les protections, la collecte des événements et les fonctionnalités de politique avancées.", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès à la gestion des politiques.", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations": "Opérations de traitement", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations.description": "Effectuez les actions de réponse liées aux processus dans la console de réponse.", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès aux opérations de traitement.", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory": "Historique des actions de réponse", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.description": "Accédez à l'historique des actions de réponse effectuées sur les points de terminaison.", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès à l'historique des actions de réponse.", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications": "Applications de confiance", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.description": "Aide à atténuer les conflits avec d'autres logiciels, généralement d'autres applications d'antivirus ou de sécurité des points de terminaison.", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.privilegesTooltip": "\"Tous les espaces\" est requis pour l'accès aux applications de confiance.", "xpack.securitySolution.fieldBrowser.actionsLabel": "Actions", "xpack.securitySolution.fieldBrowser.categoryLabel": "Catégorie", "xpack.securitySolution.fieldBrowser.createFieldButton": "Créer un champ", @@ -33325,14 +33270,11 @@ "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "Adresses IP", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "Nom", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "Hôtes associés", - "xpack.securitySolution.flyout.entities.relatedHostsToolTip": "L’utilisateur a été authentifié avec succès sur ces hôtes après l’alerte.", "xpack.securitySolution.flyout.entities.relatedUsersTitle": "Utilisateurs associés", - "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "Ces utilisateurs ont été authentifiés avec succès sur l’hôte concerné après l’alerte.", "xpack.securitySolution.flyout.entities.usersInfoTitle": "Informations sur l’utilisateur", "xpack.securitySolution.flyout.prevalenceErrorMessage": "prévalence", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "Nombre d'alertes", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "Compte du document", - "xpack.securitySolution.flyout.response.empty": "Il n’y a pas d’actions de réponse définies pour cet évènement.", "xpack.securitySolution.flyout.response.title": "Réponses", "xpack.securitySolution.flyout.sessionViewErrorMessage": "vue de session", "xpack.securitySolution.footer.autoRefreshActiveDescription": "Actualisation automatique active", @@ -34659,10 +34601,6 @@ "xpack.serverlessSearch.apiKey.roleDescriptorsLinkLabel": "Découvrir comment structurer les descripteurs de rôles", "xpack.serverlessSearch.apiKey.setup.description": "Les détails de la configuration de base pour créer votre clé d’API.", "xpack.serverlessSearch.apiKey.setup.title": "Configuration", - "xpack.serverlessSearch.apiKey.stepOneDescription": "Identifiant unique pour l’authentification et l’autorisation. ", - "xpack.serverlessSearch.apiKey.stepOneTitle": "Générer et stocker vos clés d’API", - "xpack.serverlessSearch.apiKey.stepTwoDescription": "Identifiant unique pour un projet spécifique. ", - "xpack.serverlessSearch.apiKey.stepTwoTitle": "Stocker votre identifiant unique du cloud", "xpack.serverlessSearch.apiKey.title": "Stocker votre clé d’API et votre identifiant du cloud", "xpack.serverlessSearch.apiKey.userFieldHelpText": "Identifiant de l’utilisateur créant la clé d’API.", "xpack.serverlessSearch.apiKey.userFieldLabel": "Utilisateur", @@ -34673,20 +34611,6 @@ "xpack.serverlessSearch.configureClient.title": "Configurer votre client", "xpack.serverlessSearch.disabled": "Désactivé", "xpack.serverlessSearch.enabled": "Activé", - "xpack.serverlessSearch.footer.billing.title": "Facturation et utilisation", - "xpack.serverlessSearch.footer.community.title": "Rejoindre la communauté", - "xpack.serverlessSearch.footer.discoverCard.buttonText": "Explorer les données dans Discover", - "xpack.serverlessSearch.footer.discoverCard.description": "Avec Discover, vous pouvez rapidement rechercher et filtrer vos données, obtenir des informations sur la structure des champs et afficher vos résultats dans une visualisation.", - "xpack.serverlessSearch.footer.discoverCard.title": "Explorer et visualiser vos données dans Discover", - "xpack.serverlessSearch.footer.feedback.title": "Donner un retour", - "xpack.serverlessSearch.footer.inviteUsers.title": "Inviter d'autres utilisateurs", - "xpack.serverlessSearch.footer.pipelinesCard.buttonText": "Configurer vos pipelines d’ingestion", - "xpack.serverlessSearch.footer.pipelinesCard.description": "Prétraiter vos données avant leur indexation dans Elasticsearch. Supprimez des champs, extrayez des valeurs de textes ou enrichissez vos données avec des modèles de Machine Learning comme ELSER.", - "xpack.serverlessSearch.footer.pipelinesCard.title": "Transformer vos données à l’aide de pipelines", - "xpack.serverlessSearch.footer.searchUI.buttonText": "Créer avec Search UI", - "xpack.serverlessSearch.footer.searchUI.description": "L’interface utilisateur Search est une bibliothèque JavaScript libre et gratuite maintenue par Elastic pour un développement rapide d’expériences de recherche modernes et attrayantes.", - "xpack.serverlessSearch.footer.searchUI.title": "Créer une interface utilisateur avec Search UI", - "xpack.serverlessSearch.footer.title": "Et ensuite ?", "xpack.serverlessSearch.header.title": "Lancez-vous avec Elasticsearch", "xpack.serverlessSearch.invalidJsonError": "JSON non valide", "xpack.serverlessSearch.languages.cURL": "cURL", @@ -37639,7 +37563,6 @@ "xpack.synthetics.synthetics.waterfallChart.labels.timings.ssl": "TLS", "xpack.synthetics.synthetics.waterfallChart.labels.timings.wait": "En attente (TTFB)", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", - "xpack.synthetics.syntheticsMonitors": "Uptime - Moniteur", "xpack.synthetics.testDetails.after": "Après ", "xpack.synthetics.testDetails.codeExecuted": "Code exécuté", "xpack.synthetics.testDetails.console": "Console", @@ -37764,26 +37687,19 @@ "xpack.transform.alertTypes.transformHealth.healthCheckRecoveryMessage": "La/les {count, plural, one {Transformer} many {Transformations} other {Transformations}} {transformsString} {count, plural, one {est} many {sont} other {sont}} saine(s).", "xpack.transform.alertTypes.transformHealth.notStartedMessage": "La/les {count, plural, one {Transformer} many {Transformer} other {Transformer}} {transformsString} {count, plural, one {est} many {sont} other {ne sont pas démarrées}}.", "xpack.transform.alertTypes.transformHealth.notStartedRecoveryMessage": "La/les {count, plural, one {Transformer} many {Transformer} other {Transformer}} {transformsString} {count, plural, one {est} many {sont} other {sont}} lancée(s).", - "xpack.transform.app.deniedPrivilegeDescription": "Pour utiliser cette section des transformations, vous devez avoir {privilegesCount, plural, one {ce privilège de cluster} many {ces privilèges de cluster} other {ces privilèges de cluster}} : {missingPrivileges}.", "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message} Veuillez contacter votre administrateur.", "xpack.transform.clone.noDataViewErrorPromptText": "Impossible de cloner la transformation {transformId}. Il n'existe aucune vue de données pour {dataViewTitle}.", "xpack.transform.danglingTasksError": "{count} {count, plural, one {transformation n'a pas de} many {transformations n'ont pas de} other {transformations n'ont pas de}} détails de configuration : [{transformIds}] {count, plural, one {Elle ne peut pas être récupérée et doit être supprimée} many {Elles ne peuvent pas être récupérées et doivent être supprimées} other {Elles ne peuvent pas être récupérées et doivent être supprimées}}.", "xpack.transform.deleteTransform.deleteAnalyticsWithDataViewErrorMessage": "Une erreur est survenue lors de la suppression de la vue de données {destinationIndex}", - "xpack.transform.deleteTransform.deleteAnalyticsWithDataViewSuccessMessage": "Requête de suppression de la vue de données {destinationIndex} reconnue.", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "Une erreur s'est produite lors de la suppression de l'index de destination {destinationIndex}", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage": "Requête de suppression de l'index de destination {destinationIndex} reconnue.", "xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage": "Une erreur s'est produite lors de la vérification de l'existence de la vue de données {dataView} : {error}", "xpack.transform.edit.noDataViewErrorPromptText": "Impossible d'obtenir la vue de données pour la transformation {transformId}. Il n'existe aucune vue de données pour {dataViewTitle}.", "xpack.transform.forceDeleteTransformMessage": "Supprimer {count} {count, plural, one {transformation} many {transformations} other {transformations}}", "xpack.transform.managedTransformsWarningCallout": "{count, plural, one {Cette transformation} many {Au moins l'une de ces transformations} other {Au moins l'une de ces transformations}} est préconfigurée par Elastic. Le fait de {action} {count, plural, one {la} many {les} other {les}} avec une heure de fin spécifique peut avoir un impact sur d'autres éléments du produit.", "xpack.transform.multiTransformActionsMenu.transformsCount": "Sélection effectuée de {count} {count, plural, one {transformation} many {transformations} other {transformations}}", "xpack.transform.stepCreateForm.createDataViewErrorMessage": "Une erreur est survenue lors de la création de la vue de données Kibana {dataViewName} :", - "xpack.transform.stepCreateForm.createDataViewSuccessMessage": "La vue de données Kibana {dataViewName} a bien été créée.", "xpack.transform.stepCreateForm.createTransformErrorMessage": "Une erreur s'est produite lors de la création de la transformation {transformId} :", - "xpack.transform.stepCreateForm.createTransformSuccessMessage": "La requête pour créer la transformation {transformId} a été reconnue.", "xpack.transform.stepCreateForm.duplicateDataViewErrorMessage": "Une erreur est survenue lors de la création de la vue de données Kibana {dataViewName} : La vue de données existe déjà.", - "xpack.transform.stepCreateForm.startTransformErrorMessage": "Une erreur s'est produite lors du démarrage de la transformation {transformId} :", - "xpack.transform.stepCreateForm.startTransformSuccessMessage": "La requête pour démarrer la transformation {transformId} a été reconnue.", "xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar": "Requête non valide : {queryErrorMessage}", "xpack.transform.stepDefineForm.queryPlaceholderKql": "Par exemple, {example}", "xpack.transform.stepDefineForm.queryPlaceholderLucene": "Par exemple, {example}", @@ -37795,41 +37711,30 @@ "xpack.transform.stepDetailsForm.retentionPolicyMaxAgePlaceholderText": "max_age, par exemple {exampleValue}", "xpack.transform.transformForm.sizeNotationPlaceholder": "Exemples : {example1}, {example2}, {example3}, {example4}", "xpack.transform.transformList.alertingRules.tooltipContent": "La transformation a {rulesCount} {rulesCount, plural, one { règle} many { règles} other { règles}} d'alerte associée(s)", - "xpack.transform.transformList.bulkDeleteDestDataViewSuccessMessage": "Suppression réussie de {count} {count, plural, one {vue} many {vues} other {vues}} de données de destination.", - "xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage": "Suppression réussie de {count} {count, plural, one {index} many {index système non migrés} other {index}} de destination.", "xpack.transform.transformList.bulkDeleteModalTitle": "Supprimer {count} {count, plural, one {transformation} many {transformations} other {transformations}} ?", - "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "Suppression réussie de {count} {count, plural, one {transformation} many {transformations} other {transformations}}.", "xpack.transform.transformList.bulkReauthorizeModalTitle": "Réautoriser {count} {count, plural, one {transformation} many {transformations} other {transformations}} ?", "xpack.transform.transformList.bulkResetModalTitle": "Réinitialiser {count} {count, plural, one {transformation} many {transformations} other {transformations}} ?", - "xpack.transform.transformList.bulkResetTransformSuccessMessage": "Réinitialisation effectuée avec succès de {count} {count, plural, one {transformation} many {transformations} other {transformations}}.", "xpack.transform.transformList.bulkStartModalTitle": "Démarrer {count} {count, plural, one {transformation} many {transformations} other {transformations}} ?", "xpack.transform.transformList.bulkStopModalTitle": "Arrêter {count} {count, plural, one {transformation} many {transformations} other {transformations}} ?", "xpack.transform.transformList.cannotRestartCompleteBatchTransformToolTip": "{transformId} est une transformation par lots terminée et ne peut pas être redémarrée.", "xpack.transform.transformList.cannotScheduleNowCompleteBatchTransformToolTip": "{transformId} est une transformation par lots terminée qui ne peut pas être planifiée pour traiter les données instantanément.", "xpack.transform.transformList.deleteModalTitle": "Supprimer {transformId} ?", "xpack.transform.transformList.deleteTransformErrorMessage": "Une erreur s'est produite lors de la suppression de la transformation {transformId}", - "xpack.transform.transformList.deleteTransformSuccessMessage": "La requête pour supprimer la transformation {transformId} a été reconnue.", "xpack.transform.transformList.editFlyoutFormPlaceholderText": "Par défaut : {defaultValue}", "xpack.transform.transformList.editFlyoutTitle": "Modifier {transformId}", - "xpack.transform.transformList.editTransformSuccessMessage": "Transformation {transformId} mise à jour.", "xpack.transform.transformList.reauthorizeModalTitle": "Réautoriser {transformId} ?", "xpack.transform.transformList.reauthorizeTransformErrorMessage": "Une erreur s'est produite lors de la réautorisation de la transformation {transformId}", - "xpack.transform.transformList.reauthorizeTransformSuccessMessage": "La requête pour réautoriser la transformation {transformId} a été reconnue.", "xpack.transform.transformList.resetModalTitle": "Réinitialiser {transformId} ?", "xpack.transform.transformList.resetTransformErrorMessage": "Une erreur s'est produite lors de la réinitialisation de la transformation {transformId}", - "xpack.transform.transformList.resetTransformSuccessMessage": "La requête pour réinitialiser la transformation {transformId} a été reconnue.", "xpack.transform.transformList.rowCollapse": "Masquer les détails pour {transformId}", "xpack.transform.transformList.rowExpand": "Afficher les détails pour {transformId}", "xpack.transform.transformList.scheduleNowTransformErrorMessage": "Une erreur s'est produite lors de la planification de la transformation {transformId} pour traiter les données instantanément.", - "xpack.transform.transformList.scheduleNowTransformSuccessMessage": "La demande de planification de transformation {transformId} pour traiter les données immédiatement a été reconnue.", "xpack.transform.transformList.startedTransformToolTip": "{transformId} a déjà démarré.", "xpack.transform.transformList.startModalTitle": "Démarrer {transformId} ?", "xpack.transform.transformList.startTransformErrorMessage": "Une erreur s'est produite lors du démarrage de la transformation {transformId}", - "xpack.transform.transformList.startTransformSuccessMessage": "La requête pour démarrer la transformation {transformId} a été reconnue.", "xpack.transform.transformList.stopModalTitle": "Arrêter {transformId} ?", "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} est déjà arrêtée.", "xpack.transform.transformList.stopTransformErrorMessage": "Une erreur s'est produite lors de l'arrêt de la transformation du cadre de données {transformId}", - "xpack.transform.transformList.stopTransformSuccessMessage": "La requête pour arrêter la transformation du cadre de données {transformId} a été reconnue.", "xpack.transform.transformList.unauthorizedTransformsCallout.insufficientPermissionsMsg": "{unauthorizedCnt, plural, one {Une transformation a été créée avec des autorisations insuffisantes.} many {# transformations ont été créées avec des autorisations insuffisantes.} other {# transformations ont été créées avec des autorisations insuffisantes.}}", "xpack.transform.transformList.unauthorizedTransformsCallout.reauthorizeMsg": "Réautorisez pour démarrer {unauthorizedCnt, plural, one {la transformation} many {# transformations} other {# transformations}}.", "xpack.transform.transformNodes.noTransformNodesCallOutBody": "Vous ne pourrez ni créer ni exécuter de transformations. {learnMoreLink}", @@ -37875,9 +37780,6 @@ "xpack.transform.alertTypes.transformHealth.notStartedCheckName": "La transformation n'a pas démarré", "xpack.transform.alertTypes.transformHealth.testsConfigTransforms.errorMessage": "Au moins une vérification d'intégrité doit être sélectionnée", "xpack.transform.alertTypes.transformHealth.testsSelection.enableTestLabel": "Activer", - "xpack.transform.app.checkingPrivilegesDescription": "Vérification des privilèges…", - "xpack.transform.app.checkingPrivilegesErrorMessage": "Erreur lors de la récupération des privilèges utilisateur depuis le serveur", - "xpack.transform.app.deniedPrivilegeTitle": "Vous ne disposez pas de privilèges de cluster", "xpack.transform.appName": "Transformations", "xpack.transform.appTitle": "Transformations", "xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip": "Vous ne disposez pas d'autorisation pour créer des règles d'alerte de transformation.", @@ -37923,7 +37825,6 @@ "xpack.transform.licenseCheckErrorMessage": "La vérification de la licence a échoué", "xpack.transform.list.emptyPromptButtonText": "Créez votre première transformation", "xpack.transform.list.emptyPromptTitle": "Aucune transformation n'a été trouvée", - "xpack.transform.list.errorPromptTitle": "Une erreur s'est produite lors de l'obtention de la liste de transformations", "xpack.transform.mode": "Mode", "xpack.transform.modeFilter": "Mode", "xpack.transform.models.transformService.allOtherRequestsCancelledDescription": "Toutes les autres requêtes ont été annulées.", @@ -38155,7 +38056,6 @@ "xpack.transform.transformList.editFlyoutUpdateButtonText": "Mettre à jour", "xpack.transform.transformList.editManagedTransformsDescription": "modification", "xpack.transform.transformList.editTransformGenericErrorMessage": "Une erreur s'est produite lors de l'appel du point de terminaison de l'API pour mettre à jour les transformations.", - "xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "Une erreur s'est produite lors de la vérification de la possibilité pour un utilisateur de supprimer l'index de destination", "xpack.transform.transformList.managedBadgeLabel": "Géré", "xpack.transform.transformList.managedBadgeTooltip": "Cette transformation est préconfigurée et gérée par Elastic. Les autres éléments du produit peuvent dépendre de ce comportement.", "xpack.transform.transformList.needsReauthorizationBadge.contactAdminTooltip": "Contactez votre administrateur pour demander les autorisations requises.", @@ -38444,15 +38344,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "AND", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "quand", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "quand", - "xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle": "Ajouter une variable", - "xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "Ajouter une variable", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreHidden": "Les variables déclassées sont masquées", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreShown": "Les variables déclassées sont affichées", - "xpack.triggersActionsUI.components.addMessageVariables.hideDeprecatedVariables": "Masquer", - "xpack.triggersActionsUI.components.addMessageVariables.loadingMessage": "Chargement des variables", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesAvailable": "Aucune variable disponible", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesFound": "Aucune variable trouvée", - "xpack.triggersActionsUI.components.addMessageVariables.showAllDeprecatedVariables": "Afficher tout", "xpack.triggersActionsUI.components.alertTable.useFetchAlerts.errorMessageText": "Une erreur s'est produite lors de la recherche des alertes", "xpack.triggersActionsUI.components.alertTable.useFetchBrowserFieldsCapabilities.errorMessageText": "Une erreur s'est produite lors du chargement des champs du navigateur", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "Choisir…", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0afa87b22236a..26fa98a750f8d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2170,7 +2170,7 @@ "discover.advancedSettings.disableDocumentExplorerDescription": "クラシックビューではなく、{documentExplorerDocs}を使用するには、このオプションをオフにします。ドキュメントエクスプローラーでは、データの並べ替え、列のサイズ変更、全画面表示といった優れた機能を使用できます。", "discover.advancedSettings.discover.showFieldStatisticsDescription": "{fieldStatisticsDocs}を有効にすると、数値フィールドの最大/最小値やジオフィールドの地図といった詳細が表示されます。この機能はベータ段階で、変更される可能性があります。", "discover.advancedSettings.discover.showMultifieldsDescription": "拡張ドキュメントビューに{multiFields}が表示されるかどうかを制御します。ほとんどの場合、マルチフィールドは元のフィールドと同じです。「searchFieldsFromSource」がオフのときにのみこのオプションを使用できます。", - "discover.advancedSettings.enableSQLDescription": "{technicalPreviewLabel}このパッチプレビュー機能は実験段階です。本番の保存された検索、可視化、またはダッシュボードでは、この機能を信頼しないでください。この設定により、DiscoverとLensでテキストベースのクエリ言語としてSQLを使用できます。このエクスペリエンスに関するフィードバックがございましたら、{link}からお問い合わせください", + "discover.advancedSettings.enableESQLDescription": "{technicalPreviewLabel}このパッチプレビュー機能は実験段階です。本番の保存された検索、可視化、またはダッシュボードでは、この機能を信頼しないでください。この設定により、DiscoverとLensでテキストベースのクエリ言語としてSQLを使用できます。このエクスペリエンスに関するフィードバックがございましたら、{link}からお問い合わせください", "discover.context.contextOfTitle": "#{anchorId}の周りのドキュメント", "discover.context.newerDocumentsWarning": "アンカーよりも新しいドキュメントは{docCount}件しか見つかりませんでした。", "discover.context.olderDocumentsWarning": "アンカーよりも古いドキュメントは{docCount}件しか見つかりませんでした。", @@ -2193,11 +2193,6 @@ "discover.dscTour.stepAddFields.description": "{plusIcon}をクリックして、関心があるフィールドを追加します。", "discover.dscTour.stepExpand.description": "{expandIcon}をクリックすると、ドキュメントを表示、比較、フィルタリングできます。", "discover.errorCalloutFormattedTitle": "{title}: {errorMessage}", - "discover.grid.copyClipboardButtonTitle": "{column}の値をコピー", - "discover.grid.copyColumnValuesToClipboard.toastTitle": "\"{column}\"列の値がクリップボードにコピーされました", - "discover.grid.filterForAria": "この{value}でフィルターを適用", - "discover.grid.filterOutAria": "この{value}を除外", - "discover.gridSampleSize.limitDescription": "検索結果は{sampleSize}ドキュメントに制限されています。検索を絞り込むには、その他の検索用語を追加してください。", "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの{sampleSize}件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", "discover.noMatchRoute.bannerText": "Discoverアプリケーションはこのルートを認識できません:{route}", "discover.noResults.kqlExamples.kqlDescription": "{kqlLink}の詳細", @@ -2207,9 +2202,6 @@ "discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}", "discover.savedSearchAliasMatchRedirect.objectNoun": "{savedSearch}検索", "discover.savedSearchURLConflictCallout.objectNoun": "{savedSearch}検索", - "discover.searchGenerationWithDescription": "検索{searchTitle}で生成されたテーブル", - "discover.searchGenerationWithDescriptionGrid": "検索{searchTitle}で生成されたテーブル({searchDescription})", - "discover.selectedDocumentsNumber": "{nr}個のドキュメントが選択されました", "discover.showingDefaultDataViewWarningDescription": "デフォルトのデータビューを表示しています:\"{loadedDataViewTitle}\"({loadedDataViewId})", "discover.showingSavedDataViewWarningDescription": "保存されたデータビューを表示しています:\"{ownDataViewTitle}\"({ownDataViewId})", "discover.singleDocRoute.errorMessage": "ID {dataViewId}の一致するデータビューが見つかりません", @@ -2235,8 +2227,8 @@ "discover.advancedSettings.docTableHideTimeColumnText": "Discover と、ダッシュボードのすべての保存された検索で、「時刻」列を非表示にします。", "discover.advancedSettings.docTableHideTimeColumnTitle": "「時刻」列を非表示", "discover.advancedSettings.documentExplorerLinkText": "ドキュメントエクスプローラー", - "discover.advancedSettings.enableSQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", - "discover.advancedSettings.enableSQLTitle": "SQLを有効にする", + "discover.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", + "discover.advancedSettings.enableESQLTitle": "SQLを有効にする", "discover.advancedSettings.fieldsPopularLimitText": "最も頻繁に使用されるフィールドのトップNを表示します", "discover.advancedSettings.fieldsPopularLimitTitle": "頻繁に使用されるフィールドの制限", "discover.advancedSettings.maxDocFieldsDisplayedText": "ドキュメント概要でレンダリングされるフィールドの最大数", @@ -2262,7 +2254,6 @@ "discover.backToTopLinkText": "最上部へ戻る。", "discover.badge.readOnly.text": "読み取り専用", "discover.badge.readOnly.tooltip": "検索を保存できません", - "discover.clearSelection": "選択した項目をクリア", "discover.confirmDataViewSave.cancel": "キャンセル", "discover.confirmDataViewSave.message": "選択したアクションでは、保存されたデータビューが必要です。", "discover.confirmDataViewSave.saveAndContinue": "保存して続行", @@ -2283,8 +2274,6 @@ "discover.context.unableToLoadAnchorDocumentDescription": "アンカードキュメントを読み込めません", "discover.context.unableToLoadDocumentDescription": "ドキュメントを読み込めません", "discover.contextViewRoute.errorTitle": "エラーが発生しました", - "discover.controlColumnHeader": "列の制御", - "discover.copyToClipboardJSON": "ドキュメントをクリップボードにコピー(JSON)", "discover.discoverBreadcrumbTitle": "Discover", "discover.discoverDefaultSearchSessionName": "Discover", "discover.discoverDescription": "ドキュメントにクエリをかけたりフィルターを適用することで、データをインタラクティブに閲覧できます。", @@ -2325,35 +2314,36 @@ "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "周りのドキュメントを表示", "discover.documentsAriaLabel": "ドキュメント", "discover.documentsErrorTitle": "検索エラー", - "discover.docView.table.actions.label": "アクション", - "discover.docView.table.actions.open": "アクションを開く", - "discover.docView.table.ignored.multiAboveTooltip": "このフィールドの1つ以上の値が長すぎるため、検索またはフィルタリングできません。", - "discover.docView.table.ignored.multiMalformedTooltip": "このフィールドは、検索またはフィルタリングできない正しくない形式の値が1つ以上あります。", - "discover.docView.table.ignored.multiUnknownTooltip": "このフィールドの1つ以上の値がElasticsearchによって無視されたため、検索またはフィルタリングできません。", - "discover.docView.table.ignored.singleAboveTooltip": "このフィールドの値が長すぎるため、検索またはフィルタリングできません。", - "discover.docView.table.ignored.singleMalformedTooltip": "このフィールドの値の形式が正しくないため、検索またはフィルタリングできません。", - "discover.docView.table.ignored.singleUnknownTooltip": "このフィールドの値はElasticsearchによって無視されたため、検索またはフィルタリングできません。", - "discover.docView.table.searchPlaceHolder": "検索フィールド名", - "discover.docViews.json.jsonTitle": "JSON", - "discover.docViews.table.filterForFieldPresentButtonAriaLabel": "フィールド表示のフィルター", - "discover.docViews.table.filterForFieldPresentButtonTooltip": "フィールド表示のフィルター", - "discover.docViews.table.filterForValueButtonAriaLabel": "値でフィルター", - "discover.docViews.table.filterForValueButtonTooltip": "値でフィルター", - "discover.docViews.table.filterOutValueButtonAriaLabel": "値を除外", - "discover.docViews.table.filterOutValueButtonTooltip": "値を除外", - "discover.docViews.table.ignored.multiValueLabel": "無視された値を含む", - "discover.docViews.table.ignored.singleValueLabel": "無視された値", - "discover.docViews.table.pinFieldAriaLabel": "フィールドを固定", - "discover.docViews.table.pinFieldLabel": "フィールドを固定", + "unifiedDocViewer.docView.table.actions.label": "アクション", + "unifiedDocViewer.docView.table.actions.open": "アクションを開く", + "unifiedDocViewer.docView.table.ignored.multiAboveTooltip": "このフィールドの1つ以上の値が長すぎるため、検索またはフィルタリングできません。", + "unifiedDocViewer.docView.table.ignored.multiMalformedTooltip": "このフィールドは、検索またはフィルタリングできない正しくない形式の値が1つ以上あります。", + "unifiedDocViewer.docView.table.ignored.multiUnknownTooltip": "このフィールドの1つ以上の値がElasticsearchによって無視されたため、検索またはフィルタリングできません。", + "unifiedDocViewer.docView.table.ignored.singleAboveTooltip": "このフィールドの値が長すぎるため、検索またはフィルタリングできません。", + "unifiedDocViewer.docView.table.ignored.singleMalformedTooltip": "このフィールドの値の形式が正しくないため、検索またはフィルタリングできません。", + "unifiedDocViewer.docView.table.ignored.singleUnknownTooltip": "このフィールドの値はElasticsearchによって無視されたため、検索またはフィルタリングできません。", + "unifiedDocViewer.docView.table.searchPlaceHolder": "検索フィールド名", + "unifiedDocViewer.docViews.json.jsonTitle": "JSON", + "unifiedDocViewer.docViews.table.filterForFieldPresentButtonAriaLabel": "フィールド表示のフィルター", + "unifiedDocViewer.docViews.table.filterForFieldPresentButtonTooltip": "フィールド表示のフィルター", + "unifiedDocViewer.docViews.table.filterForValueButtonAriaLabel": "値でフィルター", + "unifiedDocViewer.docViews.table.filterForValueButtonTooltip": "値でフィルター", + "unifiedDocViewer.docViews.table.filterOutValueButtonAriaLabel": "値を除外", + "unifiedDocViewer.docViews.table.filterOutValueButtonTooltip": "値を除外", + "unifiedDocViewer.docViews.table.ignored.multiValueLabel": "無視された値を含む", + "unifiedDocViewer.docViews.table.ignored.singleValueLabel": "無視された値", + "unifiedDocViewer.docViews.table.pinFieldAriaLabel": "フィールドを固定", + "unifiedDocViewer.docViews.table.pinFieldLabel": "フィールドを固定", "discover.docViews.table.scoreSortWarningTooltip": "_scoreの値を取得するには、並べ替える必要があります。", - "discover.docViews.table.tableTitle": "表", - "discover.docViews.table.toggleColumnInTableButtonAriaLabel": "表の列を切り替える", - "discover.docViews.table.toggleColumnInTableButtonTooltip": "表の列を切り替える", - "discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "メタフィールドの有無でフィルタリングできません", - "discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "スクリプトフィールドの有無でフィルタリングできません", - "discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "インデックスがないフィールドまたは無視された値は検索できません", - "discover.docViews.table.unpinFieldAriaLabel": "フィールドを固定解除", - "discover.docViews.table.unpinFieldLabel": "フィールドを固定解除", + "unifiedDocViewer.docViews.table.tableTitle": "表", + "unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel": "表の列を切り替える", + "unifiedDocViewer.docViews.table.toggleColumnInTableButtonTooltip": "表の列を切り替える", + "unifiedDocViewer.fieldChooser.discoverField.name": "フィールド詳細を切り替える", + "unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "メタフィールドの有無でフィルタリングできません", + "unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "スクリプトフィールドの有無でフィルタリングできません", + "unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "インデックスがないフィールドまたは無視された値は検索できません", + "unifiedDocViewer.docViews.table.unpinFieldAriaLabel": "フィールドを固定解除", + "unifiedDocViewer.docViews.table.unpinFieldLabel": "フィールドを固定解除", "discover.dropZoneTableLabel": "フィールドを列として表に追加するには、ゾーンをドロップします", "discover.dscTour.stepAddFields.imageAltText": "[使用可能なフィールド]リストで、プラスアイコンをクリックし、フィールドをドキュメントテーブルに切り替えます。", "discover.dscTour.stepAddFields.title": "フィールドをテーブルに追加", @@ -2374,47 +2364,32 @@ "discover.embeddable.search.displayName": "検索", "discover.errorCalloutShowErrorMessage": "詳細を表示", "discover.fieldChooser.availableFieldsTooltip": "フィールドをテーブルに表示できます。", - "discover.fieldChooser.discoverField.actions": "アクション", + "unifiedDocViewer.fieldChooser.discoverField.actions": "アクション", "discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加", - "discover.fieldChooser.discoverField.multiField": "複数フィールド", - "discover.fieldChooser.discoverField.multiFieldTooltipContent": "複数フィールドにはフィールドごとに複数の値を入力できます", - "discover.fieldChooser.discoverField.name": "フィールド", + "unifiedDocViewer.fieldChooser.discoverField.multiField": "複数フィールド", + "unifiedDocViewer.fieldChooser.discoverField.multiFieldTooltipContent": "複数フィールドにはフィールドごとに複数の値を入力できます", + "unifiedDocViewer.fieldChooser.discoverField.name": "フィールド", "discover.fieldChooser.discoverField.removeFieldTooltip": "フィールドを表から削除", - "discover.fieldChooser.discoverField.value": "値", + "unifiedDocViewer.fieldChooser.discoverField.value": "値", "discover.goToDiscoverButtonText": "Discoverに移動", - "discover.grid.closePopover": "ポップオーバーを閉じる", - "discover.grid.copyCellValueButton": "値をコピー", - "discover.grid.copyColumnNameToClipboard.toastTitle": "クリップボードにコピーされました", - "discover.grid.copyColumnNameToClipBoardButton": "名前をコピー", - "discover.grid.copyColumnValuesToClipBoardButton": "列をコピー", - "discover.grid.copyEscapedValueWithFormulasToClipboardWarningText": "値にはエスケープされた式を含めることができます。", - "discover.grid.copyFailedErrorText": "このブラウザーではクリップボードにコピーできません", - "discover.grid.copyValueToClipboard.toastTitle": "クリップボードにコピーされました", - "discover.grid.documentHeader": "ドキュメント", - "discover.grid.editFieldButton": "データビューフィールドを編集", - "discover.grid.filterFor": "フィルター", - "discover.grid.filterOut": "除外", "discover.grid.flyout.documentNavigation": "ドキュメントナビゲーション", "discover.grid.flyout.toastColumnAdded": "列'{columnName}'が追加されました", "discover.grid.flyout.toastColumnRemoved": "列'{columnName}'が削除されました", - "discover.grid.selectDoc": "ドキュメント'{rowNumber}'を選択", "discover.grid.tableRow.detailHeading": "拡張ドキュメント", "discover.grid.tableRow.textBasedDetailHeading": "展開された行", "discover.grid.tableRow.viewSingleDocumentLinkTextSimple": "1つのドキュメント", "discover.grid.tableRow.viewSurroundingDocumentsHover": "このドキュメントの前後に出現したドキュメントを検査します。周りのドキュメントビューでは、固定されたフィルターのみがアクティブのままです。", "discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple": "周りのドキュメント", "discover.grid.tableRow.viewText": "表示:", - "discover.grid.viewDoc": "詳細ダイアログを切り替え", "discover.helpMenu.appName": "Discover", "discover.inspectorRequestDataTitleDocuments": "ドキュメント", "discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、ドキュメントを取得します。", "discover.invalidFiltersWarnToast.description": "一部の適用されたフィルターのデータビューID参照は、現在のデータビューとは異なります。", "discover.invalidFiltersWarnToast.title": "別のインデックス参照", - "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", - "discover.json.copyToClipboardLabel": "クリップボードにコピー", + "unifiedDocViewer.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", + "unifiedDocViewer.json.copyToClipboardLabel": "クリップボードにコピー", "discover.loadingDocuments": "ドキュメントを読み込み中", - "discover.loadingJSON": "JSONを読み込んでいます", - "discover.loadingResults": "結果を読み込み中", + "unifiedDocViewer.loadingJSON": "JSONを読み込んでいます", "discover.localMenu.alertsDescription": "アラート", "discover.localMenu.fallbackReportTitle": "無題のDiscover検索", "discover.localMenu.inspectTitle": "検査", @@ -2457,28 +2432,23 @@ "discover.noResults.suggestion.syntaxPopoverExampleHeader": "例", "discover.noResults.suggestion.tryText": "次の方法を試してください:", "discover.noResults.suggestion.viewAllMatchesButtonText": "すべての一致を表示", - "discover.noResultsFound": "結果が見つかりませんでした", "discover.notifications.invalidTimeRangeText": "指定された時間範囲が無効です。(開始:'{from}'、終了:'{to}')", "discover.notifications.invalidTimeRangeTitle": "無効な時間範囲", "discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。", "discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。", "discover.pageTitleWithoutSavedSearch": "Discover - 検索は保存されていません", "discover.reloadSavedSearchButton": "検索をリセット", - "discover.removeColumnLabel": "列を削除", "discover.rootBreadcrumb": "Discover", "discover.sampleData.viewLinkLabel": "Discover", "discover.savedSearch.savedObjectName": "保存検索", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "Discoverで開く", "discover.searchingTitle": "検索中", - "discover.selectColumnHeader": "列を選択", "discover.serverLocatorExtension.titleFromLocatorUnknown": "不明な検索", - "discover.showAllDocuments": "すべてのドキュメントを表示", - "discover.showSelectedDocumentsOnly": "選択したドキュメントのみを表示", "discover.singleDocRoute.errorTitle": "エラーが発生しました", "discover.skipToBottomButtonLabel": "テーブルの最後に移動", - "discover.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。", - "discover.sourceViewer.errorMessageTitle": "エラーが発生しました", - "discover.sourceViewer.refresh": "更新", + "unifiedDocViewer.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。", + "unifiedDocViewer.sourceViewer.errorMessageTitle": "エラーが発生しました", + "unifiedDocViewer.sourceViewer.refresh": "更新", "discover.toggleSidebarAriaLabel": "サイドバーを切り替える", "discover.topNav.openSearchPanel.manageSearchesButtonLabel": "検索の管理", "discover.topNav.openSearchPanel.noSearchesFoundDescription": "一致する検索が見つかりませんでした。", @@ -2495,6 +2465,25 @@ "discover.viewAlert.searchSourceErrorTitle": "検索ソースの取得エラー", "discover.viewModes.document.label": "ドキュメント", "discover.viewModes.fieldStatistics.label": "フィールド統計情報", + "unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel": "{timeFieldName} - このフィールドはイベントの発生時刻を表します。", + "unifiedDataTable.searchGenerationWithDescription": "検索{searchTitle}で生成されたテーブル", + "unifiedDataTable.searchGenerationWithDescriptionGrid": "検索{searchTitle}で生成されたテーブル({searchDescription})", + "unifiedDataTable.selectedDocumentsNumber": "{nr}個のドキュメントが選択されました", + "unifiedDataTable.clearSelection": "選択した項目をクリア", + "unifiedDataTable.controlColumnHeader": "列の制御", + "unifiedDataTable.copyToClipboardJSON": "ドキュメントをクリップボードにコピー(JSON)", + "unifiedDataTable.tableHeader.timeFieldIconTooltip": "このフィールドはイベントの発生時刻を表します。", + "unifiedDataTable.grid.copyColumnNameToClipBoardButton": "名前をコピー", + "unifiedDataTable.grid.copyColumnValuesToClipBoardButton": "列をコピー", + "unifiedDataTable.grid.documentHeader": "ドキュメント", + "unifiedDataTable.grid.editFieldButton": "データビューフィールドを編集", + "unifiedDataTable.grid.selectDoc": "ドキュメント'{rowNumber}'を選択", + "unifiedDataTable.loadingResults": "結果を読み込み中", + "unifiedDataTable.noResultsFound": "結果が見つかりませんでした", + "unifiedDataTable.removeColumnLabel": "列を削除", + "unifiedDataTable.selectColumnHeader": "列を選択", + "unifiedDataTable.showAllDocuments": "すべてのドキュメントを表示", + "unifiedDataTable.showSelectedDocumentsOnly": "選択したドキュメントのみを表示", "domDragDrop.announce.cancelled": "移動がキャンセルされました。{label}は初期位置に戻りました", "domDragDrop.announce.cancelledItem": "移動がキャンセルされました。{label}は位置{position}の{groupLabel}グループに戻りました", "domDragDrop.announce.dropped.combineCompatible": "レイヤー{dropLayerNumber}の位置{dropPosition}でグループ{dropGroupLabel}の{dropLabel}にグループ{groupLabel}の{label}を結合しました。", @@ -5712,34 +5701,34 @@ "unifiedFieldList.fieldNameDescription.textField": "電子メール本文や製品説明などの全文テキスト。", "unifiedFieldList.fieldNameDescription.unknownField": "不明なフィールド", "unifiedFieldList.fieldNameDescription.versionField": "ソフトウェアバージョン。「セマンティックバージョニング」優先度ルールをサポートします。", - "unifiedFieldList.fieldNameIcons.binaryAriaLabel": "バイナリー", - "unifiedFieldList.fieldNameIcons.booleanAriaLabel": "ブール", - "unifiedFieldList.fieldNameIcons.conflictFieldAriaLabel": "競合", - "unifiedFieldList.fieldNameIcons.counterFieldAriaLabel": "カウンターメトリック", - "unifiedFieldList.fieldNameIcons.dateFieldAriaLabel": "日付", - "unifiedFieldList.fieldNameIcons.dateRangeFieldAriaLabel": "日付範囲", - "unifiedFieldList.fieldNameIcons.denseVectorFieldAriaLabel": "密集ベクトル", - "unifiedFieldList.fieldNameIcons.flattenedFieldAriaLabel": "平坦化済み", - "unifiedFieldList.fieldNameIcons.gaugeFieldAriaLabel": "ゲージメトリック", - "unifiedFieldList.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイント", - "unifiedFieldList.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報図形", - "unifiedFieldList.fieldNameIcons.histogramFieldAriaLabel": "ヒストグラム", - "unifiedFieldList.fieldNameIcons.ipAddressFieldAriaLabel": "IP アドレス", - "unifiedFieldList.fieldNameIcons.ipRangeFieldAriaLabel": "IP範囲", - "unifiedFieldList.fieldNameIcons.keywordFieldAriaLabel": "キーワード", - "unifiedFieldList.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3", - "unifiedFieldList.fieldNameIcons.nestedFieldAriaLabel": "ネスト済み", - "unifiedFieldList.fieldNameIcons.numberFieldAriaLabel": "数字", - "unifiedFieldList.fieldNameIcons.pointFieldAriaLabel": "点", - "unifiedFieldList.fieldNameIcons.rankFeatureFieldAriaLabel": "ランク特性", - "unifiedFieldList.fieldNameIcons.rankFeaturesFieldAriaLabel": "ランク特性", - "unifiedFieldList.fieldNameIcons.recordAriaLabel": "記録", - "unifiedFieldList.fieldNameIcons.shapeFieldAriaLabel": "形状", - "unifiedFieldList.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド", - "unifiedFieldList.fieldNameIcons.stringFieldAriaLabel": "文字列", - "unifiedFieldList.fieldNameIcons.textFieldAriaLabel": "テキスト", - "unifiedFieldList.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド", - "unifiedFieldList.fieldNameIcons.versionFieldAriaLabel": "バージョン", + "discover.fieldNameIcons.binaryAriaLabel": "バイナリー", + "discover.fieldNameIcons.booleanAriaLabel": "ブール", + "discover.fieldNameIcons.conflictFieldAriaLabel": "競合", + "discover.fieldNameIcons.counterFieldAriaLabel": "カウンターメトリック", + "discover.fieldNameIcons.dateFieldAriaLabel": "日付", + "discover.fieldNameIcons.dateRangeFieldAriaLabel": "日付範囲", + "discover.fieldNameIcons.denseVectorFieldAriaLabel": "密集ベクトル", + "discover.fieldNameIcons.flattenedFieldAriaLabel": "平坦化済み", + "discover.fieldNameIcons.gaugeFieldAriaLabel": "ゲージメトリック", + "discover.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイント", + "discover.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報図形", + "discover.fieldNameIcons.histogramFieldAriaLabel": "ヒストグラム", + "discover.fieldNameIcons.ipAddressFieldAriaLabel": "IP アドレス", + "discover.fieldNameIcons.ipRangeFieldAriaLabel": "IP範囲", + "discover.fieldNameIcons.keywordFieldAriaLabel": "キーワード", + "discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3", + "discover.fieldNameIcons.nestedFieldAriaLabel": "ネスト済み", + "discover.fieldNameIcons.numberFieldAriaLabel": "数字", + "discover.fieldNameIcons.pointFieldAriaLabel": "点", + "discover.fieldNameIcons.rankFeatureFieldAriaLabel": "ランク特性", + "discover.fieldNameIcons.rankFeaturesFieldAriaLabel": "ランク特性", + "discover.fieldNameIcons.recordAriaLabel": "記録", + "discover.fieldNameIcons.shapeFieldAriaLabel": "形状", + "discover.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド", + "discover.fieldNameIcons.stringFieldAriaLabel": "文字列", + "discover.fieldNameIcons.textFieldAriaLabel": "テキスト", + "discover.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド", + "discover.fieldNameIcons.versionFieldAriaLabel": "バージョン", "unifiedFieldList.fieldNameSearch.filterByNameLabel": "検索フィールド名", "unifiedFieldList.fieldPopover.addExistsFilterLabel": "フィールド表示のフィルター", "unifiedFieldList.fieldPopover.deleteFieldLabel": "データビューフィールドを削除", @@ -5940,9 +5929,6 @@ "unifiedSearch.query.queryBar.indexPattern.findFilterSet": "クエリを検索", "unifiedSearch.query.queryBar.indexPattern.manageFieldButton": "このデータビューを管理", "unifiedSearch.query.queryBar.indexPattern.temporaryDataviewLabel": "一時", - "unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning": "データビューを切り替えると、現在のSQLクエリが削除されます。この検索を保存すると、作業内容が失われないことが保証されます。", - "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesLabel": "テキストベースのクエリ言語", - "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody": "データビューを切り替えると、現在のSQLクエリが削除されます。この検索を保存すると、作業内容が失われないことが保証されます。", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton": "保存せずに切り替え", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalDismissButton": "次回以降この警告を表示しない", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalSaveButton": "保存して切り替え", @@ -27639,8 +27625,6 @@ "xpack.observability.slo.sloEdit.sliType.customKql.goodQuery.tooltip": "このKQLクエリは、SLOを計算する目的で、「良好」または「成功」と見なされるイベントのサブセットを返します。このクエリは、ステータスコード、エラー、メッセージ、または他の関連するフィールドなどの一部の関連する条件に基づいて、イベントをフィルタリングします。", "xpack.observability.slo.sloEdit.sliType.customKql.goodQueryPlaceholder": "良いイベントを定義", "xpack.observability.slo.sloEdit.sliType.customKql.queryFilter": "クエリのフィルター", - "xpack.observability.slo.sloEdit.sliType.customKql.timestampField.label": "タイムスタンプフィールド", - "xpack.observability.slo.sloEdit.sliType.customKql.timestampField.placeholder": "タイムスタンプフィールドを選択", "xpack.observability.slo.sloEdit.sliType.customKql.totalQuery": "合計クエリ", "xpack.observability.slo.sloEdit.sliType.customKql.totalQuery.tooltip": "このKQLクエリは、良好なイベントと問題があるイベントの両方を含む、SLO計算に関連するすべてのイベントを返します。", "xpack.observability.slo.sloEdit.sliType.customKql.totalQueryPlaceholder": "合計イベントを定義", @@ -27654,8 +27638,6 @@ "xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder": "メトリックフィールドを選択", "xpack.observability.slo.sloEdit.sliType.customMetric.queryFilter": "クエリのフィルター", "xpack.observability.slo.sloEdit.sliType.customMetric.sumLabel": "の合計", - "xpack.observability.slo.sloEdit.sliType.customMetric.timestampField.label": "タイムスタンプフィールド", - "xpack.observability.slo.sloEdit.sliType.customMetric.timestampField.placeholder": "タイムスタンプフィールドを選択", "xpack.observability.slo.sloEdit.tags.label": "タグ", "xpack.observability.slo.sloEdit.tags.placeholder": "タグを追加", "xpack.observability.slo.sloEdit.targetSlo.label": "目標 / SLO(%)", @@ -33185,43 +33167,6 @@ "xpack.securitySolution.expandedValue.showTopN.showTopValues": "上位の値を表示", "xpack.securitySolution.explore.landing.pageTitle": "探索", "xpack.securitySolution.featureCatalogueDescription": "インフラストラクチャー全体の統合保護のため、脅威を防止、収集、検出し、それに対応します。", - "xpack.securitySolution.featureRegistry.deleteSubFeatureDetails": "ケースとコメントを削除", - "xpack.securitySolution.featureRegistry.deleteSubFeatureName": "削除", - "xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle": "ケース", - "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "セキュリティ", - "xpack.securitySolution.featureRegistry.subFeatures.blockList": "ブロックリスト", - "xpack.securitySolution.featureRegistry.subFeatures.blockList.description": "Elastic Defendの悪意のあるプロセスに対する保護機能を拡張し、潜在的に有害なアプリケーションから保護します。", - "xpack.securitySolution.featureRegistry.subFeatures.blockList.privilegesTooltip": "ブロックリストのアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList": "エンドポイントリスト", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList.description": "Elastic Defendを実行しているすべてのホストと、関連する統合の詳細が表示されます。", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList.privilegesTooltip": "エンドポイントリストのアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters": "イベントフィルター", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters.description": "Elasticsearchに保存する必要のない、あるいは保存しないエンドポイントイベントをフィルターします。", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters.privilegesTooltip": "イベントフィルターのアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations": "実行操作", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations.description": "エンドポイントでスクリプトを実行します。", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations.privilegesTooltip": "実行操作のアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations": "ファイル操作", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations.description": "対応コンソールでファイル関連の対応アクションを実行します。", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations.privilegesTooltip": "ファイル操作のアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation": "ホスト分離", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.description": "「isolate」および「release」応答アクションを実行します。", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.privilegesTooltip": "ホスト分離のアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions": "ホスト分離例外", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.description": "ネットワークの他の部分から分離された場合でも、分離されたホストが通信することを許可する特定のIPアドレスを追加します。", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip": "ホスト分離例外のアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement": "Elastic Defendポリシー管理", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement.description": "Elastic Defendの統合ポリシーにアクセスし、プロテクション、イベント収集、および高度なポリシー機能を設定することができます。", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement.privilegesTooltip": "ポリシー管理のアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations": "プロセス操作", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations.description": "対応コンソールでプロセス関連の対応アクションを実行します。", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations.privilegesTooltip": "プロセス操作のアクセスには、すべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory": "対応アクション履歴", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.description": "エンドポイントで実行された対応アクションの履歴を表示します。", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip": "対応アクション履歴アクセスにはすべてのスペースが必要です。", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications": "信頼できるアプリケーション", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.description": "他のソフトウェア(通常は他のウイルス対策またはエンドポイントセキュリティアプリケーション)との競合を軽減することができます。", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.privilegesTooltip": "信頼できるアプリケーションのアクセスには、すべてのスペースが必要です。", "xpack.securitySolution.fieldBrowser.actionsLabel": "アクション", "xpack.securitySolution.fieldBrowser.categoryLabel": "カテゴリー", "xpack.securitySolution.fieldBrowser.createFieldButton": "フィールドを作成", @@ -33324,14 +33269,11 @@ "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "IPアドレス", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "名前", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "関連するホスト", - "xpack.securitySolution.flyout.entities.relatedHostsToolTip": "アラート後、ユーザーはこれらのホストへの認証に成功しました。", "xpack.securitySolution.flyout.entities.relatedUsersTitle": "関連するユーザー", - "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "アラート後、ユーザーは影響を受けるホストへの認証に成功しました。", "xpack.securitySolution.flyout.entities.usersInfoTitle": "ユーザー情報", "xpack.securitySolution.flyout.prevalenceErrorMessage": "発生率", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "アラート件数", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "ドキュメントカウント", - "xpack.securitySolution.flyout.response.empty": "このイベントに対する対応アクションは定義されていません。", "xpack.securitySolution.flyout.response.title": "対応", "xpack.securitySolution.flyout.sessionViewErrorMessage": "セッションビュー", "xpack.securitySolution.footer.autoRefreshActiveDescription": "自動更新アクション", @@ -34658,10 +34600,6 @@ "xpack.serverlessSearch.apiKey.roleDescriptorsLinkLabel": "ロール記述子を構造化する方法をご覧ください", "xpack.serverlessSearch.apiKey.setup.description": "APIキーを作成するための基本構成詳細情報。", "xpack.serverlessSearch.apiKey.setup.title": "セットアップ", - "xpack.serverlessSearch.apiKey.stepOneDescription": "認証と認可のための一意の識別子。", - "xpack.serverlessSearch.apiKey.stepOneTitle": "APIキーを生成して保存", - "xpack.serverlessSearch.apiKey.stepTwoDescription": "特定のプロジェクトの一意の識別子。", - "xpack.serverlessSearch.apiKey.stepTwoTitle": "一意のCloud IDを保存", "xpack.serverlessSearch.apiKey.title": "APIキーとCloud IDを保存", "xpack.serverlessSearch.apiKey.userFieldHelpText": "APIキーを作成するユーザーのID。", "xpack.serverlessSearch.apiKey.userFieldLabel": "ユーザー", @@ -34672,20 +34610,6 @@ "xpack.serverlessSearch.configureClient.title": "クライアントを構成", "xpack.serverlessSearch.disabled": "無効", "xpack.serverlessSearch.enabled": "有効", - "xpack.serverlessSearch.footer.billing.title": "請求と使用状況", - "xpack.serverlessSearch.footer.community.title": "コミュニティに参加", - "xpack.serverlessSearch.footer.discoverCard.buttonText": "Discoverでデータを探索", - "xpack.serverlessSearch.footer.discoverCard.description": "Discoverを使えば、データをすばやく検索してフィルタリングし、フィールドの構造に関する情報を取得し、調査結果を可視化して表示できます。", - "xpack.serverlessSearch.footer.discoverCard.title": "Discoverでデータを探索して可視化", - "xpack.serverlessSearch.footer.feedback.title": "フィードバックを作成する", - "xpack.serverlessSearch.footer.inviteUsers.title": "その他のユーザーを招待", - "xpack.serverlessSearch.footer.pipelinesCard.buttonText": "インジェストパイプラインを構成", - "xpack.serverlessSearch.footer.pipelinesCard.description": "Elasticsearchでインデックス化する前にデータを前処理します。ELSERなどの機械学習モデルを使用して、フィールドを削除したり、テキストから値を抽出したり、データを強化したりします。", - "xpack.serverlessSearch.footer.pipelinesCard.title": "パイプラインを使用してデータを変換", - "xpack.serverlessSearch.footer.searchUI.buttonText": "Search UIで構築", - "xpack.serverlessSearch.footer.searchUI.description": "Search UIはElasticが管理している無料のオープンソースJavaScriptライブラリで、モダンで魅力的な検索エクスペリエンスをすばやく開発できます。", - "xpack.serverlessSearch.footer.searchUI.title": "Search UIでユーザーインターフェースを構築", - "xpack.serverlessSearch.footer.title": "次のステップ", "xpack.serverlessSearch.header.title": "Elasticsearchをはじめよう", "xpack.serverlessSearch.invalidJsonError": "無効なJSON", "xpack.serverlessSearch.languages.cURL": "cURL", @@ -37638,7 +37562,6 @@ "xpack.synthetics.synthetics.waterfallChart.labels.timings.ssl": "TLS", "xpack.synthetics.synthetics.waterfallChart.labels.timings.wait": "待機中(TTFB)", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", - "xpack.synthetics.syntheticsMonitors": "アップタイム - モニター", "xpack.synthetics.testDetails.after": "後 ", "xpack.synthetics.testDetails.codeExecuted": "コードが実行されました", "xpack.synthetics.testDetails.console": "コンソール", @@ -37763,26 +37686,19 @@ "xpack.transform.alertTypes.transformHealth.healthCheckRecoveryMessage": "{count, plural, other {トランスフォーム}} {transformsString} {count, plural, other {あります}}は正常です。", "xpack.transform.alertTypes.transformHealth.notStartedMessage": "{count, plural, other {トランスフォーム}}\"{transformsString}\"{count, plural, other {あります}}開始されていません。", "xpack.transform.alertTypes.transformHealth.notStartedRecoveryMessage": "{count, plural, other {トランスフォーム}}\"{transformsString}\"{count, plural, other {あります}}開始しました。", - "xpack.transform.app.deniedPrivilegeDescription": "変換のこのセクションを使用するには、{privilegesCount, plural, other {これらのクラスター権限}}が必要です:{missingPrivileges}", "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message} 管理者にお問い合わせください。", "xpack.transform.clone.noDataViewErrorPromptText": "トランスフォーム{transformId}を複製できません。{dataViewTitle}のデータビューは存在しません。", "xpack.transform.danglingTasksError": "{count}個の{count, plural, other {変換}}に構成の詳細がありません:[{transformIds}]。{count, plural, other {それら}}回復できないため、削除してください。", "xpack.transform.deleteTransform.deleteAnalyticsWithDataViewErrorMessage": "データビュー{destinationIndex}の削除中にエラーが発生しました", - "xpack.transform.deleteTransform.deleteAnalyticsWithDataViewSuccessMessage": "データビュー {destinationIndex} の削除リクエストが受け付けられました。", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage": "ディスティネーションインデックス{destinationIndex}を削除する要求が確認されました。", "xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage": "データビュー{dataView}が存在するかどうかを確認するときにエラーが発生しました:{error}", "xpack.transform.edit.noDataViewErrorPromptText": "変換{transformId}のデータビューを取得できません。{dataViewTitle}のデータビューは存在しません。", "xpack.transform.forceDeleteTransformMessage": "{count}{count, plural, other {変換}}の削除", "xpack.transform.managedTransformsWarningCallout": "{count, plural, other {これらの変換のうちの少なくとも1個の変換}}はElasticによってあらかじめ構成されています。{count, plural, other {それらを}}{action}すると、製品の他の部分に影響する可能性があります。", "xpack.transform.multiTransformActionsMenu.transformsCount": "{count}個の{count, plural, other {変換}}を選択済み", "xpack.transform.stepCreateForm.createDataViewErrorMessage": "Kibanaデータビュー{dataViewName}の作成中にエラーが発生しました:", - "xpack.transform.stepCreateForm.createDataViewSuccessMessage": "Kibanaデータビュー{dataViewName}が正常に作成されました。", "xpack.transform.stepCreateForm.createTransformErrorMessage": "変換 {transformId} の取得中にエラーが発生しました。", - "xpack.transform.stepCreateForm.createTransformSuccessMessage": "変換 {transformId} の作成リクエストが受け付けられました。", "xpack.transform.stepCreateForm.duplicateDataViewErrorMessage": "Kibanaデータビュー{dataViewName}の作成中にエラーが発生しました:データビューはすでに存在します。", - "xpack.transform.stepCreateForm.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました。", - "xpack.transform.stepCreateForm.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", "xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar": "無効なクエリ:{queryErrorMessage}", "xpack.transform.stepDefineForm.queryPlaceholderKql": "例: {example}.", "xpack.transform.stepDefineForm.queryPlaceholderLucene": "例: {example}.", @@ -37794,41 +37710,30 @@ "xpack.transform.stepDetailsForm.retentionPolicyMaxAgePlaceholderText": "max_age 例:{exampleValue}", "xpack.transform.transformForm.sizeNotationPlaceholder": "例:{example1}, {example2}, {example3}, {example4}", "xpack.transform.transformList.alertingRules.tooltipContent": "変換には{rulesCount}個の関連付けられたアラート{rulesCount, plural, other { ルール}}があります", - "xpack.transform.transformList.bulkDeleteDestDataViewSuccessMessage": "{count}個のディスティネーションデータ{count, plural, other {ビュー}}が正常に削除されました。", - "xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage": "{count}個のディスティネーション{count, plural, other {インデックス}}が正常に削除されました。", "xpack.transform.transformList.bulkDeleteModalTitle": "{count}{count, plural, other {変換}}を削除しますか?", - "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "{count}個の{count, plural, other {トランスフォーム}}が正常に削除されました。", "xpack.transform.transformList.bulkReauthorizeModalTitle": "{count} {count, plural, other {トランスフォーム}}をもう一度認可しますか?", "xpack.transform.transformList.bulkResetModalTitle": "{count}個の{count, plural, other {変換}}をリセットしますか?", - "xpack.transform.transformList.bulkResetTransformSuccessMessage": "{count}個の{count, plural, other {変換}}が正常にリセットされました。", "xpack.transform.transformList.bulkStartModalTitle": "{count}個の{count, plural, other {変換}}を開始しますか?", "xpack.transform.transformList.bulkStopModalTitle": "{count}個のを{count, plural, other {変換}}停止しますか?", "xpack.transform.transformList.cannotRestartCompleteBatchTransformToolTip": "{transformId} は完了済みの一斉変換で、再度開始できません。", "xpack.transform.transformList.cannotScheduleNowCompleteBatchTransformToolTip": "{transformId}は完了済みのバッチ変換であるため、データの即時処理のスケジュールを設定できません。", "xpack.transform.transformList.deleteModalTitle": "{transformId}を削除しますか?", "xpack.transform.transformList.deleteTransformErrorMessage": "変換 {transformId} の削除中にエラーが発生しました", - "xpack.transform.transformList.deleteTransformSuccessMessage": "変換 {transformId} の削除リクエストが受け付けられました。", "xpack.transform.transformList.editFlyoutFormPlaceholderText": "デフォルト:{defaultValue}", "xpack.transform.transformList.editFlyoutTitle": "{transformId}の編集", - "xpack.transform.transformList.editTransformSuccessMessage": "変換{transformId}が更新されました。", "xpack.transform.transformList.reauthorizeModalTitle": "{transformId}をもう一度許可しますか?", "xpack.transform.transformList.reauthorizeTransformErrorMessage": "変換{transformId}の再認可中にエラーが発生しました", - "xpack.transform.transformList.reauthorizeTransformSuccessMessage": "変換{transformId}の再認可リクエストが受け付けられました。", "xpack.transform.transformList.resetModalTitle": "{transformId}をリセットしますか?", "xpack.transform.transformList.resetTransformErrorMessage": "変換{transformId}のリセット中にエラーが発生しました", - "xpack.transform.transformList.resetTransformSuccessMessage": "変換{transformId}のリセットリクエストが受け付けられました。", "xpack.transform.transformList.rowCollapse": "{transformId} の詳細を非表示", "xpack.transform.transformList.rowExpand": "{transformId} の詳細を表示", "xpack.transform.transformList.scheduleNowTransformErrorMessage": "変換{transformId}でデータの即時処理のスケジュールの設定中にエラーが発生しました。", - "xpack.transform.transformList.scheduleNowTransformSuccessMessage": "変換{transformId}でデータの即時処理のスケジュールを設定するリクエストが受け付けられました。", "xpack.transform.transformList.startedTransformToolTip": "{transformId} はすでに開始済みです。", "xpack.transform.transformList.startModalTitle": "{transformId}を開始しますか?", "xpack.transform.transformList.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました", - "xpack.transform.transformList.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", "xpack.transform.transformList.stopModalTitle": "{transformId}を終了しますか?", "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} はすでに停止済みです。", "xpack.transform.transformList.stopTransformErrorMessage": "データフレーム変換 {transformId} の停止中にエラーが発生しました", - "xpack.transform.transformList.stopTransformSuccessMessage": "データフレーム変換 {transformId} の停止リクエストが受け付けられました。", "xpack.transform.transformList.unauthorizedTransformsCallout.insufficientPermissionsMsg": "{unauthorizedCnt, plural, other {十分な権限がない状態で#個の変換が作成されました。}}", "xpack.transform.transformList.unauthorizedTransformsCallout.reauthorizeMsg": "もう一度認可して、{unauthorizedCnt, plural, other {#個の変換}}を開始してください。", "xpack.transform.transformNodes.noTransformNodesCallOutBody": "変換の作成または実行はできません。{learnMoreLink}。", @@ -37874,9 +37779,6 @@ "xpack.transform.alertTypes.transformHealth.notStartedCheckName": "トランスフォームが開始していません", "xpack.transform.alertTypes.transformHealth.testsConfigTransforms.errorMessage": "1つ以上の正常性チェックを選択する必要があります", "xpack.transform.alertTypes.transformHealth.testsSelection.enableTestLabel": "有効にする", - "xpack.transform.app.checkingPrivilegesDescription": "権限を確認中…", - "xpack.transform.app.checkingPrivilegesErrorMessage": "サーバーからユーザー特権を取得しているときにエラーが発生しました", - "xpack.transform.app.deniedPrivilegeTitle": "クラスター特権が足りません", "xpack.transform.appName": "トランスフォーム", "xpack.transform.appTitle": "トランスフォーム", "xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip": "トランスフォームアラートルールを作成するアクセス権がありません。", @@ -37922,7 +37824,6 @@ "xpack.transform.licenseCheckErrorMessage": "ライセンス確認失敗", "xpack.transform.list.emptyPromptButtonText": "初めてのトランスフォームを作成", "xpack.transform.list.emptyPromptTitle": "トランスフォームが見つかりません", - "xpack.transform.list.errorPromptTitle": "トランスフォームリストの取得中にエラーが発生しました", "xpack.transform.mode": "モード", "xpack.transform.modeFilter": "モード", "xpack.transform.models.transformService.allOtherRequestsCancelledDescription": "他のすべてのリクエストはキャンセルされました。", @@ -38154,7 +38055,6 @@ "xpack.transform.transformList.editFlyoutUpdateButtonText": "更新", "xpack.transform.transformList.editManagedTransformsDescription": "編集中", "xpack.transform.transformList.editTransformGenericErrorMessage": "トランスフォームを削除するためのAPIエンドポイントの呼び出し中にエラーが発生しました。", - "xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "ユーザーがディスティネーションインデックスを削除できるかどうかを確認するときにエラーが発生しました。", "xpack.transform.transformList.managedBadgeLabel": "管理中", "xpack.transform.transformList.managedBadgeTooltip": "このトランスフォームはElasticによってあらかじめ構成および管理されています。製品の他の部分はその動作に依存関係が存在している場合があります。", "xpack.transform.transformList.needsReauthorizationBadge.contactAdminTooltip": "必要な権限をリクエストするには、管理者に問い合わせてください。", @@ -38435,15 +38335,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "AND", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "タイミング", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "タイミング", - "xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle": "変数を追加", - "xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "変数を追加", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreHidden": "廃止予定の変数は非表示です", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreShown": "廃止予定の変数は表示されます", - "xpack.triggersActionsUI.components.addMessageVariables.hideDeprecatedVariables": "非表示", - "xpack.triggersActionsUI.components.addMessageVariables.loadingMessage": "変数を読み込み中", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesAvailable": "変数がありません", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesFound": "変数が見つかりません", - "xpack.triggersActionsUI.components.addMessageVariables.showAllDeprecatedVariables": "すべて表示", "xpack.triggersActionsUI.components.alertTable.useFetchAlerts.errorMessageText": "アラート検索でエラーが発生しました", "xpack.triggersActionsUI.components.alertTable.useFetchBrowserFieldsCapabilities.errorMessageText": "ブラウザーフィールドの読み込み中にエラーが発生しました", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "選択…", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 57172509567f9..e96481d694485 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2170,7 +2170,7 @@ "discover.advancedSettings.disableDocumentExplorerDescription": "要使用新的 {documentExplorerDocs},而非经典视图,请关闭此选项。Document Explorer 提供了更合理的数据排序、可调整大小的列和全屏视图。", "discover.advancedSettings.discover.showFieldStatisticsDescription": "启用 {fieldStatisticsDocs} 以显示详细信息,如数字字段的最小和最大值,或地理字段的地图。此功能为公测版,可能会进行更改。", "discover.advancedSettings.discover.showMultifieldsDescription": "控制 {multiFields} 是否显示在展开的文档视图中。多数情况下,多字段与原始字段相同。此选项仅在 `searchFieldsFromSource` 关闭时可用。", - "discover.advancedSettings.enableSQLDescription": "{technicalPreviewLabel} 此技术预览功能为高度实验性功能 -- 请勿在生产已保存搜索、可视化或仪表板中依赖此功能。此设置在 Discover 和 Lens 中将 SQL 用作基于文本的查询语言。如果具有与此体验有关的反馈,请通过 {link} 联系我们", + "discover.advancedSettings.enableESQLDescription": "{technicalPreviewLabel} 此技术预览功能为高度实验性功能 -- 请勿在生产已保存搜索、可视化或仪表板中依赖此功能。此设置在 Discover 和 Lens 中将 SQL 用作基于文本的查询语言。如果具有与此体验有关的反馈,请通过 {link} 联系我们", "discover.context.contextOfTitle": "#{anchorId} 周围的文档", "discover.context.newerDocumentsWarning": "仅可以找到 {docCount} 个比定位标记新的文档。", "discover.context.olderDocumentsWarning": "仅可以找到 {docCount} 个比定位标记旧的文档。", @@ -2193,11 +2193,6 @@ "discover.dscTour.stepAddFields.description": "单击 {plusIcon} 以添加您感兴趣的字段。", "discover.dscTour.stepExpand.description": "单击 {expandIcon} 以查看、比较和筛选文档。", "discover.errorCalloutFormattedTitle": "{title}: {errorMessage}", - "discover.grid.copyClipboardButtonTitle": "复制 {column} 的值", - "discover.grid.copyColumnValuesToClipboard.toastTitle": "“{column}”列的值已复制到剪贴板", - "discover.grid.filterForAria": "筛留此 {value}", - "discover.grid.filterOutAria": "筛除此 {value}", - "discover.gridSampleSize.limitDescription": "搜索结果被限定为 {sampleSize} 个文档。添加更多搜索词以缩小搜索范围。", "discover.howToSeeOtherMatchingDocumentsDescription": "以下是匹配您的搜索的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", "discover.noMatchRoute.bannerText": "Discover 应用程序无法识别此路由:{route}", "discover.noResults.kqlExamples.kqlDescription": "详细了解 {kqlLink}", @@ -2207,9 +2202,6 @@ "discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}", "discover.savedSearchAliasMatchRedirect.objectNoun": "{savedSearch} 搜索", "discover.savedSearchURLConflictCallout.objectNoun": "{savedSearch} 搜索", - "discover.searchGenerationWithDescription": "搜索 {searchTitle} 生成的表", - "discover.searchGenerationWithDescriptionGrid": "搜索 {searchTitle} 生成的表({searchDescription})", - "discover.selectedDocumentsNumber": "{nr} 个文档已选择", "discover.showingDefaultDataViewWarningDescription": "正在显示默认数据视图:“{loadedDataViewTitle}”({loadedDataViewId})", "discover.showingSavedDataViewWarningDescription": "正在显示已保存数据视图:“{ownDataViewTitle}”({ownDataViewId})", "discover.singleDocRoute.errorMessage": "没有与 ID {dataViewId} 相匹配的数据视图", @@ -2235,8 +2227,8 @@ "discover.advancedSettings.docTableHideTimeColumnText": "在 Discover 中和仪表板上的所有已保存搜索中隐藏“时间”列。", "discover.advancedSettings.docTableHideTimeColumnTitle": "隐藏“时间”列", "discover.advancedSettings.documentExplorerLinkText": "Document Explorer", - "discover.advancedSettings.enableSQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", - "discover.advancedSettings.enableSQLTitle": "启用 SQL", + "discover.advancedSettings.enableESQL.discussLinkText": "discuss.elastic.co/c/elastic-stack/kibana", + "discover.advancedSettings.enableESQLTitle": "启用 SQL", "discover.advancedSettings.fieldsPopularLimitText": "要显示的排名前 N 最常见字段", "discover.advancedSettings.fieldsPopularLimitTitle": "常见字段限制", "discover.advancedSettings.maxDocFieldsDisplayedText": "在文档摘要中渲染的最大字段数目", @@ -2262,7 +2254,6 @@ "discover.backToTopLinkText": "返回顶部。", "discover.badge.readOnly.text": "只读", "discover.badge.readOnly.tooltip": "无法保存搜索", - "discover.clearSelection": "清除所选内容", "discover.confirmDataViewSave.cancel": "取消", "discover.confirmDataViewSave.message": "您选择的操作需要已保存的数据视图。", "discover.confirmDataViewSave.saveAndContinue": "保存并继续", @@ -2283,8 +2274,6 @@ "discover.context.unableToLoadAnchorDocumentDescription": "无法加载该定位点文档", "discover.context.unableToLoadDocumentDescription": "无法加载文档", "discover.contextViewRoute.errorTitle": "发生错误", - "discover.controlColumnHeader": "控制列", - "discover.copyToClipboardJSON": "将文档复制到剪贴板 (JSON)", "discover.discoverBreadcrumbTitle": "Discover", "discover.discoverDefaultSearchSessionName": "发现", "discover.discoverDescription": "通过查询和筛选原始文档来以交互方式浏览您的数据。", @@ -2325,35 +2314,36 @@ "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "查看周围文档", "discover.documentsAriaLabel": "文档", "discover.documentsErrorTitle": "搜索错误", - "discover.docView.table.actions.label": "操作", - "discover.docView.table.actions.open": "打开操作", - "discover.docView.table.ignored.multiAboveTooltip": "此字段中的一个或多个值过长,无法搜索或筛选。", - "discover.docView.table.ignored.multiMalformedTooltip": "此字段包含一个或多个格式错误的值,无法搜索或筛选。", - "discover.docView.table.ignored.multiUnknownTooltip": "此字段中的一个或多个值被 Elasticsearch 忽略,无法搜索或筛选。", - "discover.docView.table.ignored.singleAboveTooltip": "此字段中的值过长,无法搜索或筛选。", - "discover.docView.table.ignored.singleMalformedTooltip": "此字段中的值格式错误,无法搜索或筛选。", - "discover.docView.table.ignored.singleUnknownTooltip": "此字段中的值被 Elasticsearch 忽略,无法搜索或筛选。", - "discover.docView.table.searchPlaceHolder": "搜索字段名称", - "discover.docViews.json.jsonTitle": "JSON", - "discover.docViews.table.filterForFieldPresentButtonAriaLabel": "筛留存在的字段", - "discover.docViews.table.filterForFieldPresentButtonTooltip": "字段是否存在筛选", - "discover.docViews.table.filterForValueButtonAriaLabel": "筛留值", - "discover.docViews.table.filterForValueButtonTooltip": "筛留值", - "discover.docViews.table.filterOutValueButtonAriaLabel": "筛除值", - "discover.docViews.table.filterOutValueButtonTooltip": "筛除值", - "discover.docViews.table.ignored.multiValueLabel": "包含被忽略的值", - "discover.docViews.table.ignored.singleValueLabel": "被忽略的值", - "discover.docViews.table.pinFieldAriaLabel": "固定字段", - "discover.docViews.table.pinFieldLabel": "固定字段", + "unifiedDocViewer.docView.table.actions.label": "操作", + "unifiedDocViewer.docView.table.actions.open": "打开操作", + "unifiedDocViewer.docView.table.ignored.multiAboveTooltip": "此字段中的一个或多个值过长,无法搜索或筛选。", + "unifiedDocViewer.docView.table.ignored.multiMalformedTooltip": "此字段包含一个或多个格式错误的值,无法搜索或筛选。", + "unifiedDocViewer.docView.table.ignored.multiUnknownTooltip": "此字段中的一个或多个值被 Elasticsearch 忽略,无法搜索或筛选。", + "unifiedDocViewer.docView.table.ignored.singleAboveTooltip": "此字段中的值过长,无法搜索或筛选。", + "unifiedDocViewer.docView.table.ignored.singleMalformedTooltip": "此字段中的值格式错误,无法搜索或筛选。", + "unifiedDocViewer.docView.table.ignored.singleUnknownTooltip": "此字段中的值被 Elasticsearch 忽略,无法搜索或筛选。", + "unifiedDocViewer.docView.table.searchPlaceHolder": "搜索字段名称", + "unifiedDocViewer.docViews.json.jsonTitle": "JSON", + "unifiedDocViewer.docViews.table.filterForFieldPresentButtonAriaLabel": "筛留存在的字段", + "unifiedDocViewer.docViews.table.filterForFieldPresentButtonTooltip": "字段是否存在筛选", + "unifiedDocViewer.docViews.table.filterForValueButtonAriaLabel": "筛留值", + "unifiedDocViewer.docViews.table.filterForValueButtonTooltip": "筛留值", + "unifiedDocViewer.docViews.table.filterOutValueButtonAriaLabel": "筛除值", + "unifiedDocViewer.docViews.table.filterOutValueButtonTooltip": "筛除值", + "unifiedDocViewer.docViews.table.ignored.multiValueLabel": "包含被忽略的值", + "unifiedDocViewer.docViews.table.ignored.singleValueLabel": "被忽略的值", + "unifiedDocViewer.docViews.table.pinFieldAriaLabel": "固定字段", + "unifiedDocViewer.docViews.table.pinFieldLabel": "固定字段", "discover.docViews.table.scoreSortWarningTooltip": "要检索 _score 的值,必须按其筛选。", - "discover.docViews.table.tableTitle": "表", - "discover.docViews.table.toggleColumnInTableButtonAriaLabel": "在表中切换列", - "discover.docViews.table.toggleColumnInTableButtonTooltip": "在表中切换列", - "discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "无法筛选元数据字段是否存在", - "discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "无法筛选脚本字段是否存在", - "discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "无法搜索未编入索引的字段或被忽略的值", - "discover.docViews.table.unpinFieldAriaLabel": "取消固定字段", - "discover.docViews.table.unpinFieldLabel": "取消固定字段", + "unifiedDocViewer.docViews.table.tableTitle": "表", + "unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel": "在表中切换列", + "unifiedDocViewer.docViews.table.toggleColumnInTableButtonTooltip": "在表中切换列", + "unifiedDocViewer.fieldChooser.discoverField.name": "切换字段详细信息", + "unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "无法筛选元数据字段是否存在", + "unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "无法筛选脚本字段是否存在", + "unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "无法搜索未编入索引的字段或被忽略的值", + "unifiedDocViewer.docViews.table.unpinFieldAriaLabel": "取消固定字段", + "unifiedDocViewer.docViews.table.unpinFieldLabel": "取消固定字段", "discover.dropZoneTableLabel": "放置区域以将字段作为列添加到表中", "discover.dscTour.stepAddFields.imageAltText": "在可用字段列表中,单击加号图标将字段切换为文档表。", "discover.dscTour.stepAddFields.title": "将字段添加到表中", @@ -2374,47 +2364,32 @@ "discover.embeddable.search.displayName": "搜索", "discover.errorCalloutShowErrorMessage": "显示详情", "discover.fieldChooser.availableFieldsTooltip": "适用于在表中显示的字段。", - "discover.fieldChooser.discoverField.actions": "操作", + "unifiedDocViewer.fieldChooser.discoverField.actions": "操作", "discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列", - "discover.fieldChooser.discoverField.multiField": "多字段", - "discover.fieldChooser.discoverField.multiFieldTooltipContent": "多字段的每个字段可以有多个值", - "discover.fieldChooser.discoverField.name": "字段", + "unifiedDocViewer.fieldChooser.discoverField.multiField": "多字段", + "unifiedDocViewer.fieldChooser.discoverField.multiFieldTooltipContent": "多字段的每个字段可以有多个值", + "unifiedDocViewer.fieldChooser.discoverField.name": "字段", "discover.fieldChooser.discoverField.removeFieldTooltip": "从表中移除字段", - "discover.fieldChooser.discoverField.value": "值", + "unifiedDocViewer.fieldChooser.discoverField.value": "值", "discover.goToDiscoverButtonText": "前往 Discover", - "discover.grid.closePopover": "关闭弹出框", - "discover.grid.copyCellValueButton": "复制值", - "discover.grid.copyColumnNameToClipboard.toastTitle": "已复制到剪贴板", - "discover.grid.copyColumnNameToClipBoardButton": "复制名称", - "discover.grid.copyColumnValuesToClipBoardButton": "复制列", - "discover.grid.copyEscapedValueWithFormulasToClipboardWarningText": "值可能包含已转义的公式。", - "discover.grid.copyFailedErrorText": "无法在此浏览器中复制到剪贴板", - "discover.grid.copyValueToClipboard.toastTitle": "已复制到剪贴板", - "discover.grid.documentHeader": "文档", - "discover.grid.editFieldButton": "编辑数据视图字段", - "discover.grid.filterFor": "筛留", - "discover.grid.filterOut": "筛除", "discover.grid.flyout.documentNavigation": "文档导航", "discover.grid.flyout.toastColumnAdded": "已添加列“{columnName}”", "discover.grid.flyout.toastColumnRemoved": "已移除列“{columnName}”", - "discover.grid.selectDoc": "选择文档“{rowNumber}”", "discover.grid.tableRow.detailHeading": "已展开文档", "discover.grid.tableRow.textBasedDetailHeading": "已展开行", "discover.grid.tableRow.viewSingleDocumentLinkTextSimple": "单个文档", "discover.grid.tableRow.viewSurroundingDocumentsHover": "检查在此文档之前和之后出现的文档。在周围文档视图中,仅已固定筛选仍处于活动状态。", "discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple": "周围文档", "discover.grid.tableRow.viewText": "视图:", - "discover.grid.viewDoc": "切换具有详情的对话框", "discover.helpMenu.appName": "Discover", "discover.inspectorRequestDataTitleDocuments": "文档", "discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取文档。", "discover.invalidFiltersWarnToast.description": "某些应用的筛选中的数据视图 ID 引用与当前数据视图不同。", "discover.invalidFiltersWarnToast.title": "不同的索引引用", - "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", - "discover.json.copyToClipboardLabel": "复制到剪贴板", + "unifiedDocViewer.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", + "unifiedDocViewer.json.copyToClipboardLabel": "复制到剪贴板", "discover.loadingDocuments": "正在加载文档", - "discover.loadingJSON": "正在加载 JSON", - "discover.loadingResults": "正在加载结果", + "unifiedDocViewer.loadingJSON": "正在加载 JSON", "discover.localMenu.alertsDescription": "告警", "discover.localMenu.fallbackReportTitle": "未命名 Discover 搜索", "discover.localMenu.inspectTitle": "检查", @@ -2457,28 +2432,23 @@ "discover.noResults.suggestion.syntaxPopoverExampleHeader": "示例", "discover.noResults.suggestion.tryText": "这里是要尝试的一些内容:", "discover.noResults.suggestion.viewAllMatchesButtonText": "查看所有匹配项", - "discover.noResultsFound": "找不到结果", "discover.notifications.invalidTimeRangeText": "提供的时间范围无效。(自:“{from}”,至:“{to}”)", "discover.notifications.invalidTimeRangeTitle": "时间范围无效", "discover.notifications.notSavedSearchTitle": "搜索“{savedSearchTitle}”未保存。", "discover.notifications.savedSearchTitle": "搜索“{savedSearchTitle}”已保存", "discover.pageTitleWithoutSavedSearch": "Discover - 尚未保存搜索", "discover.reloadSavedSearchButton": "重置搜索", - "discover.removeColumnLabel": "移除列", "discover.rootBreadcrumb": "Discover", "discover.sampleData.viewLinkLabel": "Discover", "discover.savedSearch.savedObjectName": "已保存搜索", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "在 Discover 中打开", "discover.searchingTitle": "正在搜索", - "discover.selectColumnHeader": "选择列", "discover.serverLocatorExtension.titleFromLocatorUnknown": "未知搜索", - "discover.showAllDocuments": "显示所有文档", - "discover.showSelectedDocumentsOnly": "仅显示选定的文档", "discover.singleDocRoute.errorTitle": "发生错误", "discover.skipToBottomButtonLabel": "转到表尾", - "discover.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。", - "discover.sourceViewer.errorMessageTitle": "发生错误", - "discover.sourceViewer.refresh": "刷新", + "unifiedDocViewer.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。", + "unifiedDocViewer.sourceViewer.errorMessageTitle": "发生错误", + "unifiedDocViewer.sourceViewer.refresh": "刷新", "discover.toggleSidebarAriaLabel": "切换侧边栏", "discover.topNav.openSearchPanel.manageSearchesButtonLabel": "管理搜索", "discover.topNav.openSearchPanel.noSearchesFoundDescription": "未找到匹配的搜索。", @@ -2495,6 +2465,25 @@ "discover.viewAlert.searchSourceErrorTitle": "提取搜索源时出错", "discover.viewModes.document.label": "文档", "discover.viewModes.fieldStatistics.label": "字段统计信息", + "unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel": "{timeFieldName} - 此字段表示事件发生的时间。", + "unifiedDataTable.searchGenerationWithDescription": "搜索 {searchTitle} 生成的表", + "unifiedDataTable.searchGenerationWithDescriptionGrid": "搜索 {searchTitle} 生成的表({searchDescription})", + "unifiedDataTable.selectedDocumentsNumber": "{nr} 个文档已选择", + "unifiedDataTable.clearSelection": "清除所选内容", + "unifiedDataTable.controlColumnHeader": "控制列", + "unifiedDataTable.copyToClipboardJSON": "将文档复制到剪贴板 (JSON)", + "unifiedDataTable.tableHeader.timeFieldIconTooltip": "此字段表示事件发生的时间。", + "unifiedDataTable.grid.copyColumnNameToClipBoardButton": "复制名称", + "unifiedDataTable.grid.copyColumnValuesToClipBoardButton": "复制列", + "unifiedDataTable.grid.documentHeader": "文档", + "unifiedDataTable.grid.editFieldButton": "编辑数据视图字段", + "unifiedDataTable.grid.selectDoc": "选择文档“{rowNumber}”", + "unifiedDataTable.loadingResults": "正在加载结果", + "unifiedDataTable.noResultsFound": "找不到结果", + "unifiedDataTable.removeColumnLabel": "移除列", + "unifiedDataTable.selectColumnHeader": "选择列", + "unifiedDataTable.showAllDocuments": "显示所有文档", + "unifiedDataTable.showSelectedDocumentsOnly": "仅显示选定的文档", "domDragDrop.announce.cancelled": "移动已取消。{label} 已返回至其初始位置", "domDragDrop.announce.cancelledItem": "移动已取消。{label} 返回至 {groupLabel} 组中的位置 {position}", "domDragDrop.announce.dropped.combineCompatible": "已将组 {groupLabel} 中的 {label} 组合到图层 {dropLayerNumber} 的组 {dropGroupLabel} 中的位置 {dropPosition} 上的 {dropLabel}", @@ -5711,34 +5700,34 @@ "unifiedFieldList.fieldNameDescription.textField": "全文本,如电子邮件正文或产品描述。", "unifiedFieldList.fieldNameDescription.unknownField": "未知字段", "unifiedFieldList.fieldNameDescription.versionField": "软件版本。支持“语义版本控制”优先规则。", - "unifiedFieldList.fieldNameIcons.binaryAriaLabel": "二进制", - "unifiedFieldList.fieldNameIcons.booleanAriaLabel": "布尔型", - "unifiedFieldList.fieldNameIcons.conflictFieldAriaLabel": "冲突", - "unifiedFieldList.fieldNameIcons.counterFieldAriaLabel": "计数器指标", - "unifiedFieldList.fieldNameIcons.dateFieldAriaLabel": "日期", - "unifiedFieldList.fieldNameIcons.dateRangeFieldAriaLabel": "日期范围", - "unifiedFieldList.fieldNameIcons.denseVectorFieldAriaLabel": "密集向量", - "unifiedFieldList.fieldNameIcons.flattenedFieldAriaLabel": "扁平", - "unifiedFieldList.fieldNameIcons.gaugeFieldAriaLabel": "仪表盘指标", - "unifiedFieldList.fieldNameIcons.geoPointFieldAriaLabel": "地理点", - "unifiedFieldList.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状", - "unifiedFieldList.fieldNameIcons.histogramFieldAriaLabel": "直方图", - "unifiedFieldList.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址", - "unifiedFieldList.fieldNameIcons.ipRangeFieldAriaLabel": "IP 范围", - "unifiedFieldList.fieldNameIcons.keywordFieldAriaLabel": "关键字", - "unifiedFieldList.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3", - "unifiedFieldList.fieldNameIcons.nestedFieldAriaLabel": "嵌套", - "unifiedFieldList.fieldNameIcons.numberFieldAriaLabel": "数字", - "unifiedFieldList.fieldNameIcons.pointFieldAriaLabel": "点", - "unifiedFieldList.fieldNameIcons.rankFeatureFieldAriaLabel": "排名特征", - "unifiedFieldList.fieldNameIcons.rankFeaturesFieldAriaLabel": "排名特征", - "unifiedFieldList.fieldNameIcons.recordAriaLabel": "记录", - "unifiedFieldList.fieldNameIcons.shapeFieldAriaLabel": "形状", - "unifiedFieldList.fieldNameIcons.sourceFieldAriaLabel": "源字段", - "unifiedFieldList.fieldNameIcons.stringFieldAriaLabel": "字符串", - "unifiedFieldList.fieldNameIcons.textFieldAriaLabel": "文本", - "unifiedFieldList.fieldNameIcons.unknownFieldAriaLabel": "未知字段", - "unifiedFieldList.fieldNameIcons.versionFieldAriaLabel": "版本", + "discover.fieldNameIcons.binaryAriaLabel": "二进制", + "discover.fieldNameIcons.booleanAriaLabel": "布尔型", + "discover.fieldNameIcons.conflictFieldAriaLabel": "冲突", + "discover.fieldNameIcons.counterFieldAriaLabel": "计数器指标", + "discover.fieldNameIcons.dateFieldAriaLabel": "日期", + "discover.fieldNameIcons.dateRangeFieldAriaLabel": "日期范围", + "discover.fieldNameIcons.denseVectorFieldAriaLabel": "密集向量", + "discover.fieldNameIcons.flattenedFieldAriaLabel": "扁平", + "discover.fieldNameIcons.gaugeFieldAriaLabel": "仪表盘指标", + "discover.fieldNameIcons.geoPointFieldAriaLabel": "地理点", + "discover.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状", + "discover.fieldNameIcons.histogramFieldAriaLabel": "直方图", + "discover.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址", + "discover.fieldNameIcons.ipRangeFieldAriaLabel": "IP 范围", + "discover.fieldNameIcons.keywordFieldAriaLabel": "关键字", + "discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3", + "discover.fieldNameIcons.nestedFieldAriaLabel": "嵌套", + "discover.fieldNameIcons.numberFieldAriaLabel": "数字", + "discover.fieldNameIcons.pointFieldAriaLabel": "点", + "discover.fieldNameIcons.rankFeatureFieldAriaLabel": "排名特征", + "discover.fieldNameIcons.rankFeaturesFieldAriaLabel": "排名特征", + "discover.fieldNameIcons.recordAriaLabel": "记录", + "discover.fieldNameIcons.shapeFieldAriaLabel": "形状", + "discover.fieldNameIcons.sourceFieldAriaLabel": "源字段", + "discover.fieldNameIcons.stringFieldAriaLabel": "字符串", + "discover.fieldNameIcons.textFieldAriaLabel": "文本", + "discover.fieldNameIcons.unknownFieldAriaLabel": "未知字段", + "discover.fieldNameIcons.versionFieldAriaLabel": "版本", "unifiedFieldList.fieldNameSearch.filterByNameLabel": "搜索字段名称", "unifiedFieldList.fieldPopover.addExistsFilterLabel": "筛留存在的字段", "unifiedFieldList.fieldPopover.deleteFieldLabel": "删除数据视图字段", @@ -5939,9 +5928,6 @@ "unifiedSearch.query.queryBar.indexPattern.findFilterSet": "查找查询", "unifiedSearch.query.queryBar.indexPattern.manageFieldButton": "管理此数据视图", "unifiedSearch.query.queryBar.indexPattern.temporaryDataviewLabel": "临时", - "unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning": "切换数据视图会移除当前的 SQL 查询。保存此搜索以确保不会丢失工作。", - "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesLabel": "基于文本的查询语言", - "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody": "切换数据视图会移除当前的 SQL 查询。保存此搜索以确保不会丢失工作。", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton": "切换而不保存", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalDismissButton": "不再显示此警告", "unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalSaveButton": "保存并切换", @@ -27637,8 +27623,6 @@ "xpack.observability.slo.sloEdit.sliType.customKql.goodQuery.tooltip": "此 KQL 查询应返回用于计算 SLO 时被视为“良好”或“成功”的事件的子集。此查询应基于某些相关条件(如状态代码、错误消息或其他相关字段)筛选事件。", "xpack.observability.slo.sloEdit.sliType.customKql.goodQueryPlaceholder": "定义良好事件", "xpack.observability.slo.sloEdit.sliType.customKql.queryFilter": "查询筛选", - "xpack.observability.slo.sloEdit.sliType.customKql.timestampField.label": "时间戳字段", - "xpack.observability.slo.sloEdit.sliType.customKql.timestampField.placeholder": "选择时间戳字段", "xpack.observability.slo.sloEdit.sliType.customKql.totalQuery": "查询总数", "xpack.observability.slo.sloEdit.sliType.customKql.totalQuery.tooltip": "此 KQL 查询应返回与 SLO 计算相关的所有事件,包括良好和不良事件。", "xpack.observability.slo.sloEdit.sliType.customKql.totalQueryPlaceholder": "定义事件总数", @@ -27652,8 +27636,6 @@ "xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder": "选择指标字段", "xpack.observability.slo.sloEdit.sliType.customMetric.queryFilter": "查询筛选", "xpack.observability.slo.sloEdit.sliType.customMetric.sumLabel": "求和", - "xpack.observability.slo.sloEdit.sliType.customMetric.timestampField.label": "时间戳字段", - "xpack.observability.slo.sloEdit.sliType.customMetric.timestampField.placeholder": "选择时间戳字段", "xpack.observability.slo.sloEdit.tags.label": "标签", "xpack.observability.slo.sloEdit.tags.placeholder": "添加标签", "xpack.observability.slo.sloEdit.targetSlo.label": "目标/SLO (%)", @@ -33181,43 +33163,6 @@ "xpack.securitySolution.expandedValue.showTopN.showTopValues": "显示排名最前值", "xpack.securitySolution.explore.landing.pageTitle": "浏览", "xpack.securitySolution.featureCatalogueDescription": "预防、收集、检测和响应威胁,以对整个基础架构提供统一的保护。", - "xpack.securitySolution.featureRegistry.deleteSubFeatureDetails": "删除案例和注释", - "xpack.securitySolution.featureRegistry.deleteSubFeatureName": "删除", - "xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle": "案例", - "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "安全", - "xpack.securitySolution.featureRegistry.subFeatures.blockList": "阻止列表", - "xpack.securitySolution.featureRegistry.subFeatures.blockList.description": "针对恶意进程扩大 Elastic Defend 防护,并防范具有潜在危害的应用程序。", - "xpack.securitySolution.featureRegistry.subFeatures.blockList.privilegesTooltip": "访问阻止列表需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList": "终端列表", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList.description": "显示运行 Elastic Defend 的所有主机及其相关集成详情。", - "xpack.securitySolution.featureRegistry.subFeatures.endpointList.privilegesTooltip": "访问终端列表需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters": "事件筛选", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters.description": "筛除您不需要或希望存储在 Elasticsearch 中的终端事件。", - "xpack.securitySolution.featureRegistry.subFeatures.eventFilters.privilegesTooltip": "访问事件筛选需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations": "执行操作", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations.description": "在终端上执行脚本。", - "xpack.securitySolution.featureRegistry.subFeatures.executeOperations.privilegesTooltip": "访问执行操作需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations": "文件操作", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations.description": "在响应控制台中执行文件相关响应操作。", - "xpack.securitySolution.featureRegistry.subFeatures.fileOperations.privilegesTooltip": "访问文件操作需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation": "主机隔离", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.description": "执行“隔离”和“释放”响应操作。", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.privilegesTooltip": "访问主机隔离需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions": "主机隔离例外", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.description": "添加仍允许已隔离(即使与剩余网络隔离)主机与其通信的特定 IP 地址。", - "xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip": "访问主机隔离例外需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement": "Elastic Defend 策略管理", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement.description": "访问 Elastic Defend 集成策略以配置防护、事件收集和高级策略功能。", - "xpack.securitySolution.featureRegistry.subFeatures.policyManagement.privilegesTooltip": "访问策略管理需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations": "进程操作", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations.description": "在响应控制台中执行进程相关响应操作。", - "xpack.securitySolution.featureRegistry.subFeatures.processOperations.privilegesTooltip": "访问进程操作需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory": "响应操作历史记录", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.description": "访问在终端上执行的响应操作的历史记录。", - "xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip": "访问响应操作历史记录需要所有工作区。", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications": "受信任的应用程序", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.description": "帮助减少与其他软件(通常指其他防病毒或终端安全应用程序)的冲突。", - "xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.privilegesTooltip": "访问受信任的应用程序需要所有工作区。", "xpack.securitySolution.fieldBrowser.actionsLabel": "操作", "xpack.securitySolution.fieldBrowser.categoryLabel": "类别", "xpack.securitySolution.fieldBrowser.createFieldButton": "创建字段", @@ -33320,14 +33265,11 @@ "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "IP 地址", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "名称", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "相关主机", - "xpack.securitySolution.flyout.entities.relatedHostsToolTip": "告警后,用户已成功通过这些主机的身份验证。", "xpack.securitySolution.flyout.entities.relatedUsersTitle": "相关用户", - "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "告警后,这些用户已成功通过受影响主机的身份验证。", "xpack.securitySolution.flyout.entities.usersInfoTitle": "用户信息", "xpack.securitySolution.flyout.prevalenceErrorMessage": "普及率", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "告警计数", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "文档计数", - "xpack.securitySolution.flyout.response.empty": "没有为此事件定义响应操作。", "xpack.securitySolution.flyout.response.title": "响应", "xpack.securitySolution.flyout.sessionViewErrorMessage": "会话视图", "xpack.securitySolution.footer.autoRefreshActiveDescription": "自动刷新已启用", @@ -34654,10 +34596,6 @@ "xpack.serverlessSearch.apiKey.roleDescriptorsLinkLabel": "了解如何构造角色描述符", "xpack.serverlessSearch.apiKey.setup.description": "用于创建 API 密钥的基本配置详情。", "xpack.serverlessSearch.apiKey.setup.title": "设置", - "xpack.serverlessSearch.apiKey.stepOneDescription": "用于身份验证和授权的唯一标识符。", - "xpack.serverlessSearch.apiKey.stepOneTitle": "生成并存储 API 密钥", - "xpack.serverlessSearch.apiKey.stepTwoDescription": "用于特定项目的唯一标识符。", - "xpack.serverlessSearch.apiKey.stepTwoTitle": "存储您的唯一云 ID", "xpack.serverlessSearch.apiKey.title": "存储您的 API 密钥和云 ID", "xpack.serverlessSearch.apiKey.userFieldHelpText": "创建 API 密钥的用户的 ID。", "xpack.serverlessSearch.apiKey.userFieldLabel": "用户", @@ -34668,20 +34606,6 @@ "xpack.serverlessSearch.configureClient.title": "配置客户端", "xpack.serverlessSearch.disabled": "已禁用", "xpack.serverlessSearch.enabled": "已启用", - "xpack.serverlessSearch.footer.billing.title": "帐单和使用情况", - "xpack.serverlessSearch.footer.community.title": "加入社区", - "xpack.serverlessSearch.footer.discoverCard.buttonText": "在 Discover 中浏览数据", - "xpack.serverlessSearch.footer.discoverCard.description": "使用 Discover,您可以快速搜索和筛选数据,获取有关字段结构的信息,并在可视化中显示结果。", - "xpack.serverlessSearch.footer.discoverCard.title": "在 Discover 中浏览您的数据并进行可视化", - "xpack.serverlessSearch.footer.feedback.title": "反馈", - "xpack.serverlessSearch.footer.inviteUsers.title": "邀请更多用户", - "xpack.serverlessSearch.footer.pipelinesCard.buttonText": "配置采集管道", - "xpack.serverlessSearch.footer.pipelinesCard.description": "先预处理数据,然后索引到 Elasticsearch。移除字段,从文本中提取值,或使用 ELSER 等 Machine Learning 模型扩充您的数据。", - "xpack.serverlessSearch.footer.pipelinesCard.title": "使用管道转换数据", - "xpack.serverlessSearch.footer.searchUI.buttonText": "通过搜索 UI 构建", - "xpack.serverlessSearch.footer.searchUI.description": "搜索 UI 是一个由 Elastic 维护的免费开源 JavaScript 库,用于快速打造现代、富于吸引力的搜索体验。", - "xpack.serverlessSearch.footer.searchUI.title": "通过搜索 UI 构建用户界面", - "xpack.serverlessSearch.footer.title": "后续操作", "xpack.serverlessSearch.header.title": "Elasticsearch 入门", "xpack.serverlessSearch.invalidJsonError": "JSON 无效", "xpack.serverlessSearch.languages.cURL": "cURL", @@ -37632,7 +37556,6 @@ "xpack.synthetics.synthetics.waterfallChart.labels.timings.ssl": "TLS", "xpack.synthetics.synthetics.waterfallChart.labels.timings.wait": "等待中 (TTFB)", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", - "xpack.synthetics.syntheticsMonitors": "运行时间 - 监测", "xpack.synthetics.testDetails.after": "之后 ", "xpack.synthetics.testDetails.codeExecuted": "已执行代码", "xpack.synthetics.testDetails.console": "控制台", @@ -37757,26 +37680,19 @@ "xpack.transform.alertTypes.transformHealth.healthCheckRecoveryMessage": "{count, plural, other {转换}} {transformsString}{count, plural, other {有}}运行正常。", "xpack.transform.alertTypes.transformHealth.notStartedMessage": "{count, plural, other {转换}} {transformsString}{count, plural, other {有}}未启动。", "xpack.transform.alertTypes.transformHealth.notStartedRecoveryMessage": "{count, plural, other {转换}} {transformsString}{count, plural, other {有}}已启动。", - "xpack.transform.app.deniedPrivilegeDescription": "要使用“转换”部分,必须具有{privilegesCount, plural, other {以下集群权限}}:{missingPrivileges}。", "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message}请联系您的管理员。", "xpack.transform.clone.noDataViewErrorPromptText": "无法克隆转换 {transformId}。对于 {dataViewTitle},不存在数据视图。", "xpack.transform.danglingTasksError": "{count} 个{count, plural, other {转换}}缺少配置详情:[{transformIds}] 无法将{count, plural, other {其}}恢复,应予以删除。", "xpack.transform.deleteTransform.deleteAnalyticsWithDataViewErrorMessage": "删除数据视图 {destinationIndex} 时出错", - "xpack.transform.deleteTransform.deleteAnalyticsWithDataViewSuccessMessage": "删除数据视图 {destinationIndex} 的请求已确认。", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误", - "xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage": "删除目标索引 {destinationIndex} 的请求已确认。", "xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage": "检查数据视图 {dataView} 是否存在时发生错误:{error}", "xpack.transform.edit.noDataViewErrorPromptText": "无法获取转换 {transformId} 的数据视图。对于 {dataViewTitle},不存在数据视图。", "xpack.transform.forceDeleteTransformMessage": "删除 {count} {count, plural, other {转换}}", "xpack.transform.managedTransformsWarningCallout": "{count, plural, other {至少一个此类转换}}由 Elastic 预配置;{action} {count, plural, other {这些转换}}可能会影响该产品的其他部分。", "xpack.transform.multiTransformActionsMenu.transformsCount": "已选定 {count} 个{count, plural, other {转换}}", "xpack.transform.stepCreateForm.createDataViewErrorMessage": "创建 Kibana 数据视图 {dataViewName} 时发生错误:", - "xpack.transform.stepCreateForm.createDataViewSuccessMessage": "已成功创建 Kibana 数据视图 {dataViewName}。", "xpack.transform.stepCreateForm.createTransformErrorMessage": "创建转换 {transformId} 时出错:", - "xpack.transform.stepCreateForm.createTransformSuccessMessage": "创建转换 {transformId} 的请求已确认。", "xpack.transform.stepCreateForm.duplicateDataViewErrorMessage": "创建 Kibana 数据视图 {dataViewName} 时发生错误:数据视图已存在。", - "xpack.transform.stepCreateForm.startTransformErrorMessage": "启动转换 {transformId} 时发生错误:", - "xpack.transform.stepCreateForm.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", "xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar": "无效查询:{queryErrorMessage}", "xpack.transform.stepDefineForm.queryPlaceholderKql": "例如,{example}", "xpack.transform.stepDefineForm.queryPlaceholderLucene": "例如,{example}", @@ -37788,41 +37704,30 @@ "xpack.transform.stepDetailsForm.retentionPolicyMaxAgePlaceholderText": "max_age,例如 {exampleValue}", "xpack.transform.transformForm.sizeNotationPlaceholder": "示例:{example1}、{example2}、{example3}、{example4}", "xpack.transform.transformList.alertingRules.tooltipContent": "转换具有 {rulesCount} 个关联的告警{rulesCount, plural, other { 规则}}", - "xpack.transform.transformList.bulkDeleteDestDataViewSuccessMessage": "已成功删除 {count} 个目标数据{count, plural, other {视图}}。", - "xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage": "已成功删除 {count} 个目标数据{count, plural, other {索引}}。", "xpack.transform.transformList.bulkDeleteModalTitle": "删除 {count} 个 {count, plural, other {转换}}?", - "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "已成功删除 {count} 个{count, plural, other {转换}}。", "xpack.transform.transformList.bulkReauthorizeModalTitle": "重新授权 {count} 个{count, plural, other {转换}}?", "xpack.transform.transformList.bulkResetModalTitle": "重置 {count} 个{count, plural, other {转换}}?", - "xpack.transform.transformList.bulkResetTransformSuccessMessage": "已成功重置 {count} 个{count, plural, other {转换}}。", "xpack.transform.transformList.bulkStartModalTitle": "启动 {count} 个{count, plural, other {转换}}?", "xpack.transform.transformList.bulkStopModalTitle": "停止 {count} 个{count, plural, other {转换}}?", "xpack.transform.transformList.cannotRestartCompleteBatchTransformToolTip": "{transformId} 为已完成批量转换,无法重新启动。", "xpack.transform.transformList.cannotScheduleNowCompleteBatchTransformToolTip": "{transformId} 为已完成的批量转换,无法安排其立即处理数据。", "xpack.transform.transformList.deleteModalTitle": "删除 {transformId}?", "xpack.transform.transformList.deleteTransformErrorMessage": "删除转换 {transformId} 时发生错误", - "xpack.transform.transformList.deleteTransformSuccessMessage": "删除转换 {transformId} 的请求已确认。", "xpack.transform.transformList.editFlyoutFormPlaceholderText": "默认值:{defaultValue}", "xpack.transform.transformList.editFlyoutTitle": "编辑 {transformId}", - "xpack.transform.transformList.editTransformSuccessMessage": "转换 {transformId} 已更新。", "xpack.transform.transformList.reauthorizeModalTitle": "重新授权 {transformId}?", "xpack.transform.transformList.reauthorizeTransformErrorMessage": "重新授权转换 {transformId} 时出错", - "xpack.transform.transformList.reauthorizeTransformSuccessMessage": "重新授权转换 {transformId} 的请求已确认。", "xpack.transform.transformList.resetModalTitle": "重置 {transformId}?", "xpack.transform.transformList.resetTransformErrorMessage": "重置转换 {transformId} 时出错", - "xpack.transform.transformList.resetTransformSuccessMessage": "重置转换 {transformId} 的请求已确认。", "xpack.transform.transformList.rowCollapse": "隐藏 {transformId} 的详情", "xpack.transform.transformList.rowExpand": "显示 {transformId} 的详情", "xpack.transform.transformList.scheduleNowTransformErrorMessage": "计划转换 {transformId} 以立即处理数据时出错。", - "xpack.transform.transformList.scheduleNowTransformSuccessMessage": "计划转换 {transformId} 以立即处理数据的请求已确认。", "xpack.transform.transformList.startedTransformToolTip": "{transformId} 已启动。", "xpack.transform.transformList.startModalTitle": "启动 {transformId}?", "xpack.transform.transformList.startTransformErrorMessage": "启动转换 {transformId} 时发生错误", - "xpack.transform.transformList.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", "xpack.transform.transformList.stopModalTitle": "停止 {transformId}?", "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} 已停止。", "xpack.transform.transformList.stopTransformErrorMessage": "停止数据帧转换 {transformId} 时发生错误", - "xpack.transform.transformList.stopTransformSuccessMessage": "停止数据帧转换 {transformId} 的请求已确认。", "xpack.transform.transformList.unauthorizedTransformsCallout.insufficientPermissionsMsg": "{unauthorizedCnt, plural, other {已创建 # 个转换,但权限不足。}}", "xpack.transform.transformList.unauthorizedTransformsCallout.reauthorizeMsg": "重新授权以启动{unauthorizedCnt, plural, other {# 个转换}}。", "xpack.transform.transformNodes.noTransformNodesCallOutBody": "您将无法创建或运行转换。{learnMoreLink}", @@ -37868,9 +37773,6 @@ "xpack.transform.alertTypes.transformHealth.notStartedCheckName": "转换未启动", "xpack.transform.alertTypes.transformHealth.testsConfigTransforms.errorMessage": "必须至少选择一次运行状况检查", "xpack.transform.alertTypes.transformHealth.testsSelection.enableTestLabel": "启用", - "xpack.transform.app.checkingPrivilegesDescription": "正在检查权限……", - "xpack.transform.app.checkingPrivilegesErrorMessage": "从服务器获取用户权限时出错", - "xpack.transform.app.deniedPrivilegeTitle": "您缺少集群权限", "xpack.transform.appName": "转换", "xpack.transform.appTitle": "转换", "xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip": "您无权创建转换告警规则。", @@ -37916,7 +37818,6 @@ "xpack.transform.licenseCheckErrorMessage": "许可证检查失败", "xpack.transform.list.emptyPromptButtonText": "创建您的首个转换", "xpack.transform.list.emptyPromptTitle": "找不到转换", - "xpack.transform.list.errorPromptTitle": "获取转换列表时发生错误", "xpack.transform.mode": "模式", "xpack.transform.modeFilter": "模式", "xpack.transform.models.transformService.allOtherRequestsCancelledDescription": "所有其他请求已取消。", @@ -38148,7 +38049,6 @@ "xpack.transform.transformList.editFlyoutUpdateButtonText": "更新", "xpack.transform.transformList.editManagedTransformsDescription": "正在编辑", "xpack.transform.transformList.editTransformGenericErrorMessage": "调用用于更新转换的 API 终端时发生错误。", - "xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "检查用户是否可以删除目标索引时发生错误", "xpack.transform.transformList.managedBadgeLabel": "托管", "xpack.transform.transformList.managedBadgeTooltip": "此转换由 Elastic 预配置和管理;该产品的其他部分可能依赖于其行为。", "xpack.transform.transformList.needsReauthorizationBadge.contactAdminTooltip": "请联系管理员请求所需权限。", @@ -38429,15 +38329,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "且", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "当", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "当", - "xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle": "添加变量", - "xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "添加变量", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreHidden": "将隐藏已弃用变量", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreShown": "将显示已弃用变量", - "xpack.triggersActionsUI.components.addMessageVariables.hideDeprecatedVariables": "隐藏", - "xpack.triggersActionsUI.components.addMessageVariables.loadingMessage": "正在加载变量", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesAvailable": "无变量可用", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesFound": "找不到变量", - "xpack.triggersActionsUI.components.addMessageVariables.showAllDeprecatedVariables": "全部显示", "xpack.triggersActionsUI.components.alertTable.useFetchAlerts.errorMessageText": "搜索告警时发生错误", "xpack.triggersActionsUI.components.alertTable.useFetchBrowserFieldsCapabilities.errorMessageText": "加载浏览器字段时出错", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "选择……", diff --git a/x-pack/plugins/triggers_actions_ui/kibana.jsonc b/x-pack/plugins/triggers_actions_ui/kibana.jsonc index 653c105772711..eb660c0bbe383 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.jsonc +++ b/x-pack/plugins/triggers_actions_ui/kibana.jsonc @@ -24,6 +24,7 @@ "actions", "dashboard", "licensing", + "expressions" ], "optionalPlugins": [ "cloud", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 263c13ccd4981..a8d58e2e627f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -30,6 +30,7 @@ import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { ruleDetailsRoute } from '@kbn/rule-data-utils'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { DashboardStart } from '@kbn/dashboard-plugin/public'; +import { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; import { ActionTypeRegistryContract, @@ -70,6 +71,7 @@ export interface TriggersAndActionsUiServices extends CoreStart { theme$: Observable; unifiedSearch: UnifiedSearchPublicPluginStart; licensing: LicensingPluginStart; + expressions: ExpressionsStart; } export const renderApp = (deps: TriggersAndActionsUiServices) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/index.ts index 41ca1f51c735a..08ab85eece444 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - export { JsonEditorWithMessageVariables } from './json_editor_with_message_variables'; export { TextFieldWithMessageVariables } from './text_field_with_message_variables'; export { TextAreaWithMessageVariables } from './text_area_with_message_variables'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index 1acd3e6392450..4459735acb927 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -11,12 +11,11 @@ import { EuiFormRow, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { monaco, XJsonLang } from '@kbn/monaco'; -import './add_message_variables.scss'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { ActionVariable } from '@kbn/alerting-plugin/common'; -import { AddMessageVariables } from './add_message_variables'; +import { AddMessageVariables } from '@kbn/alerts-ui-shared'; import { templateActionVariable } from '../lib'; const NO_EDITOR_ERROR_TITLE = i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index 346dce44af60b..c19ac416b4695 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -7,9 +7,8 @@ import React, { useState } from 'react'; import { EuiTextArea, EuiFormRow } from '@elastic/eui'; -import './add_message_variables.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; -import { AddMessageVariables } from './add_message_variables'; +import { AddMessageVariables } from '@kbn/alerts-ui-shared'; import { templateActionVariable } from '../lib'; interface Props { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index 0efe53603085e..e2dc816987d3d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -7,9 +7,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiFieldText, EuiFormRow } from '@elastic/eui'; -import './add_message_variables.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; -import { AddMessageVariables } from './add_message_variables'; +import { AddMessageVariables } from '@kbn/alerts-ui-shared'; import { templateActionVariable } from '../lib'; interface Props { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/validate_params_for_warnings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/validate_params_for_warnings.ts index e0f552f6bb0f7..5a5dc6f33e076 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/validate_params_for_warnings.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/validate_params_for_warnings.ts @@ -43,10 +43,10 @@ export function validateParamsForWarnings( return publicUrlWarning; } } catch (e) { - /* - * do nothing, we don't care if the mustache is invalid - */ + // Better to set the warning msg if you do not know if the mustache template is invalid + return publicUrlWarning; } } + return null; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_data_grid.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_data_grid.tsx index 43199b924ba2d..08c9a45358f3e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_data_grid.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_data_grid.tsx @@ -66,6 +66,7 @@ export interface EventLogDataGrid { onFlyoutOpen?: (runLog: IExecutionLog) => void; setVisibleColumns: (visibleColumns: string[]) => void; setSortingColumns: (sortingColumns: EuiDataGridSorting['columns']) => void; + getRuleDetailsRoute?: (ruleId: string) => string; } export const numTriggeredActionsDisplay = i18n.translate( @@ -167,6 +168,7 @@ export const EventLogDataGrid = (props: EventLogDataGrid) => { onChangeItemsPerPage, onChangePage, onFlyoutOpen, + getRuleDetailsRoute, } = props; const { euiTheme } = useEuiTheme(); @@ -343,6 +345,7 @@ export const EventLogDataGrid = (props: EventLogDataGrid) => { ruleId={ruleId} spaceIds={spaceIds} useExecutionStatus={isRuleUsingExecutionStatus} + getRuleDetailsRoute={getRuleDetailsRoute} />
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.tsx index f11c38a9dfe11..a35e963b6ce11 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.tsx @@ -10,7 +10,7 @@ import moment from 'moment'; import { EuiLink } from '@elastic/eui'; import { RuleAlertingOutcome } from '@kbn/alerting-plugin/common'; import { useHistory } from 'react-router-dom'; -import { getRuleDetailsRoute } from '@kbn/rule-data-utils'; +import { getRuleDetailsRoute as internalGetRuleDetailsRoute } from '@kbn/rule-data-utils'; import { formatRuleAlertCount } from '../../../../../common/lib/format_rule_alert_count'; import { useKibana, useSpacesData } from '../../../../../common/lib/kibana'; import { EventLogListStatus } from './event_log_list_status'; @@ -36,6 +36,7 @@ interface EventLogListCellRendererProps { ruleId?: string; spaceIds?: string[]; useExecutionStatus?: boolean; + getRuleDetailsRoute?: (ruleId: string) => string; } export const EventLogListCellRenderer = (props: EventLogListCellRendererProps) => { @@ -47,6 +48,7 @@ export const EventLogListCellRenderer = (props: EventLogListCellRendererProps) = ruleId, spaceIds, useExecutionStatus = true, + getRuleDetailsRoute, } = props; const spacesData = useSpacesData(); const { http } = useKibana().services; @@ -66,7 +68,9 @@ export const EventLogListCellRenderer = (props: EventLogListCellRendererProps) = const ruleNamePathname = useMemo(() => { if (!ruleId) return ''; - const ruleRoute = getRuleDetailsRoute(ruleId); + const ruleRoute = getRuleDetailsRoute + ? getRuleDetailsRoute(ruleId) + : internalGetRuleDetailsRoute(ruleId); if (ruleOnDifferentSpace) { const [linkedSpaceId] = spaceIds ?? []; @@ -82,7 +86,7 @@ export const EventLogListCellRenderer = (props: EventLogListCellRendererProps) = return newPathname; } return ruleRoute; - }, [ruleId, ruleOnDifferentSpace, history, activeSpace, http, spaceIds]); + }, [ruleId, ruleOnDifferentSpace, history, activeSpace, http, spaceIds, getRuleDetailsRoute]); const onClickRuleName = useCallback(() => { if (!ruleId) return; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/global_rule_event_log_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/global_rule_event_log_list.tsx index bb939e3e26180..c7d7f442beab1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/global_rule_event_log_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/global_rule_event_log_list.tsx @@ -16,6 +16,7 @@ export interface GlobalRuleEventLogListProps { setHeaderActions?: RuleEventLogListCommonProps['setHeaderActions']; localStorageKey?: RuleEventLogListCommonProps['localStorageKey']; filteredRuleTypes?: RuleEventLogListCommonProps['filteredRuleTypes']; + getRuleDetailsRoute?: RuleEventLogListCommonProps['getRuleDetailsRoute']; } const GLOBAL_EVENT_LOG_LIST_STORAGE_KEY = @@ -31,7 +32,7 @@ const REFRESH_TOKEN = { }; export const GlobalRuleEventLogList = (props: GlobalRuleEventLogListProps) => { - const { setHeaderActions, localStorageKey, filteredRuleTypes } = props; + const { setHeaderActions, localStorageKey, filteredRuleTypes, getRuleDetailsRoute } = props; const { spaces } = useKibana().services; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -51,6 +52,7 @@ export const GlobalRuleEventLogList = (props: GlobalRuleEventLogListProps) => { localStorageKey={localStorageKey || GLOBAL_EVENT_LOG_LIST_STORAGE_KEY} filteredRuleTypes={filteredRuleTypes} setHeaderActions={setHeaderActions} + getRuleDetailsRoute={getRuleDetailsRoute} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx index 32db0a686bff5..73eb9f2c5c536 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx @@ -95,6 +95,7 @@ export interface RuleEventLogListCommonProps { hasAllSpaceSwitch?: boolean; filteredRuleTypes?: string[]; setHeaderActions?: (components?: React.ReactNode[]) => void; + getRuleDetailsRoute?: (ruleId: string) => string; } export type RuleEventLogListTableProps = @@ -116,6 +117,7 @@ export const RuleEventLogListTable = ( hasAllSpaceSwitch = false, setHeaderActions, filteredRuleTypes, + getRuleDetailsRoute, } = props; const { uiSettings, notifications } = useKibana().services; @@ -629,6 +631,7 @@ export const RuleEventLogListTable = ( onFlyoutOpen={onFlyoutOpen} setVisibleColumns={setVisibleColumns} setSortingColumns={setSortingColumns} + getRuleDetailsRoute={getRuleDetailsRoute} /> ); @@ -668,7 +671,7 @@ export const RuleEventLogListTable = ( - + { return ( - ; + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_event_log_list.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_event_log_list.tsx index 1d2e2cf580411..168f3b6d7ad38 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_event_log_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_event_log_list.tsx @@ -20,7 +20,7 @@ export const getRuleEventLogListLazy = { const core = coreMock.createStart(); @@ -70,6 +71,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { } as unknown as HTMLElement, theme$: themeServiceMock.createTheme$(), licensing: licensingPluginMock, + expressions: expressionsPluginMock.createStartContract(), } as TriggersAndActionsUiServices; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index f26881fa2639f..6d3c3bbee0d5f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -117,6 +117,7 @@ export { getIndexOptions, firstFieldOption, getTimeFieldOptions, + getTimeOptions, GroupByExpression, COMPARATORS, connectorDeprecatedMessage, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 6f66f38e8541a..689d841b9c1f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -26,6 +26,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/ import { triggersActionsRoute } from '@kbn/rule-data-utils'; import { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { ExpressionsStart } from '@kbn/expressions-plugin/public'; import type { AlertsSearchBarProps } from './application/sections/alerts_search_bar'; import { TypeRegistry } from './application/type_registry'; @@ -161,6 +162,7 @@ interface PluginsStart { spaces?: SpacesPluginStart; navigateToApp: CoreStart['application']['navigateToApp']; features: FeaturesPluginStart; + expressions: ExpressionsStart; unifiedSearch: UnifiedSearchPublicPluginStart; licensing: LicensingPluginStart; } @@ -287,6 +289,7 @@ export class Plugin alertsTableConfigurationRegistry, kibanaFeatures, licensing: pluginsStart.licensing, + expressions: pluginsStart.expressions, }); }, }); diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts index 0b3b02c325a45..04de44a088ed5 100644 --- a/x-pack/plugins/triggers_actions_ui/server/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -69,6 +69,4 @@ export class TriggersActionsPlugin implements Plugin data: this.data, }; } - - public async stop(): Promise {} } diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index 42e53b4f313b2..3bfe7239fac2c 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -54,6 +54,7 @@ "@kbn/core-ui-settings-common", "@kbn/dashboard-plugin", "@kbn/licensing-plugin", + "@kbn/expressions-plugin", ], "exclude": ["target/**/*"] } diff --git a/x-pack/test/accessibility/apps/enterprise_search.ts b/x-pack/test/accessibility/apps/enterprise_search.ts index 8b4c94576b79a..1e526695485d3 100644 --- a/x-pack/test/accessibility/apps/enterprise_search.ts +++ b/x-pack/test/accessibility/apps/enterprise_search.ts @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('loads a landing page with product cards', async function () { await retry.waitFor( 'Elasticsearch product card visible', - async () => await testSubjects.exists('elasticsearchProductCard') + async () => await testSubjects.exists('enterpriseSearchElasticsearchProductCard') ); await retry.waitFor( 'Search Applications product card visible', diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/routes.ts index f21894b93b6da..76ebd2cf20af9 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/routes.ts @@ -305,60 +305,6 @@ export function defineRoutes( } ); - router.post( - { - path: '/api/alerts_fixture/{id}/enqueue_action', - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({ - params: schema.recordOf(schema.string(), schema.any()), - }), - }, - }, - async ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> => { - try { - const [, { actions, security, spaces }] = await core.getStartServices(); - const actionsClient = await actions.getActionsClientWithRequest(req); - - const createAPIKeyResult = - security && - (await security.authc.apiKeys.grantAsInternalUser(req, { - name: `alerts_fixture:enqueue_action:${uuidv4()}`, - role_descriptors: {}, - })); - - await actionsClient.enqueueExecution({ - id: req.params.id, - spaceId: spaces ? spaces.spacesService.getSpaceId(req) : 'default', - executionId: uuidv4(), - apiKey: createAPIKeyResult - ? Buffer.from(`${createAPIKeyResult.id}:${createAPIKeyResult.api_key}`).toString( - 'base64' - ) - : null, - params: req.body.params, - source: { - type: 'HTTP_REQUEST' as any, - source: req, - }, - }); - return res.noContent(); - } catch (err) { - if (err.isBoom && err.output.statusCode === 403) { - return res.forbidden({ body: err }); - } - - return res.badRequest({ body: err }); - } - } - ); - router.post( { path: '/api/alerts_fixture/{id}/bulk_enqueue_actions', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/enqueue.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/enqueue.ts deleted file mode 100644 index b7266c2f66419..0000000000000 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/enqueue.ts +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { IValidatedEvent } from '@kbn/event-log-plugin/server'; -import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/types'; -import { systemActionScenario, UserAtSpaceScenarios } from '../../../scenarios'; -import { getEventLog, getUrlPrefix, ObjectRemover } from '../../../../common/lib'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default function ({ 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('enqueue', () => { - const objectRemover = new ObjectRemover(supertest); - - before(async () => { - await esTestIndexTool.destroy(); - await esTestIndexTool.setup(); - }); - - after(async () => { - await esTestIndexTool.destroy(); - await objectRemover.removeAll(); - }); - - for (const scenario of [...UserAtSpaceScenarios, systemActionScenario]) { - const { user, space } = scenario; - - it(`should handle enqueue request appropriately: ${scenario.id}`, async () => { - const startDate = new Date().toISOString(); - - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - connector_type_id: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - - objectRemover.add(space.id, createdAction.id, 'action', 'actions'); - - const connectorId = createdAction.id; - const name = 'My action'; - const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`; - - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alerts_fixture/${connectorId}/enqueue_action`) - .auth(user.username, user.password) - .set('kbn-xsrf', 'foo') - .send({ - params: { reference, index: ES_TEST_INDEX_NAME, message: 'Testing 123' }, - }); - - switch (scenario.id) { - case 'no_kibana_privileges at space1': - case 'space_1_all_alerts_none_actions at space1': - case 'space_1_all at space2': - expect(response.status).to.eql(403); - break; - case 'global_read at space1': - case 'space_1_all at space1': - case 'space_1_all_with_restricted_fixture at space1': - case 'superuser at space1': - case 'system_actions at space1': - expect(response.status).to.eql(204); - - await validateEventLog({ - spaceId: space.id, - connectorId, - outcome: 'success', - message: `action executed: test.index-record:${connectorId}: ${name}`, - source: ActionExecutionSourceType.HTTP_REQUEST, - startDate, - }); - - await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1); - break; - default: - throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); - } - }); - - it(`should authorize system actions correctly: ${scenario.id}`, async () => { - const startDate = new Date().toISOString(); - - const connectorId = 'system-connector-test.system-action-kibana-privileges'; - const name = 'System action: test.system-action-kibana-privileges'; - const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`; - - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alerts_fixture/${connectorId}/enqueue_action`) - .auth(user.username, user.password) - .set('kbn-xsrf', 'foo') - .send({ - params: { index: ES_TEST_INDEX_NAME, reference }, - }); - - switch (scenario.id) { - case 'no_kibana_privileges at space1': - case 'space_1_all_alerts_none_actions at space1': - case 'space_1_all at space2': - expect(response.status).to.eql(403); - break; - /** - * The users in these scenarios have access - * to Actions but do not have access to - * the system action. They should be able to - * enqueue the action but the execution should fail. - */ - case 'global_read at space1': - case 'space_1_all at space1': - case 'space_1_all_with_restricted_fixture at space1': - expect(response.status).to.eql(204); - - await validateEventLog({ - spaceId: space.id, - connectorId, - outcome: 'failure', - message: `action execution failure: test.system-action-kibana-privileges:${connectorId}: ${name}`, - errorMessage: 'Unauthorized to execute actions', - source: ActionExecutionSourceType.HTTP_REQUEST, - startDate, - }); - break; - /** - * The users in these scenarios have access - * to Actions and to the system action. They should be able to - * enqueue the action and the execution should succeed. - */ - case 'superuser at space1': - case 'system_actions at space1': - expect(response.status).to.eql(204); - - await validateEventLog({ - spaceId: space.id, - connectorId, - outcome: 'success', - message: `action executed: test.system-action-kibana-privileges:${connectorId}: ${name}`, - source: ActionExecutionSourceType.HTTP_REQUEST, - startDate, - }); - - await esTestIndexTool.waitForDocs( - 'action:test.system-action-kibana-privileges', - reference, - 1 - ); - break; - default: - throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); - } - }); - } - }); - - interface ValidateEventLogParams { - spaceId: string; - connectorId: string; - outcome: string; - message: string; - startDate: string; - errorMessage?: string; - source?: string; - } - - const validateEventLog = async (params: ValidateEventLogParams): Promise => { - const { spaceId, connectorId, outcome, message, startDate, errorMessage, source } = params; - - const events: IValidatedEvent[] = await retry.try(async () => { - const events_ = await getEventLog({ - getService, - spaceId, - type: 'action', - id: connectorId, - provider: 'actions', - actions: new Map([['execute', { gte: 1 }]]), - }); - - const filteredEvents = events_.filter((event) => event!['@timestamp']! >= startDate); - if (filteredEvents.length < 1) throw new Error('no recent events found yet'); - - return filteredEvents; - }); - - expect(events.length).to.be(1); - - const event = events[0]; - - expect(event?.message).to.eql(message); - expect(event?.event?.outcome).to.eql(outcome); - - if (errorMessage) { - expect(event?.error?.message).to.eql(errorMessage); - } - - if (source) { - expect(event?.kibana?.action?.execution?.source).to.eql(source.toLowerCase()); - } - }; -} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts index 675f0c1a02bbc..6b57d75b26835 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts @@ -48,7 +48,6 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./connector_types')); loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./enqueue')); loadTestFile(require.resolve('./bulk_enqueue')); /** diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts index 02a41b6de7afa..ddc90ec10e9b6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts @@ -26,7 +26,8 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F const esTestIndexTool = new ESTestIndexTool(es, retry); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('telemetry', () => { + // FLAKY: https://github.com/elastic/kibana/issues/140973 + describe.skip('telemetry', () => { const objectRemover = new ObjectRemover(supertest); const alwaysFiringRuleId: { [key: string]: string } = {}; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts deleted file mode 100644 index 238cfa3ae780b..0000000000000 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license 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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const es = getService('es'); - const retry = getService('retry'); - const esTestIndexTool = new ESTestIndexTool(es, retry); - - describe('enqueue', () => { - const objectRemover = new ObjectRemover(supertest); - - before(async () => { - await esTestIndexTool.destroy(); - await esTestIndexTool.setup(); - }); - after(async () => { - await esTestIndexTool.destroy(); - await objectRemover.removeAll(); - }); - - it('should handle enqueue request appropriately', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - connector_type_id: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); - - const reference = `actions-enqueue-1:${Spaces.space1.id}:${createdAction.id}`; - const response = await supertest - .post( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${createdAction.id}/enqueue_action` - ) - .set('kbn-xsrf', 'foo') - .send({ - params: { - reference, - index: ES_TEST_INDEX_NAME, - message: 'Testing 123', - }, - }); - - expect(response.status).to.eql(204); - await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1); - }); - - it('should retry task after a failure', async () => { - const testStart = new Date().toISOString(); - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - connector_type_id: 'test.failing', - config: {}, - secrets: {}, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); - - const reference = `actions-enqueue-2:${Spaces.space1.id}:${createdAction.id}`; - let runAt: number; - await supertest - .post( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${createdAction.id}/enqueue_action` - ) - .set('kbn-xsrf', 'foo') - .send({ - params: { - reference, - index: ES_TEST_INDEX_NAME, - }, - }) - .expect(204) - .then(() => { - runAt = Date.now(); - }); - await esTestIndexTool.waitForDocs('action:test.failing', reference, 1); - - await retry.try(async () => { - const searchResult = await es.search({ - index: '.kibana_task_manager', - body: { - query: { - bool: { - must: [ - { - term: { - 'task.taskType': 'actions:test.failing', - }, - }, - { - range: { - 'task.scheduledAt': { - gte: testStart, - }, - }, - }, - ], - }, - }, - }, - }); - const hit = searchResult.hits.hits as Array>; - expect(Date.parse(hit[0]._source.task.runAt)).to.greaterThan(runAt); - expect(Date.parse(hit[0]._source.task.attempts)).to.greaterThan(1); - }); - }); - - it('should never leaved a failed task, even if max attempts is reached', async () => { - // We have to provide the test.rate-limit the next runAt, for testing purposes - const retryDate = new Date(Date.now() + 1); - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - connector_type_id: 'test.no-attempts-rate-limit', - config: {}, - secrets: {}, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); - - const reference = `actions-enqueue-2:${Spaces.space1.id}:${createdAction.id}`; - await supertest - .post( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${createdAction.id}/enqueue_action` - ) - .set('kbn-xsrf', 'foo') - .send({ - params: { - reference, - index: ES_TEST_INDEX_NAME, - retryAt: retryDate.getTime(), - }, - }) - .expect(204); - - await retry.try(async () => { - const runningSearchResult = await es.search({ - index: '.kibana_task_manager', - body: { - query: { - bool: { - must: [ - { - term: { - 'task.taskType': 'actions:test.no-attempts-rate-limit', - }, - }, - ], - }, - }, - }, - }); - const total = (runningSearchResult.hits.total as estypes.SearchTotalHits).value; - expect(total).to.eql(1); - }); - - await retry.try(async () => { - const runningSearchResult = await es.search({ - index: '.kibana_task_manager', - body: { - query: { - bool: { - must: [ - { - term: { - 'task.taskType': 'actions:test.no-attempts-rate-limit', - }, - }, - ], - }, - }, - }, - }); - const total = (runningSearchResult.hits.total as estypes.SearchTotalHits).value; - expect(total).to.eql(0); - }); - }); - - it('should enqueue system actions correctly', async () => { - const connectorId = 'system-connector-test.system-action-kibana-privileges'; - const reference = `actions-enqueue-1:${Spaces.space1.id}:${connectorId}`; - - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${connectorId}/enqueue_action`) - .set('kbn-xsrf', 'foo') - .send({ - params: { index: ES_TEST_INDEX_NAME, reference }, - }); - - expect(response.status).to.eql(204); - - await esTestIndexTool.waitForDocs( - 'action:test.system-action-kibana-privileges', - reference, - 1 - ); - }); - }); -} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index f1d7f59980c23..7a77ebcb1a432 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -22,7 +22,6 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./monitoring_collection')); loadTestFile(require.resolve('./execute')); - loadTestFile(require.resolve('./enqueue')); loadTestFile(require.resolve('./bulk_enqueue')); loadTestFile(require.resolve('./connector_types/stack/email')); loadTestFile(require.resolve('./connector_types/stack/email_html')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/esql_only.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/esql_only.ts new file mode 100644 index 0000000000000..eee79e38a2dac --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/esql_only.ts @@ -0,0 +1,340 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib'; +import { + createConnector, + ES_GROUPS_TO_WRITE, + ES_TEST_DATA_STREAM_NAME, + ES_TEST_INDEX_REFERENCE, + ES_TEST_INDEX_SOURCE, + ES_TEST_OUTPUT_INDEX_NAME, + getRuleServices, + RULE_INTERVALS_TO_WRITE, + RULE_INTERVAL_MILLIS, + RULE_INTERVAL_SECONDS, + RULE_TYPE_ID, +} from './common'; +import { createDataStream, deleteDataStream } from '../../../create_test_data'; + +// eslint-disable-next-line import/no-default-export +export default function ruleTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const { + es, + esTestIndexTool, + esTestIndexToolOutput, + esTestIndexToolDataStream, + createEsDocumentsInGroups, + removeAllAADDocs, + getAllAADDocs, + } = getRuleServices(getService); + + describe('rule', async () => { + let endDate: string; + let connectorId: string; + const objectRemover = new ObjectRemover(supertest); + + beforeEach(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + await esTestIndexToolOutput.destroy(); + await esTestIndexToolOutput.setup(); + + connectorId = await createConnector(supertest, objectRemover, ES_TEST_OUTPUT_INDEX_NAME); + + // write documents in the future, figure out the end date + const endDateMillis = Date.now() + (RULE_INTERVALS_TO_WRITE - 1) * RULE_INTERVAL_MILLIS; + endDate = new Date(endDateMillis).toISOString(); + + await createDataStream(es, ES_TEST_DATA_STREAM_NAME); + }); + + afterEach(async () => { + await objectRemover.removeAll(); + await esTestIndexTool.destroy(); + await esTestIndexToolOutput.destroy(); + await deleteDataStream(es, ES_TEST_DATA_STREAM_NAME); + await removeAllAADDocs(); + }); + + it('runs correctly: threshold on ungrouped hit count < >', async () => { + // write documents from now to the future end date in groups + await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate); + await createRule({ + name: 'never fire', + esqlQuery: 'from .kibana-alerting-test-data | stats c = count(date) | where c < 0', + size: 100, + }); + await createRule({ + name: 'always fire', + esqlQuery: 'from .kibana-alerting-test-data | stats c = count(date) | where c > -1', + size: 100, + }); + + const docs = await waitForDocs(2); + const messagePattern = + /rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Query matched documents over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/; + + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`rule 'always fire' matched query`); + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + } + + const aadDocs = await getAllAADDocs(1); + + const alertDoc = aadDocs.body.hits.hits[0]._source.kibana.alert; + expect(alertDoc.reason).to.match(messagePattern); + expect(alertDoc.title).to.be("rule 'always fire' matched query"); + expect(alertDoc.evaluation.conditions).to.be('Query matched documents'); + expect(alertDoc.evaluation.value).greaterThan(0); + expect(alertDoc.url).to.contain('/s/space1/app/'); + }); + + it('runs correctly: use epoch millis - threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + const endDateMillis = Date.now() + (RULE_INTERVALS_TO_WRITE - 1) * RULE_INTERVAL_MILLIS; + endDate = new Date(endDateMillis).toISOString(); + await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate); + await createRule({ + name: 'never fire', + esqlQuery: 'from .kibana-alerting-test-data | stats c = count(date) | where c < 0', + size: 100, + timeField: 'date_epoch_millis', + }); + await createRule({ + name: 'always fire', + esqlQuery: 'from .kibana-alerting-test-data | stats c = count(date) | where c > -1', + size: 100, + timeField: 'date_epoch_millis', + }); + + const docs = await waitForDocs(2); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`rule 'always fire' matched query`); + const messagePattern = + /rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Query matched documents over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + } + }); + + it('runs correctly: no matches', async () => { + await createRule({ + name: 'always fire', + esqlQuery: 'from .kibana-alerting-test-data | stats c = count(date) | where c < 1', + size: 100, + }); + + const docs = await waitForDocs(1); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`rule 'always fire' matched query`); + const messagePattern = + /rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Query matched documents over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + } + }); + + it('runs correctly and populates recovery context', async () => { + // This rule should be active initially when the number of documents is below the threshold + // and then recover when we add more documents. + await createRule({ + name: 'fire then recovers', + esqlQuery: 'from .kibana-alerting-test-data | stats c = count(date) | where c < 1', + size: 100, + notifyWhen: 'onActionGroupChange', + timeWindowSize: RULE_INTERVAL_SECONDS, + }); + + let docs = await waitForDocs(1); + const activeDoc = docs[0]; + const { + name: activeName, + title: activeTitle, + value: activeValue, + message: activeMessage, + } = activeDoc._source.params; + + expect(activeName).to.be('fire then recovers'); + expect(activeTitle).to.be(`rule 'fire then recovers' matched query`); + expect(activeValue).to.be('1'); + expect(activeMessage).to.match( + /rule 'fire then recovers' is active:\n\n- Value: \d+\n- Conditions Met: Query matched documents over 4s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/ + ); + await createEsDocumentsInGroups(1, endDate); + docs = await waitForDocs(2); + const recoveredDoc = docs[1]; + const { + name: recoveredName, + title: recoveredTitle, + message: recoveredMessage, + } = recoveredDoc._source.params; + + expect(recoveredName).to.be('fire then recovers'); + expect(recoveredTitle).to.be(`rule 'fire then recovers' recovered`); + expect(recoveredMessage).to.match( + /rule 'fire then recovers' is recovered:\n\n- Value: \d+\n- Conditions Met: Query did NOT match documents over 4s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/ + ); + }); + + it('runs correctly over a data stream: threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + await createEsDocumentsInGroups( + ES_GROUPS_TO_WRITE, + endDate, + esTestIndexToolDataStream, + ES_TEST_DATA_STREAM_NAME + ); + await createRule({ + name: 'never fire', + esqlQuery: 'from test-data-stream | stats c = count(@timestamp) | where c < 0', + size: 100, + }); + await createRule({ + name: 'always fire', + esqlQuery: 'from test-data-stream | stats c = count(@timestamp) | where c > -1', + size: 100, + }); + + const docs = await waitForDocs(2); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`rule 'always fire' matched query`); + const messagePattern = + /rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Query matched documents over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + } + }); + + async function waitForDocs(count: number): Promise { + return await esTestIndexToolOutput.waitForDocs( + ES_TEST_INDEX_SOURCE, + ES_TEST_INDEX_REFERENCE, + count + ); + } + + interface CreateRuleParams { + name: string; + size: number; + esqlQuery: string; + timeWindowSize?: number; + timeField?: string; + notifyWhen?: string; + aggType?: string; + aggField?: string; + groupBy?: string; + termField?: string; + termSize?: number; + } + + async function createRule(params: CreateRuleParams): Promise { + const action = { + id: connectorId, + group: 'query matched', + params: { + documents: [ + { + source: ES_TEST_INDEX_SOURCE, + reference: ES_TEST_INDEX_REFERENCE, + params: { + name: '{{{rule.name}}}', + value: '{{{context.value}}}', + title: '{{{context.title}}}', + message: '{{{context.message}}}', + }, + hits: '{{context.hits}}', + date: '{{{context.date}}}', + previousTimestamp: '{{{state.latestTimestamp}}}', + }, + ], + }, + }; + + const recoveryAction = { + id: connectorId, + group: 'recovered', + params: { + documents: [ + { + source: ES_TEST_INDEX_SOURCE, + reference: ES_TEST_INDEX_REFERENCE, + params: { + name: '{{{rule.name}}}', + value: '{{{context.value}}}', + title: '{{{context.title}}}', + message: '{{{context.message}}}', + }, + hits: '{{context.hits}}', + date: '{{{context.date}}}', + }, + ], + }, + }; + + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send({ + name: params.name, + consumer: 'alerts', + enabled: true, + rule_type_id: RULE_TYPE_ID, + schedule: { interval: `${RULE_INTERVAL_SECONDS}s` }, + actions: [action, recoveryAction], + notify_when: params.notifyWhen || 'onActiveAlert', + params: { + size: params.size, + timeWindowSize: params.timeWindowSize || RULE_INTERVAL_SECONDS * 5, + timeWindowUnit: 's', + thresholdComparator: '>', + threshold: [0], + searchType: 'esqlQuery', + aggType: params.aggType, + groupBy: params.groupBy, + aggField: params.aggField, + termField: params.termField, + termSize: params.termSize, + timeField: params.timeField || 'date', + esqlQuery: { esql: params.esqlQuery }, + }, + }) + .expect(200); + + const ruleId = createdRule.id; + objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); + + return ruleId; + } + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/index.ts index a584753db7c25..758737d9749b6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/index.ts @@ -12,5 +12,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { describe('es_query', () => { loadTestFile(require.resolve('./rule')); loadTestFile(require.resolve('./query_dsl_only')); + loadTestFile(require.resolve('./esql_only')); }); } diff --git a/x-pack/test/api_integration/apis/metrics_ui/metadata.ts b/x-pack/test/api_integration/apis/metrics_ui/metadata.ts index 5cdca5a369edf..9a7f41e1d2b67 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metadata.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metadata.ts @@ -99,38 +99,42 @@ export default function ({ getService }: FtrProviderContext) { if (metadata) { expect(metadata.features.length).to.be(58); expect(metadata.name).to.equal('gke-observability-8--observability-8--bc1afd95-f0zc'); - expect(metadata.info).to.eql({ - cloud: { - availability_zone: 'europe-west1-c', - instance: { - name: 'gke-observability-8--observability-8--bc1afd95-f0zc', - id: '6200309808276807579', - }, - provider: 'gcp', - machine: { type: 'n1-standard-4' }, - project: { id: 'elastic-observability' }, - }, - agent: { - hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', - id: 'c91c0d2b-6483-46bb-9731-f06afd32bb59', - ephemeral_id: '7cb259b1-795c-4c76-beaf-2eb8f18f5b02', - type: 'metricbeat', - version: '8.0.0', - }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', - os: { - kernel: '4.14.127+', - codename: 'Core', - name: 'CentOS Linux', - family: 'redhat', - version: '7 (Core)', - platform: 'centos', - }, - containerized: false, + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.above( + timeRange800withAws.from + ); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.below( + timeRange800withAws.to + ); + expect(metadata.info?.cloud).to.eql({ + availability_zone: 'europe-west1-c', + instance: { name: 'gke-observability-8--observability-8--bc1afd95-f0zc', - architecture: 'x86_64', + id: '6200309808276807579', + }, + provider: 'gcp', + machine: { type: 'n1-standard-4' }, + project: { id: 'elastic-observability' }, + }); + expect(metadata.info?.agent).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', + id: 'c91c0d2b-6483-46bb-9731-f06afd32bb59', + ephemeral_id: '7cb259b1-795c-4c76-beaf-2eb8f18f5b02', + type: 'metricbeat', + version: '8.0.0', + }); + expect(metadata.info?.host).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', + os: { + kernel: '4.14.127+', + codename: 'Core', + name: 'CentOS Linux', + family: 'redhat', + version: '7 (Core)', + platform: 'centos', }, + containerized: false, + name: 'gke-observability-8--observability-8--bc1afd95-f0zc', + architecture: 'x86_64', }); } else { throw new Error('Metadata should never be empty'); @@ -148,38 +152,42 @@ export default function ({ getService }: FtrProviderContext) { expect(metadata.features.length).to.be(19); expect(metadata.features.some((f) => f.name === 'aws.ec2')).to.be(true); expect(metadata.name).to.equal('ip-172-31-47-9.us-east-2.compute.internal'); - expect(metadata.info).to.eql({ - cloud: { - availability_zone: 'us-east-2c', - image: { id: 'ami-0d8f6eb4f641ef691' }, - instance: { id: 'i-011454f72559c510b' }, - provider: 'aws', - machine: { type: 't2.micro' }, - region: 'us-east-2', - account: { id: '015351775590' }, - }, - agent: { - hostname: 'ip-172-31-47-9.us-east-2.compute.internal', - id: 'd0943b36-d0d3-426d-892b-7d79c071b44b', - ephemeral_id: '64c94244-88b8-4a37-adc0-30428fefaf53', - type: 'metricbeat', - version: '8.0.0', - }, - host: { - hostname: 'ip-172-31-47-9.us-east-2.compute.internal', - os: { - kernel: '4.14.123-111.109.amzn2.x86_64', - codename: 'Karoo', - name: 'Amazon Linux', - family: 'redhat', - version: '2', - platform: 'amzn', - }, - containerized: false, - name: 'ip-172-31-47-9.us-east-2.compute.internal', - id: 'ded64cbff86f478990a3dfbb63a8d238', - architecture: 'x86_64', + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.above( + timeRange800withAws.from + ); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.below( + timeRange800withAws.to + ); + expect(metadata.info?.cloud).to.eql({ + availability_zone: 'us-east-2c', + image: { id: 'ami-0d8f6eb4f641ef691' }, + instance: { id: 'i-011454f72559c510b' }, + provider: 'aws', + machine: { type: 't2.micro' }, + region: 'us-east-2', + account: { id: '015351775590' }, + }); + expect(metadata.info?.agent).to.eql({ + hostname: 'ip-172-31-47-9.us-east-2.compute.internal', + id: 'd0943b36-d0d3-426d-892b-7d79c071b44b', + ephemeral_id: '64c94244-88b8-4a37-adc0-30428fefaf53', + type: 'metricbeat', + version: '8.0.0', + }); + expect(metadata.info?.host).to.eql({ + hostname: 'ip-172-31-47-9.us-east-2.compute.internal', + os: { + kernel: '4.14.123-111.109.amzn2.x86_64', + codename: 'Karoo', + name: 'Amazon Linux', + family: 'redhat', + version: '2', + platform: 'amzn', }, + containerized: false, + name: 'ip-172-31-47-9.us-east-2.compute.internal', + id: 'ded64cbff86f478990a3dfbb63a8d238', + architecture: 'x86_64', }); } else { throw new Error('Metadata should never be empty'); @@ -197,43 +205,47 @@ export default function ({ getService }: FtrProviderContext) { expect(metadata.features.length).to.be(29); // With this data set the `kubernetes.pod.name` fields have been removed. expect(metadata.name).to.equal('fluentd-gcp-v3.2.0-np7vw'); - expect(metadata.info).to.eql({ - cloud: { - instance: { - id: '6613144177892233360', - name: 'gke-observability-8--observability-8--bc1afd95-ngmh', - }, - provider: 'gcp', - availability_zone: 'europe-west1-c', - machine: { - type: 'n1-standard-4', - }, - project: { - id: 'elastic-observability', - }, + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.above( + timeRange800withAws.from + ); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.below( + timeRange800withAws.to + ); + expect(metadata.info?.cloud).to.eql({ + instance: { + id: '6613144177892233360', + name: 'gke-observability-8--observability-8--bc1afd95-ngmh', }, - agent: { - hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', - id: '66dc19e6-da36-49d2-9471-2c9475503178', - ephemeral_id: 'a0c3a9ff-470a-41a0-bf43-d1af6b7a3b5b', - type: 'metricbeat', - version: '8.0.0', + provider: 'gcp', + availability_zone: 'europe-west1-c', + machine: { + type: 'n1-standard-4', }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', - name: 'gke-observability-8--observability-8--bc1afd95-ngmh', - os: { - codename: 'Core', - family: 'redhat', - kernel: '4.14.127+', - name: 'CentOS Linux', - platform: 'centos', - version: '7 (Core)', - }, - architecture: 'x86_64', - containerized: false, + project: { + id: 'elastic-observability', }, }); + expect(metadata.info?.agent).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', + id: '66dc19e6-da36-49d2-9471-2c9475503178', + ephemeral_id: 'a0c3a9ff-470a-41a0-bf43-d1af6b7a3b5b', + type: 'metricbeat', + version: '8.0.0', + }); + expect(metadata.info?.host).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', + name: 'gke-observability-8--observability-8--bc1afd95-ngmh', + os: { + codename: 'Core', + family: 'redhat', + kernel: '4.14.127+', + name: 'CentOS Linux', + platform: 'centos', + version: '7 (Core)', + }, + architecture: 'x86_64', + containerized: false, + }); } else { throw new Error('Metadata should never be empty'); } @@ -248,45 +260,49 @@ export default function ({ getService }: FtrProviderContext) { }); if (metadata) { expect(metadata.features.length).to.be(26); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.above( + timeRange800withAws.from + ); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.below( + timeRange800withAws.to + ); expect(metadata.name).to.equal( 'k8s_prometheus-to-sd-exporter_fluentd-gcp-v3.2.0-w68r5_kube-system_26950cde-9aed-11e9-9a96-42010a84004d_0' ); - expect(metadata.info).to.eql({ - cloud: { - instance: { - id: '4039094952262994102', - name: 'gke-observability-8--observability-8--bc1afd95-nhhw', - }, - provider: 'gcp', - availability_zone: 'europe-west1-c', - machine: { - type: 'n1-standard-4', - }, - project: { - id: 'elastic-observability', - }, + expect(metadata.info?.cloud).to.eql({ + instance: { + id: '4039094952262994102', + name: 'gke-observability-8--observability-8--bc1afd95-nhhw', }, - agent: { - hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', - id: 'c58a514c-e971-4590-8206-385400e184dd', - ephemeral_id: 'e9d46cb0-2e89-469d-bd3b-6f32d7c96cc0', - type: 'metricbeat', - version: '8.0.0', + provider: 'gcp', + availability_zone: 'europe-west1-c', + machine: { + type: 'n1-standard-4', }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', - name: 'gke-observability-8--observability-8--bc1afd95-nhhw', - os: { - codename: 'Core', - family: 'redhat', - kernel: '4.14.127+', - name: 'CentOS Linux', - platform: 'centos', - version: '7 (Core)', - }, - architecture: 'x86_64', - containerized: false, + project: { + id: 'elastic-observability', + }, + }); + expect(metadata.info?.agent).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', + id: 'c58a514c-e971-4590-8206-385400e184dd', + ephemeral_id: 'e9d46cb0-2e89-469d-bd3b-6f32d7c96cc0', + type: 'metricbeat', + version: '8.0.0', + }); + expect(metadata.info?.host).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', + name: 'gke-observability-8--observability-8--bc1afd95-nhhw', + os: { + codename: 'Core', + family: 'redhat', + kernel: '4.14.127+', + name: 'CentOS Linux', + platform: 'centos', + version: '7 (Core)', }, + architecture: 'x86_64', + containerized: false, }); } else { throw new Error('Metadata should never be empty'); diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts index b4b7eb8be2a3a..179ac28a0c939 100644 --- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts @@ -22,7 +22,9 @@ import { PrivateLocationTestService } from './services/private_location_test_ser import { comparePolicies, getTestSyntheticsPolicy } from './sample_data/test_policy'; export default function ({ getService }: FtrProviderContext) { - describe('SyncGlobalParams', function () { + // FLAKY: https://github.com/elastic/kibana/issues/162594 + // Failing: See https://github.com/elastic/kibana/issues/162594 + describe.skip('SyncGlobalParams', function () { this.tags('skipCloud'); const supertestAPI = getService('supertest'); const kServer = getService('kibanaServer'); diff --git a/x-pack/test/api_integration/apis/transform/stop_transforms.ts b/x-pack/test/api_integration/apis/transform/stop_transforms.ts index 7811432e7417a..c982c6b45f1c9 100644 --- a/x-pack/test/api_integration/apis/transform/stop_transforms.ts +++ b/x-pack/test/api_integration/apis/transform/stop_transforms.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import type { PutTransformsRequestSchema } from '@kbn/transform-plugin/common/api_schemas/transforms'; import type { StopTransformsRequestSchema } from '@kbn/transform-plugin/common/api_schemas/stop_transforms'; -import { isStopTransformsResponseSchema } from '@kbn/transform-plugin/common/api_schemas/type_guards'; import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; @@ -76,7 +75,6 @@ export default ({ getService }: FtrProviderContext) => { .send(reqBody); transform.api.assertResponseStatusCode(200, status, body); - expect(isStopTransformsResponseSchema(body)).to.eql(true); expect(body[transformId].success).to.eql(true); expect(typeof body[transformId].error).to.eql('undefined'); await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); @@ -97,7 +95,6 @@ export default ({ getService }: FtrProviderContext) => { .send(reqBody); transform.api.assertResponseStatusCode(200, status, body); - expect(isStopTransformsResponseSchema(body)).to.eql(true); expect(body[transformId].success).to.eql(false); expect(typeof body[transformId].error).to.eql('object'); @@ -121,7 +118,6 @@ export default ({ getService }: FtrProviderContext) => { .send(reqBody); transform.api.assertResponseStatusCode(200, status, body); - expect(isStopTransformsResponseSchema(body)).to.eql(true); expect(body.invalid_transform_id.success).to.eql(false); expect(body.invalid_transform_id).to.have.property('error'); }); @@ -158,8 +154,6 @@ export default ({ getService }: FtrProviderContext) => { .send(reqBody); transform.api.assertResponseStatusCode(200, status, body); - expect(isStopTransformsResponseSchema(body)).to.eql(true); - await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { expect(body[transformId].success).to.eql(true); await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); @@ -183,8 +177,6 @@ export default ({ getService }: FtrProviderContext) => { ]); transform.api.assertResponseStatusCode(200, status, body); - expect(isStopTransformsResponseSchema(body)).to.eql(true); - await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { expect(body[transformId].success).to.eql(true); await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); diff --git a/x-pack/test/api_integration/apis/transform/transforms.ts b/x-pack/test/api_integration/apis/transform/transforms.ts index 4c3d1641d6d5f..2016876ba93a6 100644 --- a/x-pack/test/api_integration/apis/transform/transforms.ts +++ b/x-pack/test/api_integration/apis/transform/transforms.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import type { GetTransformsResponseSchema } from '@kbn/transform-plugin/common/api_schemas/transforms'; -import { isGetTransformsResponseSchema } from '@kbn/transform-plugin/common/api_schemas/type_guards'; import { getCommonRequestHeader } from '../../../functional/services/ml/common_api'; import { USER } from '../../../functional/services/transform/security_common'; @@ -43,8 +42,6 @@ export default ({ getService }: FtrProviderContext) => { } function assertTransformsResponseBody(body: GetTransformsResponseSchema) { - expect(isGetTransformsResponseSchema(body)).to.eql(true); - expect(body.count).to.eql(expected.apiTransformTransforms.count); expect(body.transforms).to.have.length(expected.apiTransformTransforms.count); @@ -62,8 +59,6 @@ export default ({ getService }: FtrProviderContext) => { } function assertSingleTransformResponseBody(body: GetTransformsResponseSchema) { - expect(isGetTransformsResponseSchema(body)).to.eql(true); - expect(body.count).to.eql(expected.apiTransformTransformsTransformId.count); expect(body.transforms).to.have.length(expected.apiTransformTransformsTransformId.count); diff --git a/x-pack/test/api_integration/apis/transform/transforms_nodes.ts b/x-pack/test/api_integration/apis/transform/transforms_nodes.ts index bf3e91df69684..10f4413a8f85f 100644 --- a/x-pack/test/api_integration/apis/transform/transforms_nodes.ts +++ b/x-pack/test/api_integration/apis/transform/transforms_nodes.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import type { GetTransformNodesResponseSchema } from '@kbn/transform-plugin/common/api_schemas/transforms'; -import { isGetTransformNodesResponseSchema } from '@kbn/transform-plugin/common/api_schemas/type_guards'; import { getCommonRequestHeader } from '../../../functional/services/ml/common_api'; import { USER } from '../../../functional/services/transform/security_common'; @@ -25,8 +24,6 @@ export default ({ getService }: FtrProviderContext) => { }; function assertTransformsNodesResponseBody(body: GetTransformNodesResponseSchema) { - expect(isGetTransformNodesResponseSchema(body)).to.eql(true); - expect(body.count).to.not.be.lessThan(expected.apiTransformTransformsNodes.minCount); } diff --git a/x-pack/test/api_integration/apis/transform/transforms_stats.ts b/x-pack/test/api_integration/apis/transform/transforms_stats.ts index 41cdafd0c058f..aea5c049343e9 100644 --- a/x-pack/test/api_integration/apis/transform/transforms_stats.ts +++ b/x-pack/test/api_integration/apis/transform/transforms_stats.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import type { GetTransformsStatsResponseSchema } from '@kbn/transform-plugin/common/api_schemas/transforms_stats'; -import { isGetTransformsStatsResponseSchema } from '@kbn/transform-plugin/common/api_schemas/type_guards'; import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; import { getCommonRequestHeader } from '../../../functional/services/ml/common_api'; @@ -39,7 +38,6 @@ export default ({ getService }: FtrProviderContext) => { } function assertTransformsStatsResponseBody(body: GetTransformsStatsResponseSchema) { - expect(isGetTransformsStatsResponseSchema(body)).to.eql(true); expect(body.count).to.eql(expected.apiTransformTransforms.count); expect(body.transforms).to.have.length(expected.apiTransformTransforms.count); diff --git a/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts b/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts index 769041847bb3f..2b02096f57d39 100644 --- a/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts +++ b/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts @@ -27,16 +27,6 @@ const SAMPLE_SOURCEMAP = { mappings: 'A,AAAB;;ABCDE;', }; -const SAMPLE_ANDROID_MAP = `# compiler: R8 -# compiler_version: 3.2.47 -# min_api: 26 -# common_typos_disable -# {"id":"com.android.tools.r8.mapping","version":"2.0"} -# pg_map_id: 127b14c -# pg_map_hash: SHA-256 127b14c0be5dd1b55beee544a8d0e7c9414b432868ed8bc54ca5cc43cba12435 -a1.TableInfo$ForeignKey$$ExternalSyntheticOutline0 -> a1.e: -# {"id":"sourceFile","fileName":"R8$$SyntheticClass"}`; - export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); @@ -109,28 +99,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { return response.body; } - async function uploadAndroidMap({ - serviceName, - serviceVersion, - androidMap, - }: { - serviceName: string; - serviceVersion: string; - androidMap: string; - }) { - const response = await apmApiClient.writeUser({ - endpoint: 'POST /api/apm/androidmaps 2023-10-31', - type: 'form-data', - params: { - body: { - service_name: serviceName, - service_version: serviceVersion, - map_file: androidMap, - }, - }, - }); - return response.body; - } async function runSourceMapMigration() { await apmApiClient.writeUser({ endpoint: 'POST /internal/apm/sourcemaps/migrate_fleet_artifacts', @@ -160,12 +128,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { await Promise.all([deleteAllFleetSourceMaps(), deleteAllApmSourceMaps()]); }); - async function getDecodedMapContent(encodedContent?: string): Promise { - if (encodedContent) { - return (await unzip(Buffer.from(encodedContent, 'base64'))).toString(); - } - } - async function getDecodedSourceMapContent( encodedContent?: string ): Promise { @@ -281,111 +243,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - let androidResp: APIReturnType<'POST /api/apm/androidmaps 2023-10-31'>; - describe('upload android map', () => { - after(async () => { - await apmApiClient.writeUser({ - endpoint: 'DELETE /api/apm/sourcemaps/{id} 2023-10-31', - params: { path: { id: androidResp.id } }, - }); - }); - - before(async () => { - androidResp = await uploadAndroidMap({ - serviceName: 'uploading-test', - serviceVersion: '1.0.0', - androidMap: SAMPLE_ANDROID_MAP, - }); - - await waitForSourceMapCount(1); - }); - - it('is uploaded as a fleet artifact', async () => { - const res = await es.search({ - index: '.fleet-artifacts', - size: 1, - query: { - bool: { - filter: [{ term: { type: 'sourcemap' } }, { term: { package_name: 'apm' } }], - }, - }, - }); - - // @ts-expect-error - expect(res.hits.hits[0]._source.identifier).to.be('uploading-test-1.0.0-android'); - }); - - it('is added to .apm-source-map index', async () => { - const res = await es.search({ - index: '.apm-source-map', - }); - - const source = res.hits.hits[0]._source; - const decodedSourceMap = await getDecodedMapContent(source?.content); - expect(decodedSourceMap).to.eql(SAMPLE_ANDROID_MAP); - expect(source?.content_sha256).to.be( - '702e07279b0fbed47fdbf5e71528dff845b4f07a16ca79cab0c1b06eb71be966' - ); - expect(source?.file.path).to.be('android'); - expect(source?.service.name).to.be('uploading-test'); - expect(source?.service.version).to.be('1.0.0'); - }); - - describe('when uploading a new android map with the same service.name and service.version', () => { - let resBefore: GetResponse; - let resAfter: GetResponse; - - before(async () => { - async function getSourceMapDocFromApmIndex() { - await es.indices.refresh({ index: '.apm-source-map' }); - return await es.get({ - index: '.apm-source-map', - id: 'uploading-test-1.0.0-android', - }); - } - - resBefore = await getSourceMapDocFromApmIndex(); - - await uploadAndroidMap({ - serviceName: 'uploading-test', - serviceVersion: '1.0.0', - androidMap: '# compiler: R8\n# ANOTHER MAP', - }); - - resAfter = await getSourceMapDocFromApmIndex(); - }); - - after(async () => { - await deleteAllApmSourceMaps(); - await deleteAllFleetSourceMaps(); - }); - - it('creates one document in the .apm-source-map index', async () => { - const res = await es.search({ index: '.apm-source-map', size: 0 }); - - // @ts-expect-error - expect(res.hits.total.value).to.be(1); - }); - - it('creates two documents in the .fleet-artifacts index', async () => { - const res = await listSourcemaps({ page: 1, perPage: 10 }); - expect(res.total).to.be(2); - }); - - it('updates the content', async () => { - const contentBefore = await getDecodedMapContent(resBefore._source?.content); - const contentAfter = await getDecodedMapContent(resAfter._source?.content); - - expect(contentBefore).to.be(SAMPLE_ANDROID_MAP); - expect(contentAfter).to.be('# compiler: R8\n# ANOTHER MAP'); - }); - - it('updates the content hash', async () => { - expect(resBefore._source?.content_sha256).to.not.be(resAfter._source?.content_sha256); - }); - }); - }); - describe('list source maps', async () => { before(async () => { const totalCount = 6; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts index 70021ffe0be3b..cc1216e80dd8f 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts @@ -111,7 +111,8 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsClosed.hits.hits.length).to.equal(10); }); - it('should be able close 10 signals immediately and they all should be closed', async () => { + // Test is failing after changing refresh to false + it.skip('should be able close 10 signals immediately and they all should be closed', async () => { const rule = { ...getRuleForSignalTesting(['auditbeat-*']), query: 'process.executable: "/usr/bin/sudo"', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/open_close_signals.ts index bafde22beb1de..f66bec45e45a1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/open_close_signals.ts @@ -50,29 +50,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // remove any server generated items that are nondeterministic - body.items.forEach((_: any, index: number) => { - delete body.items[index].update.error.index_uuid; - }); delete body.took; - expect(body).to.eql({ - errors: true, - items: [ - { - update: { - _id: '123', - _index: '.internal.alerts-security.alerts-default-000001', - error: { - index: '.internal.alerts-security.alerts-default-000001', - reason: '[123]: document missing', - shard: '0', - type: 'document_missing_exception', - }, - status: 404, - }, - }, - ], - }); + expect(body).to.eql(getAlertUpdateByQueryEmptyResponse()); }); it('should not give errors when querying and the signals index does exist and is empty', async () => { @@ -84,29 +64,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // remove any server generated items that are nondeterministic - body.items.forEach((_: any, index: number) => { - delete body.items[index].update.error.index_uuid; - }); delete body.took; - expect(body).to.eql({ - errors: true, - items: [ - { - update: { - _id: '123', - _index: '.internal.alerts-security.alerts-default-000001', - error: { - index: '.internal.alerts-security.alerts-default-000001', - reason: '[123]: document missing', - shard: '0', - type: 'document_missing_exception', - }, - status: 404, - }, - }, - ], - }); + expect(body).to.eql(getAlertUpdateByQueryEmptyResponse()); await deleteAllAlerts(supertest, log, es); }); @@ -218,7 +178,8 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsClosed.hits.hits.length).to.equal(10); }); - it('should be able close signals immediately and they all should be closed', async () => { + // Test is failing after changing refresh to false + it.skip('should be able close signals immediately and they all should be closed', async () => { const rule = { ...getRuleForSignalTesting(['auditbeat-*']), query: 'process.executable: "/usr/bin/sudo"', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/set_alert_tags.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/set_alert_tags.ts index 12815b0635db9..6f64bd313be45 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/set_alert_tags.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/set_alert_tags.ts @@ -65,7 +65,8 @@ export default ({ getService }: FtrProviderContext) => { }); }); - describe('tests with auditbeat data', () => { + // Test is failing after changing refresh to false + describe.skip('tests with auditbeat data', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); }); diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 4c52547e67aef..6cf131939807c 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -19,6 +19,22 @@ export default function (providerContext: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const es = getService('es'); + const getPackage = async (pkgName: string) => { + const getPkgRes = await supertest + .get(`/api/fleet/epm/packages/${pkgName}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + return getPkgRes; + }; + const epmInstall = async (pkgName: string, pkgVersion: string) => { + const getPkgRes = await supertest + .post(`/api/fleet/epm/packages/${pkgName}/${pkgVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + return getPkgRes; + }; + describe('fleet_agent_policies', () => { skipIfNoDockerRegistry(providerContext); @@ -89,7 +105,7 @@ export default function (providerContext: FtrProviderContext) { let packagePoliciesToDeleteIds: string[] = []; after(async () => { if (systemPkgVersion) { - await supertest.delete(`/api/fleet/epm/packages/system-${systemPkgVersion}`); + await supertest.delete(`/api/fleet/epm/packages/system/${systemPkgVersion}`); } if (packagePoliciesToDeleteIds.length > 0) { await kibanaServer.savedObjects.bulkDelete({ @@ -297,21 +313,14 @@ export default function (providerContext: FtrProviderContext) { .expect(409); }); - it('should allow to create policy with the system integration policy and increment correctly the name if there is more than 10 package policy', async () => { + it('should allow to create policy with the system integration policy and increment correctly the name if package policies are more than 10', async () => { // load a bunch of fake system integration policy const policyIds = new Array(10).fill(null).map((_, i) => `package-policy-test-${i}`); packagePoliciesToDeleteIds = packagePoliciesToDeleteIds.concat(policyIds); - const getPkRes = await supertest - .get(`/api/fleet/epm/packages/system`) - .set('kbn-xsrf', 'xxxx') - .expect(200); + const getPkRes = await getPackage('system'); systemPkgVersion = getPkRes.body.item.version; // we must first force install the system package to override package verification error on policy create - const installPromise = supertest - .post(`/api/fleet/epm/packages/system-${systemPkgVersion}`) - .set('kbn-xsrf', 'xxxx') - .send({ force: true }) - .expect(200); + const installPromise = await epmInstall('system', `${systemPkgVersion}`); await Promise.all([ installPromise, @@ -419,7 +428,7 @@ export default function (providerContext: FtrProviderContext) { await Promise.all(deletedPromises); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/agents'); if (systemPkgVersion) { - await supertest.delete(`/api/fleet/epm/packages/system-${systemPkgVersion}`); + await supertest.delete(`/api/fleet/epm/packages/system/${systemPkgVersion}`); } if (packagePoliciesToDeleteIds.length > 0) { await kibanaServer.savedObjects.bulkDelete({ @@ -591,17 +600,10 @@ export default function (providerContext: FtrProviderContext) { const policyId = 'package-policy-test-'; packagePoliciesToDeleteIds.push(policyId); - const getPkRes = await supertest - .get(`/api/fleet/epm/packages/system`) - .set('kbn-xsrf', 'xxxx') - .expect(200); + const getPkRes = await getPackage('system'); systemPkgVersion = getPkRes.body.item.version; // we must first force install the system package to override package verification error on policy create - const installPromise = supertest - .post(`/api/fleet/epm/packages/system-${systemPkgVersion}`) - .set('kbn-xsrf', 'xxxx') - .send({ force: true }) - .expect(200); + const installPromise = await epmInstall('system', `${systemPkgVersion}`); await Promise.all([ installPromise, @@ -682,17 +684,10 @@ export default function (providerContext: FtrProviderContext) { it('should work with package policy with space in name', async () => { const policyId = 'package-policy-test-1'; packagePoliciesToDeleteIds.push(policyId); - const getPkRes = await supertest - .get(`/api/fleet/epm/packages/system`) - .set('kbn-xsrf', 'xxxx') - .expect(200); + const getPkRes = await getPackage('system'); systemPkgVersion = getPkRes.body.item.version; // we must first force install the system package to override package verification error on policy create - const installPromise = supertest - .post(`/api/fleet/epm/packages/system-${systemPkgVersion}`) - .set('kbn-xsrf', 'xxxx') - .send({ force: true }) - .expect(200); + const installPromise = await epmInstall('system', `${systemPkgVersion}`); await Promise.all([ installPromise, @@ -1192,17 +1187,10 @@ export default function (providerContext: FtrProviderContext) { }); setupFleetAndAgents(providerContext); before(async () => { - const getPkRes = await supertest - .get(`/api/fleet/epm/packages/system`) - .set('kbn-xsrf', 'xxxx') - .expect(200); + const getPkRes = await getPackage('system'); // we must first force install the system package to override package verification error on policy create - await supertest - .post(`/api/fleet/epm/packages/system-${getPkRes.body.item.version}`) - .set('kbn-xsrf', 'xxxx') - .send({ force: true }) - .expect(200); + await epmInstall('system', `${getPkRes.body.item.version}`); const { body: { item: createdPolicy }, diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index 328e4a240e77c..845a0b1a83659 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -216,6 +216,7 @@ export default function ({ getService }: FtrProviderContext) { it('should return a status summary if getStatusSummary provided', async () => { const { body: apiResponse } = await supertest .get('/api/fleet/agents?getStatusSummary=true&perPage=0') + .set('kbn-xsrf', 'xxxx') .expect(200); expect(apiResponse.items).to.eql([]); diff --git a/x-pack/test/fleet_api_integration/apis/agents/status.ts b/x-pack/test/fleet_api_integration/apis/agents/status.ts index 498fbe7c42bce..b0879afc5ccb7 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/status.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/status.ts @@ -8,6 +8,8 @@ import expect from '@kbn/expect'; import { INGEST_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import { API_VERSIONS } from '@kbn/fleet-plugin/common/constants'; + import { AGENTS_INDEX } from '@kbn/fleet-plugin/common'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { testUsers } from '../test_users'; @@ -230,7 +232,11 @@ export default function ({ getService }: FtrProviderContext) { }); it('should work with deprecated api', async () => { - await supertest.get(`/api/fleet/agent-status`).expect(200); + await supertest + .get(`/api/fleet/agent-status`) + .set('kbn-xsrf', 'xxxx') + .set('Elastic-Api-Version', `${API_VERSIONS.internal.v1}`) + .expect(200); }); it('should work with adequate package privileges', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index d717c6e285c04..47c4d7ecd9f7b 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; +import { API_VERSIONS } from '@kbn/fleet-plugin/common/constants'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { setupFleetAndAgents, getEsClientForAPIKey } from '../agents/services'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -324,6 +325,7 @@ export default function (providerContext: FtrProviderContext) { const { body: apiResponse } = await supertest .post(`/api/fleet/enrollment-api-keys`) .set('kbn-xsrf', 'xxx') + .set('Elastic-Api-Version', `${API_VERSIONS.internal.v1}`) .send({ policy_id: 'policy1', }) @@ -332,11 +334,20 @@ export default function (providerContext: FtrProviderContext) { }); it('should get and delete with deprecated API', async () => { - await supertest.get(`/api/fleet/enrollment-api-keys`).expect(200); - await supertest.get(`/api/fleet/enrollment-api-keys/${ENROLLMENT_KEY_ID}`).expect(200); + await supertest + .get(`/api/fleet/enrollment-api-keys`) + .set('Elastic-Api-Version', `${API_VERSIONS.internal.v1}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + await supertest + .get(`/api/fleet/enrollment-api-keys/${ENROLLMENT_KEY_ID}`) + .set('Elastic-Api-Version', `${API_VERSIONS.internal.v1}`) + .set('kbn-xsrf', 'xxx') + .expect(200); await supertest .delete(`/api/fleet/enrollment-api-keys/${keyId}`) + .set('Elastic-Api-Version', `${API_VERSIONS.internal.v1}`) .set('kbn-xsrf', 'xxx') .expect(200); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/get.ts b/x-pack/test/fleet_api_integration/apis/epm/get.ts index fb15de6847952..2ca984ceb67dd 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/get.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/get.ts @@ -200,7 +200,10 @@ export default function (providerContext: FtrProviderContext) { it('returns package info in item field when calling without version', async function () { // this will install through the registry by default await installPackage(testPkgName, testPkgVersion); - const res = await supertest.get(`/api/fleet/epm/packages/${testPkgName}`).expect(200); + const res = await supertest + .get(`/api/fleet/epm/packages/${testPkgName}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); const packageInfo = res.body.item; // the uploaded version will have this description expect(packageInfo.name).to.equal('apache'); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts b/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts index 1c4a53760e8d1..2ba688dfb8bf5 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts @@ -46,8 +46,7 @@ export default function (providerContext: FtrProviderContext) { pkgVersion = getResp.body.response.version; }); - // FLAKY: https://github.com/elastic/kibana/issues/156941 - describe.skip('install', () => { + describe('install', () => { transforms.forEach((transform) => { it(`should have installed the [${transform.id}] transform`, async function () { const res = await es.transport.request( diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_integration_in_multiple_spaces.ts b/x-pack/test/fleet_api_integration/apis/epm/install_integration_in_multiple_spaces.ts index b0be168685d89..d6023ae0492e1 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_integration_in_multiple_spaces.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_integration_in_multiple_spaces.ts @@ -67,8 +67,7 @@ export default function (providerContext: FtrProviderContext) { }) .catch(() => {}); - // FLAKY: https://github.com/elastic/kibana/issues/161624 - describe.skip('When installing system integration in multiple spaces', async () => { + describe('When installing system integration in multiple spaces', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_prerelease.ts b/x-pack/test/fleet_api_integration/apis/epm/install_prerelease.ts index f9d72c62e2738..7b300c271e304 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_prerelease.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_prerelease.ts @@ -22,7 +22,6 @@ export default function (providerContext: FtrProviderContext) { await supertest.delete(`/api/fleet/epm/packages/${pkg}/${version}`).set('kbn-xsrf', 'xxxx'); }; - // Failing: See https://github.com/elastic/kibana/issues/150343 describe('installs package that has a prerelease version', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); @@ -70,13 +69,21 @@ export default function (providerContext: FtrProviderContext) { expect(response.body.items.find((item: any) => item.id.includes(gaVersion))); }); - it('should install the beta package when no version is provided and prerelease is true', async function () { + it('should install the beta package when prerelease is true', async function () { const response = await supertest - .post(`/api/fleet/epm/packages/${testPackage}?prerelease=true`) + .post(`/api/fleet/epm/packages/${testPackage}/${testPackageVersion}?prerelease=true`) .set('kbn-xsrf', 'xxxx') .send({ force: true }) // using force to ignore package verification error .expect(200); + expect(response.body.items.find((item: any) => item.id.includes(betaVersion))); + }); + it('should install the beta package when no version is provided and prerelease is true', async function () { + const response = await supertest + .post(`/api/fleet/epm/packages/${testPackage}/${testPackageVersion}?prerelease=true`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) // using force to ignore package verification error + .expect(200); expect(response.body.items.find((item: any) => item.id.includes(betaVersion))); }); diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/get.ts b/x-pack/test/fleet_api_integration/apis/package_policy/get.ts index ce0a7a1f219c6..6801c3908e658 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/get.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/get.ts @@ -24,6 +24,14 @@ export default function (providerContext: FtrProviderContext) { // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions + const deleteEndpointPackage = async () => { + await supertest + .delete(`/api/fleet/epm/packages/endpoint/8.6.1`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }; + describe('Package Policy APIs', () => { skipIfNoDockerRegistry(providerContext); @@ -111,12 +119,7 @@ export default function (providerContext: FtrProviderContext) { .send({ packagePolicyIds: [packagePolicyId, endpointPackagePolicyId] }) .expect(200); - // uninstall endpoint package - await supertest - .delete(`/api/fleet/epm/packages/endpoint-8.6.1`) - .set('kbn-xsrf', 'xxxx') - .send({ force: true }) - .expect(200); + await deleteEndpointPackage(); }); it('should succeed with a valid id', async function () { @@ -254,12 +257,7 @@ export default function (providerContext: FtrProviderContext) { .send({ packagePolicyIds: [packagePolicyId, endpointPackagePolicyId] }) .expect(200); - // uninstall endpoint package - await supertest - .delete(`/api/fleet/epm/packages/endpoint-8.6.1`) - .set('kbn-xsrf', 'xxxx') - .send({ force: true }) - .expect(200); + await deleteEndpointPackage(); }); it('should succeed with valid ids', async function () { @@ -476,12 +474,7 @@ export default function (providerContext: FtrProviderContext) { .send({ packagePolicyIds: [endpointPackagePolicyId] }) .expect(200); - // uninstall endpoint package - await supertest - .delete(`/api/fleet/epm/packages/endpoint-8.6.1`) - .set('kbn-xsrf', 'xxxx') - .send({ force: true }) - .expect(200); + await deleteEndpointPackage(); }); it('should return 200 if the passed kuery is correct', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/input_package_create_upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/input_package_create_upgrade.ts index 263b9ac88adac..a1e6fe5d5556f 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/input_package_create_upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/input_package_create_upgrade.ts @@ -33,7 +33,7 @@ export default function (providerContext: FtrProviderContext) { }; const getInstallationSavedObject = async (name: string, version: string) => { - const res = await supertest.get(`/api/fleet/epm/packages/${name}-${version}`).expect(200); + const res = await supertest.get(`/api/fleet/epm/packages/${name}/${version}`).expect(200); return res.body.item.savedObject.attributes; }; diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts index 15ad0a82b4359..865341abc5bcc 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts @@ -25,7 +25,7 @@ export default function (providerContext: FtrProviderContext) { }; const getInstallationSavedObject = async (name: string, version: string) => { - const res = await supertest.get(`/api/fleet/epm/packages/${name}-${version}`).expect(200); + const res = await supertest.get(`/api/fleet/epm/packages/${name}/${version}`).expect(200); return res.body.item.savedObject.attributes; }; @@ -229,11 +229,11 @@ export default function (providerContext: FtrProviderContext) { .send({ agentPolicyId }); // uninstall endpoint package await supertest - .delete(`/api/fleet/epm/packages/endpoint-8.6.1`) + .delete(`/api/fleet/epm/packages/endpoint/8.6.1`) .set('kbn-xsrf', 'xxxx') .expect(200); await supertest - .delete(`/api/fleet/epm/packages/input_package-1.0.0`) + .delete(`/api/fleet/epm/packages/input_package/1.0.0`) .set('kbn-xsrf', 'xxxx') .expect(200); }); diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index 557d5a9b4d9b2..c5c3d6a67a829 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -36,7 +36,7 @@ export default function (providerContext: FtrProviderContext) { } const getInstallationSavedObject = async (name: string, version: string) => { - const res = await supertest.get(`/api/fleet/epm/packages/${name}-${version}`).expect(200); + const res = await supertest.get(`/api/fleet/epm/packages/${name}/${version}`).expect(200); return res.body.item.savedObject.attributes; }; diff --git a/x-pack/test/fleet_api_integration/apis/service_tokens.ts b/x-pack/test/fleet_api_integration/apis/service_tokens.ts index bd13c3f5b8ca8..e1830a8ac7f97 100644 --- a/x-pack/test/fleet_api_integration/apis/service_tokens.ts +++ b/x-pack/test/fleet_api_integration/apis/service_tokens.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { API_VERSIONS } from '@kbn/fleet-plugin/common/constants'; import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; export default function (providerContext: FtrProviderContext) { @@ -46,7 +47,11 @@ export default function (providerContext: FtrProviderContext) { }); it('should work with deprecated api', async () => { - await supertest.post(`/api/fleet/service-tokens`).set('kbn-xsrf', 'xxxx').expect(200); + await supertest + .post(`/api/fleet/service-tokens`) + .set('kbn-xsrf', 'xxxx') + .set('Elastic-Api-Version', `${API_VERSIONS.internal.v1}`) + .expect(200); }); }); } diff --git a/x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts index 80354eda9eefd..bf76152a459c4 100644 --- a/x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts @@ -128,7 +128,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample flights data set dashboard', async () => { - await appMenu.clickLink('Dashboard'); + await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); @@ -144,7 +144,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); - await appMenu.clickLink('Dashboard'); + await appMenu.clickLink('Dashboard', { + category: 'recentlyViewed', + closeCollapsibleNav: true, + }); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking charts rendered'); @@ -157,7 +160,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); - await appMenu.clickLink('Dashboard'); + await appMenu.clickLink('Dashboard', { + category: 'recentlyViewed', + closeCollapsibleNav: true, + }); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking charts rendered'); @@ -170,7 +176,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); - await appMenu.clickLink('Dashboard'); + await appMenu.clickLink('Dashboard', { + category: 'recentlyViewed', + closeCollapsibleNav: true, + }); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking charts rendered'); diff --git a/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts index 2604b942f7313..aec34ba0d5d9e 100644 --- a/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts @@ -16,7 +16,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const kibanaServer = getService('kibanaServer'); - describe('dashboard lens by value', function () { + // FLAKY: https://github.com/elastic/kibana/issues/165461 + describe.skip('dashboard lens by value', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load( diff --git a/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts index d9a0062298eec..5503d1a08dc4e 100644 --- a/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts @@ -60,7 +60,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); if (!saveToDashboard) { - await appsMenu.clickLink('Dashboard'); + await appsMenu.clickLink('Dashboard', { + category: 'kibana', + closeCollapsibleNav: true, + }); } } else { await PageObjects.maps.clickSaveAndReturnButton(); diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 75fa7feb02057..173869a85465f 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -29,6 +29,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }; const getReport = async () => { + // close any open notification toasts + await PageObjects.reporting.clearToastNotifications(); + await PageObjects.reporting.openCsvReportingPanel(); await PageObjects.reporting.clickGenerateReportButton(); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 76b380274f207..8b6f830a11406 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -45,7 +45,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.unsetTime(); }); - describe('Customize time range', () => { + // FLAKY: https://github.com/elastic/kibana/issues/104578 + describe.skip('Customize time range', () => { it('should be possible to customize time range for saved searches on dashboards', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts index 32cd9b5360da0..a61a2abba1870 100644 --- a/x-pack/test/functional/apps/discover/visualize_field.ts +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -33,7 +33,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const monacoEditor = getService('monacoEditor'); const defaultSettings = { - 'discover:enableSql': true, + 'discover:enableESQL': true, }; async function setDiscoverTimeRange() { @@ -144,15 +144,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should visualize correctly text based language queries in Discover', async () => { - await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.discover.selectTextBaseLang(); await PageObjects.header.waitUntilLoadingHasFinished(); await monacoEditor.setCodeEditorValue( - 'SELECT extension, AVG("bytes") as average FROM "logstash-*" GROUP BY extension' + 'from logstash-* | stats averageB = avg(bytes) by extension' ); await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); - expect(await testSubjects.exists('heatmapChart')).to.be(true); + expect(await testSubjects.exists('xyVisChart')).to.be(true); await PageObjects.discover.chooseLensChart('Donut'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -161,10 +161,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should allow changing dimensions', async () => { await elasticChart.setNewChartUiDebugFlag(true); - await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.discover.selectTextBaseLang(); await PageObjects.header.waitUntilLoadingHasFinished(); await monacoEditor.setCodeEditorValue( - 'SELECT extension, AVG("bytes") as average FROM "logstash-*" GROUP BY extension' + 'from logstash-* | stats averageB = avg(bytes) by extension' ); await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -186,11 +186,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { assertMatchesExpectedData(data!); }); - it('should visualize correctly text based language queries in Lenss', async () => { - await PageObjects.discover.selectTextBaseLang('SQL'); + it('should visualize correctly text based language queries in Lens', async () => { + await PageObjects.discover.selectTextBaseLang(); await PageObjects.header.waitUntilLoadingHasFinished(); await monacoEditor.setCodeEditorValue( - 'SELECT extension, AVG("bytes") as average FROM "logstash-*" GROUP BY extension' + 'from logstash-* | stats averageB = avg(bytes) by extension' ); await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -201,15 +201,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await retry.waitFor('lens flyout', async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased'); - return dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'average'; + return dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'averageB'; }); }); it('should visualize correctly text based language queries based on index patterns', async () => { - await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.discover.selectTextBaseLang(); await PageObjects.header.waitUntilLoadingHasFinished(); await monacoEditor.setCodeEditorValue( - 'SELECT extension, AVG("bytes") as average FROM "logstash*" GROUP BY extension' + 'from logstash* | stats averageB = avg(bytes) by extension' ); await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -220,15 +220,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await retry.waitFor('lens flyout', async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased'); - return dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'average'; + return dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'averageB'; }); }); it('should save and edit chart in the dashboard on the fly', async () => { - await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.discover.selectTextBaseLang(); await PageObjects.header.waitUntilLoadingHasFinished(); await monacoEditor.setCodeEditorValue( - 'SELECT extension, AVG("bytes") as average FROM "logstash*" GROUP BY extension' + 'from logstash-* | stats averageB = avg(bytes) by extension' ); await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/discover_log_explorer/customization.ts b/x-pack/test/functional/apps/discover_log_explorer/customization.ts deleted file mode 100644 index 6cd713a40f63a..0000000000000 --- a/x-pack/test/functional/apps/discover_log_explorer/customization.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'navigationalSearch']); - const testSubjects = getService('testSubjects'); - - describe('Customizations', () => { - before('initialize tests', async () => { - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); - }); - - after('clean up archives', async () => { - await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); - }); - - describe('when Discover is loaded with the log-explorer profile', () => { - it('DatasetSelector should replace the DataViewPicker', async () => { - // Assert does not render on discover app - await PageObjects.common.navigateToApp('discover'); - await testSubjects.missingOrFail('datasetSelectorPopover'); - - // Assert it renders on log-explorer profile - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); - await testSubjects.existOrFail('datasetSelectorPopover'); - }); - - it('the TopNav bar should hide then New, Open and Save options', async () => { - // Assert does not render on discover app - await PageObjects.common.navigateToApp('discover'); - await testSubjects.existOrFail('discoverNewButton'); - await testSubjects.existOrFail('discoverOpenButton'); - await testSubjects.existOrFail('shareTopNavButton'); - await testSubjects.existOrFail('discoverAlertsButton'); - await testSubjects.existOrFail('openInspectorButton'); - await testSubjects.existOrFail('discoverSaveButton'); - - // Assert it renders on log-explorer profile - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); - await testSubjects.missingOrFail('discoverNewButton'); - await testSubjects.missingOrFail('discoverOpenButton'); - await testSubjects.existOrFail('shareTopNavButton'); - await testSubjects.existOrFail('discoverAlertsButton'); - await testSubjects.existOrFail('openInspectorButton'); - await testSubjects.missingOrFail('discoverSaveButton'); - }); - - it('should add a searchable deep link to the profile page', async () => { - await PageObjects.common.navigateToApp('home'); - await PageObjects.navigationalSearch.searchFor('discover log explorer'); - - const results = await PageObjects.navigationalSearch.getDisplayedResults(); - expect(results[0].label).to.eql('Discover / Logs Explorer'); - }); - - it('should render a filter controls section as part of the unified search bar', async () => { - // Assert does not render on discover app - await PageObjects.common.navigateToApp('discover'); - await testSubjects.missingOrFail('datasetFiltersCustomization'); - - // Assert it renders on log-explorer profile - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); - await testSubjects.existOrFail('datasetFiltersCustomization', { allowHidden: true }); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 8ea0ad09a50f0..3d24a4d134c2a 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -56,8 +56,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/164164 - describe.skip('with metrics present', () => { + describe('with metrics present', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await pageObjects.common.navigateToApp('infraOps'); diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index d46d94121961c..7ff494d4cb584 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -233,6 +233,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Processes Tab', () => { before(async () => { await pageObjects.assetDetails.clickProcessesTab(); + await pageObjects.header.waitUntilLoadingHasFinished(); }); it('should render processes tab and with Total Value summary', async () => { @@ -261,6 +262,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); await searchInput.clearValue(); }); + + it('shows an error message when typing invalid term into the search input', async () => { + const searchInput = await pageObjects.assetDetails.getProcessesSearchField(); + + await pageObjects.assetDetails.processesSearchInputErrorMissing(); + await searchInput.type(','); + await pageObjects.assetDetails.processesSearchInputErrorExists(); + }); }); describe('Logs Tab', () => { diff --git a/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts b/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts index d60d7b89a0121..fab04049c5b89 100644 --- a/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts +++ b/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts @@ -19,6 +19,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToUrl('integrations', 'browse', { useActualUrl: true, }); + const searchInput = await find.byCssSelector('[data-test-subj="epmList.searchBar"]'); + await searchInput.type('GeoJSON'); const geoFileCard = await find.byCssSelector( '[data-test-subj="integration-card:ui_link:ingest_geojson"]' ); diff --git a/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts index 67f3728eab9f9..4ad82d75346bd 100644 --- a/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts +++ b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts @@ -17,7 +17,9 @@ export default function ({ getService }: FtrProviderContext) { id: model.name, })); - describe('trained models', function () { + // Failing: See https://github.com/elastic/kibana/issues/165083 + // Failing: See https://github.com/elastic/kibana/issues/165084 + describe.skip('trained models', function () { // 'Created at' will be different on each run, // so we will just assert that the value is in the expected timestamp format. const builtInModelData = { diff --git a/x-pack/test/functional/apps/observability_log_explorer/app.ts b/x-pack/test/functional/apps/observability_log_explorer/app.ts new file mode 100644 index 0000000000000..26fb9b4e8d19e --- /dev/null +++ b/x-pack/test/functional/apps/observability_log_explorer/app.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'navigationalSearch', 'observabilityLogExplorer']); + const testSubjects = getService('testSubjects'); + + describe('Application', () => { + it('is shown in the global search', async () => { + await PageObjects.common.navigateToApp('home'); + await PageObjects.navigationalSearch.searchFor('log explorer'); + + const results = await PageObjects.navigationalSearch.getDisplayedResults(); + expect(results[0].label).to.eql('Log Explorer'); + }); + + it('is shown in the observability side navigation', async () => { + await PageObjects.observabilityLogExplorer.navigateTo(); + await testSubjects.existOrFail('observability-nav-observability-log-explorer-explorer'); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover_log_explorer/columns_selection.ts b/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts similarity index 69% rename from x-pack/test/functional/apps/discover_log_explorer/columns_selection.ts rename to x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts index c1a9aee81758a..c61a2586522fd 100644 --- a/x-pack/test/functional/apps/discover_log_explorer/columns_selection.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts @@ -5,6 +5,8 @@ * 2.0. */ import expect from '@kbn/expect'; +import rison from '@kbn/rison'; +import querystring from 'querystring'; import { FtrProviderContext } from '../../ftr_provider_context'; const defaultLogColumns = ['@timestamp', 'message']; @@ -12,24 +14,24 @@ const defaultLogColumns = ['@timestamp', 'message']; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'discover']); + const PageObjects = getPageObjects(['discover', 'observabilityLogExplorer']); describe('Columns selection initialization and update', () => { before(async () => { await esArchiver.load( - 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' ); }); after(async () => { await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' ); }); - describe('when the log explorer profile loads', () => { + describe('when the log explorer loads', () => { it("should initialize the table columns to logs' default selection", async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); @@ -39,8 +41,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should restore the table columns from the URL state if exists', async () => { - await PageObjects.common.navigateToApp('discover', { - hash: '/p/log-explorer?_a=(columns:!(message,data_stream.namespace))', + await PageObjects.observabilityLogExplorer.navigateTo({ + search: querystring.stringify({ + _a: rison.encode({ + columns: ['message', 'data_stream.namespace'], + }), + }), }); await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); diff --git a/x-pack/test/functional/apps/discover_log_explorer/config.ts b/x-pack/test/functional/apps/observability_log_explorer/config.ts similarity index 100% rename from x-pack/test/functional/apps/discover_log_explorer/config.ts rename to x-pack/test/functional/apps/observability_log_explorer/config.ts diff --git a/x-pack/test/functional/apps/discover_log_explorer/dataset_selection_state.ts b/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts similarity index 64% rename from x-pack/test/functional/apps/discover_log_explorer/dataset_selection_state.ts rename to x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts index c1c2b335358bc..c9bcade9dce5b 100644 --- a/x-pack/test/functional/apps/discover_log_explorer/dataset_selection_state.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts @@ -5,19 +5,21 @@ * 2.0. */ import expect from '@kbn/expect'; +import rison from '@kbn/rison'; +import querystring from 'querystring'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'discoverLogExplorer']); + const PageObjects = getPageObjects(['common', 'observabilityLogExplorer']); describe('DatasetSelection initialization and update', () => { describe('when the "index" query param does not exist', () => { it('should initialize the "All log datasets" selection', async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); const datasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(datasetSelectionTitle).to.be('All log datasets'); }); @@ -27,53 +29,58 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should decode and restore the selection from a valid encoded index', async () => { const azureActivitylogsIndex = 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; - await PageObjects.common.navigateToApp('discover', { - hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`, + await PageObjects.observabilityLogExplorer.navigateTo({ + search: querystring.stringify({ + _a: rison.encode({ index: azureActivitylogsIndex }), + }), }); const datasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(datasetSelectionTitle).to.be('[Azure Logs] activitylogs'); }); it('should fallback to the "All log datasets" selection and notify the user of an invalid encoded index', async () => { const invalidEncodedIndex = 'invalid-encoded-index'; - await PageObjects.common.navigateToApp('discover', { - hash: `/p/log-explorer?_a=(index:${encodeURIComponent(invalidEncodedIndex)})`, + await PageObjects.observabilityLogExplorer.navigateTo({ + search: querystring.stringify({ + _a: rison.encode({ index: invalidEncodedIndex }), + }), }); const datasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); - await PageObjects.discoverLogExplorer.assertRestoreFailureToastExist(); + await PageObjects.observabilityLogExplorer.assertRestoreFailureToastExist(); expect(datasetSelectionTitle).to.be('All log datasets'); }); }); describe('when navigating back and forth on the page history', () => { it('should decode and restore the selection for the current index', async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); const allDatasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(allDatasetSelectionTitle).to.be('All log datasets'); const azureActivitylogsIndex = 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; - await PageObjects.common.navigateToApp('discover', { - hash: `/p/log-explorer?_a=(index:${encodeURIComponent( - azureActivitylogsIndex - )})&controlPanels=()`, + await PageObjects.observabilityLogExplorer.navigateTo({ + search: querystring.stringify({ + _a: rison.encode({ index: azureActivitylogsIndex }), + controlPanels: rison.encode({}), + }), }); const azureDatasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(azureDatasetSelectionTitle).to.be('[Azure Logs] activitylogs'); // Go back to previous page selection await retry.try(async () => { await browser.goBack(); const backNavigationDatasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(backNavigationDatasetSelectionTitle).to.be('All log datasets'); }); @@ -81,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { await browser.goForward(); const forwardNavigationDatasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(forwardNavigationDatasetSelectionTitle).to.be('[Azure Logs] activitylogs'); }); }); diff --git a/x-pack/test/functional/apps/discover_log_explorer/dataset_selector.ts b/x-pack/test/functional/apps/observability_log_explorer/dataset_selector.ts similarity index 61% rename from x-pack/test/functional/apps/discover_log_explorer/dataset_selector.ts rename to x-pack/test/functional/apps/observability_log_explorer/dataset_selector.ts index b456da8bddb2a..f7a5595634895 100644 --- a/x-pack/test/functional/apps/discover_log_explorer/dataset_selector.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/dataset_selector.ts @@ -20,28 +20,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'discoverLogExplorer']); + const PageObjects = getPageObjects(['common', 'observabilityLogExplorer']); describe('Dataset Selector', () => { before(async () => { - await PageObjects.discoverLogExplorer.removeInstalledPackages(); + await PageObjects.observabilityLogExplorer.removeInstalledPackages(); }); describe('without installed integrations or uncategorized data streams', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); describe('when open on the first navigation level', () => { it('should always display the "All log datasets" entry as the first item', async () => { const allLogDatasetButton = - await PageObjects.discoverLogExplorer.getAllLogDatasetsButton(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getAllLogDatasetsButton(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); const allLogDatasetTitle = await allLogDatasetButton.getVisibleText(); const firstEntryTitle = await menuEntries[0].getVisibleText(); @@ -52,8 +52,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should always display the unmanaged datasets entry as the second item', async () => { const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); const unmanagedDatasetTitle = await unamanagedDatasetButton.getVisibleText(); const secondEntryTitle = await menuEntries[1].getVisibleText(); @@ -66,15 +66,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Skip the test in case network condition utils are not available try { await retry.try(async () => { - await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoIntegrationsPromptExists(); }); await PageObjects.common.sleep(5000); await browser.setNetworkConditions('OFFLINE'); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('a'); await retry.try(async () => { - await PageObjects.discoverLogExplorer.assertNoIntegrationsErrorExists(); + await PageObjects.observabilityLogExplorer.assertNoIntegrationsErrorExists(); }); await browser.restoreNetworkConditions(); @@ -84,10 +84,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should display an empty prompt for no integrations', async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations.length).to.be(0); - await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoIntegrationsPromptExists(); }); }); @@ -97,10 +97,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { try { await browser.setNetworkConditions('SLOW_3G'); // Almost stuck network conditions const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await unamanagedDatasetButton.click(); - await PageObjects.discoverLogExplorer.assertLoadingSkeletonExists(); + await PageObjects.observabilityLogExplorer.assertLoadingSkeletonExists(); await browser.restoreNetworkConditions(); } catch (error) { @@ -112,18 +112,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Skip the test in case network condition utils are not available try { const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await unamanagedDatasetButton.click(); await retry.try(async () => { - await PageObjects.discoverLogExplorer.assertNoDataStreamsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoDataStreamsPromptExists(); }); await browser.setNetworkConditions('OFFLINE'); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('a'); await retry.try(async () => { - await PageObjects.discoverLogExplorer.assertNoDataStreamsErrorExists(); + await PageObjects.observabilityLogExplorer.assertNoDataStreamsErrorExists(); }); await browser.restoreNetworkConditions(); @@ -134,15 +134,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should display an empty prompt for no data streams', async () => { const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await unamanagedDatasetButton.click(); const unamanagedDatasetEntries = - await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(unamanagedDatasetEntries.length).to.be(0); - await PageObjects.discoverLogExplorer.assertNoDataStreamsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoDataStreamsPromptExists(); }); }); }); @@ -152,32 +152,33 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await esArchiver.load( - 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' ); - cleanupIntegrationsSetup = await PageObjects.discoverLogExplorer.setupInitialIntegrations(); + cleanupIntegrationsSetup = + await PageObjects.observabilityLogExplorer.setupInitialIntegrations(); }); after(async () => { await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' ); await cleanupIntegrationsSetup(); }); describe('when open on the first navigation level', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); it('should always display the "All log datasets" entry as the first item', async () => { const allLogDatasetButton = - await PageObjects.discoverLogExplorer.getAllLogDatasetsButton(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getAllLogDatasetsButton(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); const allLogDatasetTitle = await allLogDatasetButton.getVisibleText(); const firstEntryTitle = await menuEntries[0].getVisibleText(); @@ -188,8 +189,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should always display the unmanaged datasets entry as the second item', async () => { const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); const unmanagedDatasetTitle = await unamanagedDatasetButton.getVisibleText(); const secondEntryTitle = await menuEntries[1].getVisibleText(); @@ -199,7 +200,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should display a list of installed integrations', async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations.length).to.be(3); expect(integrations).to.eql(initialPackagesTexts); @@ -207,82 +208,82 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should sort the integrations list by the clicked sorting option', async () => { // Test ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts); }); // Test descending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('desc'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts.slice().reverse()); }); // Test back ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts); }); }); it('should filter the integrations list by the typed integration name', async () => { - await PageObjects.discoverLogExplorer.typeSearchFieldWith('system'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('system'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.system]); }); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('a'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.apache, initialPackageMap.aws]); }); }); it('should display an empty prompt when the search does not match any result', async () => { - await PageObjects.discoverLogExplorer.typeSearchFieldWith('no result search text'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('no result search text'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations.length).to.be(0); }); - await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoIntegrationsPromptExists(); }); it('should load more integrations by scrolling to the end of the list', async () => { // Install more integrations and reload the page const cleanupAdditionalSetup = - await PageObjects.discoverLogExplorer.setupAdditionalIntegrations(); + await PageObjects.observabilityLogExplorer.setupAdditionalIntegrations(); await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); // Initially fetched integrations await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(nodes.length).to.be(15); - await nodes.at(-1)?.scrollIntoViewIfNecessary(); + await nodes.at(-1)?.scrollIntoView(); }); // Load more integrations await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(nodes.length).to.be(20); - await nodes.at(-1)?.scrollIntoViewIfNecessary(); + await nodes.at(-1)?.scrollIntoView(); }); // No other integrations to load after scrolling to last integration await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(nodes.length).to.be(20); }); @@ -292,24 +293,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('when clicking on integration and moving into the second navigation level', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); it('should display a list of available datasets', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); expect(await menuEntries[0].getVisibleText()).to.be('access'); @@ -319,39 +320,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should sort the datasets list by the clicked sorting option', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); }); // Test ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); }); // Test descending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('desc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('error'); expect(await menuEntries[1].getVisibleText()).to.be('access'); }); // Test back ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); @@ -360,28 +361,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should filter the datasets list by the typed dataset name', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); }); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('err'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('err'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(menuEntries.length).to.be(1); expect(await menuEntries[0].getVisibleText()).to.be('error'); @@ -390,26 +391,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should update the current selection with the clicked dataset', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('access'); menuEntries[0].click(); }); await retry.try(async () => { - const selectorButton = await PageObjects.discoverLogExplorer.getDatasetSelectorButton(); + const selectorButton = + await PageObjects.observabilityLogExplorer.getDatasetSelectorButton(); expect(await selectorButton.getVisibleText()).to.be('[Apache HTTP Server] access'); }); @@ -418,22 +420,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('when navigating into Uncategorized data streams', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); it('should display a list of available datasets', async () => { - const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const button = await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await button.click(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); @@ -443,20 +445,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should sort the datasets list by the clicked sorting option', async () => { - const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const button = await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await button.click(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); }); // Test ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); @@ -464,9 +466,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // Test descending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('desc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[2]); expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); @@ -474,9 +476,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // Test back ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); @@ -485,28 +487,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should filter the datasets list by the typed dataset name', async () => { - const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const button = await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await button.click(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); }); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('retail'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('retail'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(menuEntries.length).to.be(1); expect(await menuEntries[0].getVisibleText()).to.be('logs-retail-*'); @@ -514,25 +516,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should update the current selection with the clicked dataset', async () => { - const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const button = await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await button.click(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('logs-gaming-*'); menuEntries[0].click(); }); await retry.try(async () => { - const selectorButton = await PageObjects.discoverLogExplorer.getDatasetSelectorButton(); + const selectorButton = + await PageObjects.observabilityLogExplorer.getDatasetSelectorButton(); expect(await selectorButton.getVisibleText()).to.be('logs-gaming-*'); }); @@ -541,37 +544,37 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('when open/close the selector', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); it('should restore the latest navigation panel', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); }); - await PageObjects.discoverLogExplorer.closeDatasetSelector(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.closeDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); expect(await menuEntries[0].getVisibleText()).to.be('access'); @@ -580,18 +583,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should restore the latest search results', async () => { - await PageObjects.discoverLogExplorer.typeSearchFieldWith('system'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('system'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.system]); }); - await PageObjects.discoverLogExplorer.closeDatasetSelector(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.closeDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.system]); }); }); @@ -599,35 +602,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('when switching between integration panels', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); it('should remember the latest search and restore its results for each integration', async () => { - await PageObjects.discoverLogExplorer.openDatasetSelector(); - await PageObjects.discoverLogExplorer.clearSearchField(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.clearSearchField(); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('apache'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('apache'); await retry.try(async () => { - const { nodes, integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes, integrations } = + await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.apache]); nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); }); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('err'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('err'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(menuEntries.length).to.be(1); expect(await menuEntries[0].getVisibleText()).to.be('error'); @@ -635,23 +639,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Navigate back to integrations const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); panelTitleNode.click(); await retry.try(async () => { - const { nodes, integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes, integrations } = + await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.apache]); - const searchValue = await PageObjects.discoverLogExplorer.getSearchFieldValue(); + const searchValue = await PageObjects.observabilityLogExplorer.getSearchFieldValue(); expect(searchValue).to.eql('apache'); nodes[0].click(); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); - const searchValue = await PageObjects.discoverLogExplorer.getSearchFieldValue(); + const searchValue = await PageObjects.observabilityLogExplorer.getSearchFieldValue(); expect(searchValue).to.eql('err'); expect(menuEntries.length).to.be(1); diff --git a/x-pack/test/functional/apps/observability_log_explorer/filter_controls.ts b/x-pack/test/functional/apps/observability_log_explorer/filter_controls.ts new file mode 100644 index 0000000000000..db04f6251d9bc --- /dev/null +++ b/x-pack/test/functional/apps/observability_log_explorer/filter_controls.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['observabilityLogExplorer']); + const testSubjects = getService('testSubjects'); + + describe('Filter controls customization', () => { + before('initialize tests', async () => { + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + }); + + after('clean up archives', async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + + it('renders a filter controls section as part of the unified search bar', async () => { + await PageObjects.observabilityLogExplorer.navigateTo(); + await testSubjects.existOrFail('datasetFiltersCustomization', { allowHidden: true }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover_log_explorer/index.ts b/x-pack/test/functional/apps/observability_log_explorer/index.ts similarity index 78% rename from x-pack/test/functional/apps/discover_log_explorer/index.ts rename to x-pack/test/functional/apps/observability_log_explorer/index.ts index dd8b99db79ad0..90a52663e34ce 100644 --- a/x-pack/test/functional/apps/discover_log_explorer/index.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/index.ts @@ -8,10 +8,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('Discover Log-Explorer profile', function () { + describe('Observability Log Explorer', function () { + loadTestFile(require.resolve('./app')); loadTestFile(require.resolve('./columns_selection')); - loadTestFile(require.resolve('./customization')); loadTestFile(require.resolve('./dataset_selection_state')); loadTestFile(require.resolve('./dataset_selector')); + loadTestFile(require.resolve('./filter_controls')); }); } diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index 36dc0c6d84c3c..5bb94fd5a8897 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -168,6 +168,9 @@ export default async function ({ readConfigFile }) { observability: { pathname: '/app/observability', }, + observabilityLogExplorer: { + pathname: '/app/observability-log-explorer', + }, connectors: { pathname: '/app/management/insightsAndAlerting/triggersActionsConnectors/', }, diff --git a/x-pack/test/functional/es_archives/discover_log_explorer/data_streams/data.json.gz b/x-pack/test/functional/es_archives/observability_log_explorer/data_streams/data.json.gz similarity index 100% rename from x-pack/test/functional/es_archives/discover_log_explorer/data_streams/data.json.gz rename to x-pack/test/functional/es_archives/observability_log_explorer/data_streams/data.json.gz diff --git a/x-pack/test/functional/es_archives/discover_log_explorer/data_streams/mappings.json b/x-pack/test/functional/es_archives/observability_log_explorer/data_streams/mappings.json similarity index 100% rename from x-pack/test/functional/es_archives/discover_log_explorer/data_streams/mappings.json rename to x-pack/test/functional/es_archives/observability_log_explorer/data_streams/mappings.json diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts index c7600d6f1b5f4..98448ab473b94 100644 --- a/x-pack/test/functional/page_objects/asset_details.ts +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -136,6 +136,14 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return await testSubjects.find('infraAssetDetailsProcessesSearchBarInput'); }, + async processesSearchInputErrorMissing() { + return await testSubjects.missingOrFail('infraAssetDetailsProcessesSearchInputError'); + }, + + async processesSearchInputErrorExists() { + return await testSubjects.existOrFail('infraAssetDetailsProcessesSearchInputError'); + }, + // Logs async clickLogsTab() { return testSubjects.click('infraAssetDetailsLogsTab'); diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index dc7d2f9ee9a13..a8ac0895ed255 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -15,7 +15,6 @@ import { CanvasPageProvider } from './canvas_page'; import { CopySavedObjectsToSpacePageProvider } from './copy_saved_objects_to_space_page'; import { CrossClusterReplicationPageProvider } from './cross_cluster_replication_page'; import { DetectionsPageObject } from '../../security_solution_ftr/page_objects/detections'; -import { DiscoverLogExplorerPageObject } from './discover_log_explorer'; import { GeoFileUploadPageObject } from './geo_file_upload'; import { GisPageObject } from './gis_page'; import { GraphPageObject } from './graph_page'; @@ -34,6 +33,7 @@ import { LogstashPageObject } from './logstash_page'; import { MaintenanceWindowsPageProvider } from './maintenance_windows_page'; import { MonitoringPageObject } from './monitoring_page'; import { NavigationalSearchPageObject } from './navigational_search'; +import { ObservabilityLogExplorerPageObject } from './observability_log_explorer'; import { ObservabilityPageProvider } from './observability_page'; import { RemoteClustersPageProvider } from './remote_clusters_page'; import { ReportingPageObject } from './reporting_page'; @@ -62,7 +62,6 @@ export const pageObjects = { copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider, crossClusterReplication: CrossClusterReplicationPageProvider, detections: DetectionsPageObject, - discoverLogExplorer: DiscoverLogExplorerPageObject, geoFileUpload: GeoFileUploadPageObject, graph: GraphPageObject, grokDebugger: GrokDebuggerPageObject, @@ -81,6 +80,7 @@ export const pageObjects = { maps: GisPageObject, monitoring: MonitoringPageObject, navigationalSearch: NavigationalSearchPageObject, + observabilityLogExplorer: ObservabilityLogExplorerPageObject, observability: ObservabilityPageProvider, remoteClusters: RemoteClustersPageProvider, reporting: ReportingPageObject, diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index 34bcec45e7aa6..b4e8839ba89e2 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -43,7 +43,7 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide throw new Error(); } }); - return await testSubjects.find('waffleMap'); + return testSubjects.find('waffleMap'); }, async getWaffleMapTooltips() { @@ -84,7 +84,7 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide const color = await nodeValue.getAttribute('color'); return { name, value: parseFloat(value), color }; }); - return await Promise.all(promises); + return Promise.all(promises); }, async sortNodesBy(sort: string) { @@ -171,34 +171,31 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide return timelineSelectorsVisible.every((visible) => !visible); }, - async openInventorySwitcher() { - await testSubjects.click('openInventorySwitcher'); - return await testSubjects.find('goToHost1'); - }, - async toggleInventorySwitcher() { await testSubjects.click('openInventorySwitcher'); await testSubjects.find('goToHost'); await testSubjects.click('openInventorySwitcher'); - return await testSubjects.missingOrFail('goToHost'); + retry.tryForTime(2 * 1000, async () => { + return testSubjects.missingOrFail('goToHost'); + }); }, async goToHost() { await testSubjects.click('openInventorySwitcher'); await testSubjects.find('goToHost'); - return await testSubjects.click('goToHost'); + return testSubjects.click('goToHost'); }, async goToPods() { await testSubjects.click('openInventorySwitcher'); await testSubjects.find('goToHost'); - return await testSubjects.click('goToPods'); + return testSubjects.click('goToPods'); }, async goToDocker() { await testSubjects.click('openInventorySwitcher'); await testSubjects.find('goToHost'); - return await testSubjects.click('goToDocker'); + return testSubjects.click('goToDocker'); }, async goToSettings() { @@ -238,23 +235,23 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide }, async getSaveViewButton() { - return await testSubjects.find('openSaveViewModal'); + return testSubjects.find('openSaveViewModal'); }, async getLoadViewsButton() { - return await testSubjects.find('loadViews'); + return testSubjects.find('loadViews'); }, async openSaveViewsFlyout() { - return await testSubjects.click('loadViews'); + return testSubjects.click('loadViews'); }, async closeSavedViewFlyout() { - return await testSubjects.click('cancelSavedViewModal'); + return testSubjects.click('cancelSavedViewModal'); }, async openCreateSaveViewModal() { - return await testSubjects.click('openSaveViewModal'); + return testSubjects.click('openSaveViewModal'); }, async openEnterViewNameAndSave() { @@ -263,23 +260,23 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide }, async getNoMetricsIndicesPrompt() { - return await testSubjects.find('noDataPage'); + return testSubjects.find('noDataPage'); }, async getNoMetricsDataPrompt() { - return await testSubjects.find('noMetricsDataPrompt'); + return testSubjects.find('noMetricsDataPrompt'); }, async getNoRemoteClusterPrompt() { - return await testSubjects.find('infraHostsNoRemoteCluster'); + return testSubjects.find('infraHostsNoRemoteCluster'); }, async getInfraMissingMetricsIndicesCallout() { - return await testSubjects.find('infraIndicesPanelSettingsWarningCallout'); + return testSubjects.find('infraIndicesPanelSettingsWarningCallout'); }, async getInfraMissingRemoteClusterIndicesCallout() { - return await testSubjects.find('infraIndicesPanelSettingsDangerCallout'); + return testSubjects.find('infraIndicesPanelSettingsDangerCallout'); }, async openSourceConfigurationFlyout() { @@ -436,7 +433,7 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide }, async clickDismissKubernetesTourButton() { - return await testSubjects.click('infra-kubernetesTour-dismiss'); + return testSubjects.click('infra-kubernetesTour-dismiss'); }, }; } diff --git a/x-pack/test/functional/page_objects/discover_log_explorer.ts b/x-pack/test/functional/page_objects/observability_log_explorer.ts similarity index 95% rename from x-pack/test/functional/page_objects/discover_log_explorer.ts rename to x-pack/test/functional/page_objects/observability_log_explorer.ts index 282a703863dc2..33e85e06a16a9 100644 --- a/x-pack/test/functional/page_objects/discover_log_explorer.ts +++ b/x-pack/test/functional/page_objects/observability_log_explorer.ts @@ -98,12 +98,18 @@ const packages: IntegrationPackage[] = [ const initialPackages = packages.slice(0, 3); const additionalPackages = packages.slice(3); -export function DiscoverLogExplorerPageObject({ getService }: FtrProviderContext) { +export function ObservabilityLogExplorerPageObject({ + getPageObjects, + getService, +}: FtrProviderContext) { + const PageObjects = getPageObjects(['common']); const log = getService('log'); const supertest = getService('supertest'); const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); + type NavigateToAppOptions = Parameters[1]; + return { uninstallPackage: ({ name, version }: IntegrationPackage) => { return supertest.delete(`/api/fleet/epm/packages/${name}/${version}`).set('kbn-xsrf', 'xxxx'); @@ -165,6 +171,10 @@ export function DiscoverLogExplorerPageObject({ getService }: FtrProviderContext }; }, + async navigateTo(options?: NavigateToAppOptions) { + return await PageObjects.common.navigateToApp('observabilityLogExplorer', options); + }, + getDatasetSelector() { return testSubjects.find('datasetSelectorPopover'); }, diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index bf5f6aed44789..a256c623adbe0 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test'; import { JobType } from '@kbn/ml-plugin/common/types/saved_objects'; +import { API_VERSIONS } from '@kbn/fleet-plugin/common/constants'; import { savedSearches, dashboards } from './test_resources_data'; import { getCommonRequestHeader } from './common_api'; import { MlApi } from './api'; @@ -567,7 +568,7 @@ export function MachineLearningTestResourcesProvider( await retry.tryForTime(2 * 60 * 1000, async () => { const { body, status } = await supertest .post(`/api/fleet/setup`) - .set(getCommonRequestHeader('1')); + .set(getCommonRequestHeader(`${API_VERSIONS.public.v1}`)); mlApi.assertResponseStatusCode(200, status, body); }); log.debug(` > Setup done`); @@ -581,7 +582,7 @@ export function MachineLearningTestResourcesProvider( await retry.tryForTime(30 * 1000, async () => { const { body, status } = await supertest .post(`/api/fleet/epm/packages/${packageName}/${version}`) - .set(getCommonRequestHeader('1')); + .set(getCommonRequestHeader(`${API_VERSIONS.public.v1}`)); mlApi.assertResponseStatusCode(200, status, body); }); @@ -595,7 +596,7 @@ export function MachineLearningTestResourcesProvider( await retry.tryForTime(30 * 1000, async () => { const { body, status } = await supertest .delete(`/api/fleet/epm/packages/${packageName}/${version}`) - .set(getCommonRequestHeader('1')); + .set(getCommonRequestHeader(`${API_VERSIONS.public.v1}`)); mlApi.assertResponseStatusCode(200, status, body); }); @@ -609,7 +610,7 @@ export function MachineLearningTestResourcesProvider( await retry.tryForTime(10 * 1000, async () => { const { body, status } = await supertest .get(`/api/fleet/epm/packages?experimental=true`) - .set(getCommonRequestHeader('1')); + .set(getCommonRequestHeader(`${API_VERSIONS.public.v1}`)); mlApi.assertResponseStatusCode(200, status, body); packageVersion = diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 5986de63a2d74..57403ef8c3ab3 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -57,6 +57,15 @@ export function ObservabilityAlertsCommonProvider({ ); }; + const navigateToRulesLogsPage = async () => { + return await pageObjects.common.navigateToUrlWithBrowserHistory( + 'observability', + '/alerts/rules/logs', + '', + { ensureCurrentUrl: false } + ); + }; + const navigateToAlertDetails = async (alertId: string) => { return await pageObjects.common.navigateToUrlWithBrowserHistory( 'observability', @@ -338,6 +347,7 @@ export function ObservabilityAlertsCommonProvider({ getAlertsFlyoutViewRuleDetailsLinkOrFail, getRuleStatValue, navigateToRulesPage, + navigateToRulesLogsPage, navigateToRuleDetailsByRuleId, navigateToAlertDetails, }; diff --git a/x-pack/test/functional/services/observability/alerts/rules_page.ts b/x-pack/test/functional/services/observability/alerts/rules_page.ts index ff8e21c943725..226d257ed918b 100644 --- a/x-pack/test/functional/services/observability/alerts/rules_page.ts +++ b/x-pack/test/functional/services/observability/alerts/rules_page.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export function ObservabilityAlertsRulesProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const find = getService('find'); const getManageRulesPageHref = async () => { const manageRulesPageButton = await testSubjects.find('manageRulesPageButton'); @@ -23,10 +24,18 @@ export function ObservabilityAlertsRulesProvider({ getService }: FtrProviderCont const clickDisableFromDropDownMenu = async () => testSubjects.click('statusDropdownDisabledItem'); + const clickLogsTab = async () => testSubjects.click('ruleLogsTab'); + + const clickOnRuleInEventLogs = async () => { + await find.clickByButtonText('metric-threshold'); + }; + return { getManageRulesPageHref, clickCreateRuleButton, clickRuleStatusDropDownMenu, clickDisableFromDropDownMenu, + clickLogsTab, + clickOnRuleInEventLogs, }; } diff --git a/x-pack/test/functional_execution_context/test_utils.ts b/x-pack/test/functional_execution_context/test_utils.ts index 65d0d7ece02c4..75215a5f42110 100644 --- a/x-pack/test/functional_execution_context/test_utils.ts +++ b/x-pack/test/functional_execution_context/test_utils.ts @@ -4,18 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import Fs from 'fs/promises'; import Path from 'path'; +import JSON5 from 'json5'; +import Fs from 'fs/promises'; import { isEqualWith } from 'lodash'; import type { Ecs, KibanaExecutionContext } from '@kbn/core/server'; -import type { RetryService } from '@kbn/ftr-common-functional-services'; -import { concatMap, defer, filter, firstValueFrom, ReplaySubject, scan, timeout } from 'rxjs'; export const logFilePath = Path.resolve(__dirname, './kibana.log'); export const ANY = Symbol('any'); -let logstream$: ReplaySubject | undefined; - export function getExecutionContextFromLogRecord(record: Ecs | undefined): KibanaExecutionContext { if (record?.log?.logger !== 'execution_context' || !record?.message) { throw new Error(`The record is not an entry of execution context`); @@ -37,95 +34,41 @@ export function isExecutionContextLog( } } -// to avoid splitting log record containing \n symbol -const endOfLine = /(?<=})\s*\n/; -export async function assertLogContains({ - description, +/** + * Checks the provided log records against the provided predicate + */ +export function assertLogContains({ + logs, predicate, - retry, + description, }: { - description: string; + logs: Ecs[]; predicate: (record: Ecs) => boolean; - retry: RetryService; -}): Promise { - // logs are written to disk asynchronously. I sacrificed performance to reduce flakiness. - await retry.waitFor(description, async () => { - if (!logstream$) { - logstream$ = getLogstream$(); - } - try { - await firstValueFrom(logstream$.pipe(filter(predicate), timeout(5_000))); - return true; - } catch (err) { - return false; - } - }); + description: string; +}) { + if (!logs.some(predicate)) { + throw new Error(`Unable to find log entries: ${description}`); + } } /** - * Creates an observable that continuously tails the log file. + * Reads the log file and parses the JSON objects that it contains. */ -function getLogstream$(): ReplaySubject { - const stream$ = new ReplaySubject(); - - defer(async function* () { - const fd = await Fs.open(logFilePath, 'rs'); - while (!stream$.isStopped) { - const { bytesRead, buffer } = await fd.read(); - if (bytesRead) { - yield buffer.toString('utf8', 0, bytesRead); - } - } - await fd.close(); - }) - .pipe( - scan( - ({ buffer }, chunk) => { - const logString = buffer.concat(chunk); - const lines = logString.split(endOfLine); - const lastLine = lines.pop(); - const records = lines.map((s) => JSON.parse(s)); - - let leftover = ''; - if (lastLine) { - try { - const validRecord = JSON.parse(lastLine); - records.push(validRecord); - } catch (err) { - leftover = lastLine; - } - } - - return { buffer: leftover, records }; - }, - { - records: [], // The ECS entries in the logs - buffer: '', // Accumulated leftovers from the previous operation - } - ), - concatMap(({ records }) => records) - ) - .subscribe(stream$); - - // let the content start flowing - stream$.subscribe(); - - return stream$; -} - -export function closeLogstream() { - logstream$?.complete(); - logstream$ = undefined; +export async function readLogFile(): Promise { + await forceSyncLogFile(); + const logFileContent = await Fs.readFile(logFilePath, 'utf-8'); + return logFileContent + .split('\n') + .filter(Boolean) + .map((str) => JSON5.parse(str)); } /** * Truncates the log file to avoid tests looking at the logs from previous executions. */ export async function clearLogFile() { - closeLogstream(); await Fs.writeFile(logFilePath, '', 'utf8'); await forceSyncLogFile(); - logstream$ = getLogstream$(); } /** diff --git a/x-pack/test/functional_execution_context/tests/browser.ts b/x-pack/test/functional_execution_context/tests/browser.ts index a67a1e64cea67..c168bf1783d55 100644 --- a/x-pack/test/functional_execution_context/tests/browser.ts +++ b/x-pack/test/functional_execution_context/tests/browser.ts @@ -7,14 +7,12 @@ import type { Ecs, KibanaExecutionContext } from '@kbn/core/server'; import type { FtrProviderContext } from '../ftr_provider_context'; -import { assertLogContains, forceSyncLogFile, isExecutionContextLog } from '../test_utils'; +import { assertLogContains, isExecutionContextLog, readLogFile } from '../test_utils'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home', 'timePicker']); - const retry = getService('retry'); - // Failing: See https://github.com/elastic/kibana/issues/149611 - describe.skip('Browser apps', () => { + describe('Browser apps', () => { before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, @@ -32,11 +30,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('discover app', () => { + let logs: Ecs[]; + before(async () => { await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setCommonlyUsedTime('Last_7 days'); await PageObjects.header.waitUntilLoadingHasFinished(); - await forceSyncLogFile(); + logs = await readLogFile(); }); function checkExecutionContextEntry(expectedExecutionContext: KibanaExecutionContext) { @@ -56,7 +56,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', predicate: (record) => Boolean(record.http?.request?.id?.includes('kibana:application:discover')), - retry, + logs, }); }); @@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { description: 'fetch documents', }, }), - retry, + logs, }); }); @@ -104,20 +104,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); }); describe('dashboard app', () => { + let logs: Ecs[]; + before(async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); await PageObjects.timePicker.setCommonlyUsedTime('Last_7 days'); await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.header.waitUntilLoadingHasFinished(); - await forceSyncLogFile(); + logs = await readLogFile(); }); function checkHttpRequestId(suffix: string) { @@ -172,7 +174,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { predicate: checkHttpRequestId( 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' ), - retry, + logs, }); }); @@ -199,19 +201,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); - describe('lnsMetric', () => { + describe.skip('lnsMetric', () => { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await assertLogContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', predicate: checkHttpRequestId( 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsLegacyMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' ), - retry, + logs, }); }); @@ -238,7 +240,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); @@ -250,7 +252,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { predicate: checkHttpRequestId( 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' ), - retry, + logs, }); }); @@ -277,7 +279,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); @@ -289,7 +291,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { predicate: checkHttpRequestId( 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' ), - retry, + logs, }); }); @@ -316,7 +318,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); @@ -329,7 +331,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { predicate: checkHttpRequestId( 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' ), - retry, + logs, }); }); @@ -356,17 +358,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); - describe('propagates context for TSVB visualizations', () => { + describe.skip('propagates context for TSVB visualizations', () => { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await assertLogContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', predicate: checkHttpRequestId('agg_based:metrics:bcb63b50-4c89-11e8-b3d7-01146121b73d'), - retry, + logs, }); }); @@ -387,7 +389,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { url: '/app/visualize#/edit/bcb63b50-4c89-11e8-b3d7-01146121b73d', }, }), - retry, + logs, }); }); }); @@ -399,7 +401,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { predicate: checkHttpRequestId( 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;agg_based:vega:ed78a660-53a0-11e8-acbd-0be0ad9d822b' ), - retry, + logs, }); }); @@ -426,19 +428,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); - describe('propagates context for Tag Cloud visualization', () => { + describe.skip('propagates context for Tag Cloud visualization', () => { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await assertLogContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', predicate: checkHttpRequestId( 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;agg_based:tagcloud:293b5a30-4c8f-11e8-b3d7-01146121b73d' ), - retry, + logs, }); }); @@ -465,19 +467,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); - describe('propagates context for Vertical bar visualization', () => { + describe.skip('propagates context for Vertical bar visualization', () => { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await assertLogContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', predicate: checkHttpRequestId( 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;agg_based:histogram:9886b410-4c8b-11e8-b3d7-01146121b73d' ), - retry, + logs, }); }); @@ -504,7 +506,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }, }), - retry, + logs, }); }); }); diff --git a/x-pack/test/functional_execution_context/tests/index.ts b/x-pack/test/functional_execution_context/tests/index.ts index ac83d4246bb8c..b50d2411a694c 100644 --- a/x-pack/test/functional_execution_context/tests/index.ts +++ b/x-pack/test/functional_execution_context/tests/index.ts @@ -6,7 +6,7 @@ */ import { FtrProviderContext } from '../ftr_provider_context'; -import { clearLogFile, closeLogstream } from '../test_utils'; +import { clearLogFile } from '../test_utils'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Execution context', function () { @@ -15,9 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { // If any of the tests rely on logs generating during bootstrap, we might need to change this. await clearLogFile(); }); - after(() => { - closeLogstream(); - }); loadTestFile(require.resolve('./browser')); loadTestFile(require.resolve('./server')); diff --git a/x-pack/test/functional_execution_context/tests/log_correlation.ts b/x-pack/test/functional_execution_context/tests/log_correlation.ts index 35585f092bbcf..2dade69e6f1ee 100644 --- a/x-pack/test/functional_execution_context/tests/log_correlation.ts +++ b/x-pack/test/functional_execution_context/tests/log_correlation.ts @@ -6,10 +6,9 @@ */ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../ftr_provider_context'; -import { assertLogContains, forceSyncLogFile } from '../test_utils'; +import { readLogFile, assertLogContains } from '../test_utils'; export default function ({ getService }: FtrProviderContext) { - const retry = getService('retry'); const supertest = getService('supertest'); describe('Log Correlation', () => { @@ -24,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response2.body.traceId).not.to.be(response1.body.traceId); - await forceSyncLogFile(); + const logs = await readLogFile(); let responseTraceId: string | undefined; await assertLogContains({ @@ -41,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { } return false; }, - retry, + logs, }); expect(responseTraceId).to.be.a('string'); @@ -56,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) { record.message?.includes('HEAD /') ), - retry, + logs, }); }); }); diff --git a/x-pack/test/functional_execution_context/tests/server.ts b/x-pack/test/functional_execution_context/tests/server.ts index 64035a0077966..9f8f3a9dfa3f7 100644 --- a/x-pack/test/functional_execution_context/tests/server.ts +++ b/x-pack/test/functional_execution_context/tests/server.ts @@ -11,14 +11,13 @@ import { } from '@kbn/core-http-common'; import expect from '@kbn/expect'; import type { FtrProviderContext } from '../ftr_provider_context'; -import { assertLogContains, isExecutionContextLog, ANY } from '../test_utils'; +import { readLogFile, assertLogContains, isExecutionContextLog, ANY } from '../test_utils'; function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } export default function ({ getService }: FtrProviderContext) { - const retry = getService('retry'); const supertest = getService('supertest'); const log = getService('log'); @@ -69,6 +68,7 @@ export default function ({ getService }: FtrProviderContext) { const alertId = createdAlert.id; await waitForStatus(alertId, new Set(['ok']), 90_000); + const logs = await readLogFile(); await assertLogContains({ description: @@ -80,7 +80,7 @@ export default function ({ getService }: FtrProviderContext) { `kibana:task%20manager:run%20alerting%3Atest.executionContext:` ) ), - retry, + logs, }); await assertLogContains({ @@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) { Boolean( record.http?.request?.id?.includes(`alert:execute%20test.executionContext:${alertId}`) ), - retry, + logs, }); await assertLogContains({ @@ -109,7 +109,7 @@ export default function ({ getService }: FtrProviderContext) { description: 'execute [test.executionContext] with name [abc] in [default] namespace', }, }), - retry, + logs, }); }); @@ -122,6 +122,8 @@ export default function ({ getService }: FtrProviderContext) { .send({ unencrypted: false }) .expect(200); + const logs = await readLogFile(); + await assertLogContains({ description: 'usage_collection execution context propagates to Elasticsearch via "x-opaque-id" header', @@ -132,7 +134,7 @@ export default function ({ getService }: FtrProviderContext) { `kibana:usage_collection:collector.fetch:application_usage` ) ), - retry, + logs, }); await assertLogContains({ @@ -144,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) { id: 'application_usage', description: 'Fetch method in the Collector "application_usage"', }), - retry, + logs, }); }); }); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_lists_by_size.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_lists_by_size.ts index 9452d95e7f972..0d348f5c63424 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_lists_by_size.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_lists_by_size.ts @@ -37,6 +37,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .get(`${INTERNAL_FIND_LISTS_BY_SIZE}`) .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') .send() .expect(200); @@ -65,6 +66,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .get(`${INTERNAL_FIND_LISTS_BY_SIZE}`) .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') .send() .expect(200); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/get_exception_filter.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/get_exception_filter.ts index a98f704d8a3d3..913ce14db1f00 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/get_exception_filter.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/get_exception_filter.ts @@ -41,6 +41,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .post(`${INTERNAL_EXCEPTION_FILTER}`) .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') .send(getExceptionFilterFromExceptionItemsSchemaMock()) .expect(200); @@ -121,6 +122,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .post(`${INTERNAL_EXCEPTION_FILTER}`) .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') .send(getExceptionFilterFromExceptionIdsSchemaMock()) .expect(200); diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index 180e2826ba120..1c7f17961db81 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -273,6 +273,17 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { return true; }); }); + + it('should navigate to the details page when clicking on a rule in event logs tab', async () => { + await observability.alerts.rulesPage.clickLogsTab(); + await observability.alerts.rulesPage.clickOnRuleInEventLogs(); + await testSubjects.existOrFail('ruleDetails'); + }); + + it('shows the rule event log when navigating by URL', async () => { + await observability.alerts.common.navigateToRulesLogsPage(); + await testSubjects.existOrFail('ruleEventLogListTable'); + }); }); describe('User permissions', () => { diff --git a/x-pack/test/observability_onboarding_api_integration/tests/flow/progress/progress.spec.ts b/x-pack/test/observability_onboarding_api_integration/tests/flow/progress/progress.spec.ts index 93fd4294a0778..2cf9157d356c1 100644 --- a/x-pack/test/observability_onboarding_api_integration/tests/flow/progress/progress.spec.ts +++ b/x-pack/test/observability_onboarding_api_integration/tests/flow/progress/progress.spec.ts @@ -49,6 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { state: { datasetName, namespace, + logFilePaths: ['my-service.log'], }, }, }, diff --git a/x-pack/test/observability_onboarding_api_integration/tests/logs/create.spec.ts b/x-pack/test/observability_onboarding_api_integration/tests/logs/create.spec.ts index e263fb24d7580..f4fbdd8e5a57b 100644 --- a/x-pack/test/observability_onboarding_api_integration/tests/logs/create.spec.ts +++ b/x-pack/test/observability_onboarding_api_integration/tests/logs/create.spec.ts @@ -56,7 +56,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('when required privileges are set', () => { it('returns a flow id and apiKey encoded', async () => { - const request = await callApiWithPrivileges('logFiles'); + const state = { + datasetName: 'my-dataset', + serviceName: 'my-service', + namespace: 'my-namespace', + logFilePaths: ['my-service-logs.log'], + }; + + const request = await callApiWithPrivileges('logFiles', state); expect(request.status).to.be(200); expect(request.body.apiKeyEncoded).to.not.empty(); @@ -68,7 +75,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { datasetName: 'my-dataset', serviceName: 'my-service', namespace: 'my-namespace', - logFilePaths: 'my-service-logs.log', + logFilePaths: ['my-service-logs.log'], }; const request = await callApiWithPrivileges('logFiles', state); diff --git a/x-pack/test/observability_onboarding_api_integration/tests/logs/update_step_progress.spec.ts b/x-pack/test/observability_onboarding_api_integration/tests/logs/update_step_progress.spec.ts index 4554cbbfba733..7c53792769e5e 100644 --- a/x-pack/test/observability_onboarding_api_integration/tests/logs/update_step_progress.spec.ts +++ b/x-pack/test/observability_onboarding_api_integration/tests/logs/update_step_progress.spec.ts @@ -69,7 +69,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { body: { type: 'logFiles', name: 'name', - state: {}, + state: { + datasetName: 'my-dataset', + serviceName: 'my-service', + namespace: 'my-namespace', + logFilePaths: ['my-service.log'], + }, }, }, }); diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts index 684633d4aac13..daf1821ef5c05 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts @@ -87,7 +87,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Applications provider', function () { it('can search for root-level applications', async () => { const results = await findResultsWithApi('discover'); - expect(results.length).to.be(2); + expect(results.length).to.be(1); expect(results[0].title).to.be('Discover'); }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts index 80334e09f6999..6a30645c60b06 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts @@ -29,6 +29,7 @@ import { } from '@kbn/rule-registry-plugin/server/utils/create_lifecycle_executor'; import { Dataset, IRuleDataClient, RuleDataService } from '@kbn/rule-registry-plugin/server'; import { RuleExecutorOptions } from '@kbn/alerting-plugin/server'; +import { getDataStreamAdapter } from '@kbn/alerting-plugin/server/alerts_service/lib/data_stream_adapter'; import type { FtrProviderContext } from '../../../common/ftr_provider_context'; import { MockRuleParams, @@ -42,7 +43,6 @@ import { cleanupRegistryIndices, getMockAlertFactory } from '../../../common/lib // eslint-disable-next-line import/no-default-export export default function createLifecycleExecutorApiTest({ getService }: FtrProviderContext) { const es = getService('es'); - const log = getService('log'); const fakeLogger = (msg: string, meta?: Meta) => @@ -65,6 +65,8 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid return Promise.resolve(client); }; + const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: false }); + describe('createLifecycleExecutor', () => { let ruleDataClient: IRuleDataClient; let pluginStop$: Subject; @@ -86,6 +88,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid getContextInitializationPromise: async () => ({ result: false }), }, pluginStop$, + dataStreamAdapter, }); // This initializes the service. This happens immediately after the creation @@ -201,6 +204,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid lookBackWindow: 20, statusChangeThreshold: 4, }, + dataStreamAdapter, } as unknown as RuleExecutorOptions< MockRuleParams, WrappedLifecycleRuleState, diff --git a/x-pack/test/scalability/apis/api.core.capabilities.json b/x-pack/test/scalability/apis/api.core.capabilities.json index 15c9cb270bbb0..a05f5d1fcb5a8 100644 --- a/x-pack/test/scalability/apis/api.core.capabilities.json +++ b/x-pack/test/scalability/apis/api.core.capabilities.json @@ -35,7 +35,7 @@ "method": "POST", "path": "/api/core/capabilities", "query": "?useDefaultCapabilities=true", - "body": "{\"applications\":[\"error\",\"status\",\"kibana\",\"dev_tools\",\"r\",\"short_url_redirect\",\"home\",\"management\",\"space_selector\",\"security_access_agreement\",\"security_capture_url\",\"security_login\",\"security_logout\",\"security_logged_out\",\"security_overwritten_session\",\"security_account\",\"reportingRedirect\",\"graph\",\"discover\",\"integrations\",\"fleet\",\"ingestManager\",\"visualize\",\"canvas\",\"dashboards\",\"lens\",\"maps\",\"osquery\",\"observability-overview\",\"ml\",\"uptime\",\"synthetics\",\"securitySolutionUI\",\"siem\",\"logs\",\"metrics\",\"infra\",\"monitoring\",\"enterpriseSearch\",\"enterpriseSearchContent\",\"enterpriseSearchAnalytics\",\"elasticsearch\",\"appSearch\",\"workplaceSearch\",\"searchExperiences\",\"apm\",\"ux\",\"kibanaOverview\"]}", + "body": "{\"applications\":[\"error\",\"status\",\"kibana\",\"dev_tools\",\"r\",\"short_url_redirect\",\"home\",\"management\",\"space_selector\",\"security_access_agreement\",\"security_capture_url\",\"security_login\",\"security_logout\",\"security_logged_out\",\"security_overwritten_session\",\"security_account\",\"reportingRedirect\",\"graph\",\"discover\",\"integrations\",\"fleet\",\"ingestManager\",\"visualize\",\"canvas\",\"dashboards\",\"lens\",\"maps\",\"osquery\",\"observability-overview\",\"ml\",\"uptime\",\"synthetics\",\"securitySolutionUI\",\"siem\",\"logs\",\"metrics\",\"infra\",\"monitoring\",\"enterpriseSearch\",\"enterpriseSearchContent\",\"enterpriseSearchAnalytics\",\"enterpriseSearchElasticsearch\",\"appSearch\",\"workplaceSearch\",\"searchExperiences\",\"apm\",\"ux\",\"kibanaOverview\"]}", "headers": { "Cookie": "", "Kbn-Version": "", diff --git a/x-pack/test/scalability/apis/api.status.json b/x-pack/test/scalability/apis/api.status.json new file mode 100644 index 0000000000000..396d9d0645fc3 --- /dev/null +++ b/x-pack/test/scalability/apis/api.status.json @@ -0,0 +1,48 @@ +{ + "journeyName": "POST /api/status", + "scalabilitySetup": { + "responseTimeThreshold": { + "threshold1": 1000, + "threshold2": 3000, + "threshold3": 5000 + }, + "warmup": [ + { + "action": "constantUsersPerSec", + "userCount": 10, + "duration": "30s" + } + ], + "test": [ + { + "action": "rampUsersPerSec", + "minUsersCount": 10, + "maxUsersCount": 700, + "duration": "138s" + } + ], + "maxDuration": "6m" + }, + "testData": { + "esArchives": [], + "kbnArchives": [] + }, + "streams": [ + { + "requests": [ + { + "http": { + "method": "GET", + "path": "/api/status", + "headers": { + "Cookie": "", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/json; charset=utf-8" + }, + "statusCode": 200 + } + } + ] + } + ] +} diff --git a/x-pack/test/scalability/apis/api.status.no_auth.json b/x-pack/test/scalability/apis/api.status.no_auth.json new file mode 100644 index 0000000000000..2fe293f072a25 --- /dev/null +++ b/x-pack/test/scalability/apis/api.status.no_auth.json @@ -0,0 +1,47 @@ +{ + "journeyName": "POST /api/status", + "scalabilitySetup": { + "responseTimeThreshold": { + "threshold1": 1000, + "threshold2": 3000, + "threshold3": 5000 + }, + "warmup": [ + { + "action": "constantUsersPerSec", + "userCount": 10, + "duration": "30s" + } + ], + "test": [ + { + "action": "rampUsersPerSec", + "minUsersCount": 10, + "maxUsersCount": 1210, + "duration": "80s" + } + ], + "maxDuration": "4m" + }, + "testData": { + "esArchives": [], + "kbnArchives": [] + }, + "streams": [ + { + "requests": [ + { + "http": { + "method": "GET", + "path": "/api/status", + "headers": { + "Accept-Encoding": "gzip", + "Content-Type": "application/json; charset=utf-8" + }, + "statusCode": 200 + } + } + ] + } + ] +} diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts index ddbd07ef0c10e..9dc632931d301 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts @@ -35,7 +35,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'x-pack/test/functional/fixtures/kbn_archiver/discover/default' ); await kibanaServer.uiSettings.replace({ - 'discover:enableSql': true, + 'discover:enableESQL': true, }); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); @@ -131,6 +131,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(searchesCountBeforeRestore).to.be(searchesCountAfterRestore); // no new searches started during restore }); + + it('should should clean the search session when navigating to ESQL mode, and reinitialize when navigating back', async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await searchSessions.exists()).to.be(true); + await PageObjects.discover.selectTextBaseLang(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await searchSessions.missingOrFail(); + await browser.goBack(); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await searchSessions.exists()).to.be(true); + }); }); async function getSearchSessionId(): Promise { diff --git a/x-pack/test/security_solution_cypress/cypress/README.md b/x-pack/test/security_solution_cypress/cypress/README.md index 9a8ee567ff9a1..21a6a8a9db493 100644 --- a/x-pack/test/security_solution_cypress/cypress/README.md +++ b/x-pack/test/security_solution_cypress/cypress/README.md @@ -21,6 +21,8 @@ If you are still having doubts, questions or queries, please feel free to ping o [**Test data**](#test-data) +[**Serverless**](#serverless) + [**Development Best Practices**](#development-best-practices) [**Test Artifacts**](#test-artifacts) @@ -38,17 +40,9 @@ of data for your test, [**Running the tests**](#running-the-tests) to know how t Please, before opening a PR with the new test, please make sure that the test fails. If you never see your test fail you don’t know if your test is actually testing the right thing, or testing anything at all. -Note that we use tags in order to select which tests we want to execute: - -```typescript -export const tag = { - SERVERLESS: '@serverless', - ESS: '@ess', - BROKEN_IN_SERVERLESS: '@brokenInServerless', -}; -``` +Note that we use tags in order to select which tests we want to execute: @serverless, @ess and @brokenInServerless -Please, before opening a PR with the new test, make sure that the test fails. If you never see your test fail you don’t know if your test is actually testing the right thing, or testing anything at all. +Please, before opening a PR with a new test, make sure that the test fails. If you never see your test fail you don’t know if your test is actually testing the right thing, or testing anything at all. ## Running the tests @@ -184,10 +178,63 @@ Note that the command will create the folder if it does not exist. Task [cypress/support/es_archiver.ts](https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts) provides helpers such as `esArchiverLoad` and `esArchiverUnload` by means of `es_archiver`'s CLI. +## Serverless + +Note that we use tags in order to select which tests we want to execute, if you want a test to be executed on serverless you need to add @serverless tag to it. + + +### Running the serverless tests locally + +Run the tests with the following yarn scripts from `x-pack/test/security_solution_cypress`: + +| Script Name | Description | +| ----------- | ----------- | +| cypress:open:serverless | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a mocked serverless environment. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | +| cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | +| cypress:investigations:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | +| cypress:explore:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode | + +Please note that all the headless mode commands do not open the Cypress UI and are typically used in CI/CD environments. The scripts that open the Cypress UI are useful for development and debugging. + +### PLIs +When running serverless Cypress tests, the following PLIs are set by default: + +``` + { product_line: 'security', product_tier: 'complete' }, + { product_line: 'endpoint', product_tier: 'complete' }, + { product_line: 'cloud', product_tier: 'complete' }, +``` + +With the above configuration we'll be able to cover most of the scenarios, but there are some cases were we might want to use a different configuration. In that case, we just need to pass to the header of the test, which is the configuration we want for it. + +```typescript +describe( + 'Entity Analytics Dashboard in Serverless', + { + tags: '@serverless', + env: { + ftrConfig: { + productTypes: [ + { product_line: 'security', product_tier: 'essentials' }, + { product_line: 'endpoint', product_tier: 'essentials' }, + ], + }, + }, + }, +``` + +Per the way we set the environment during the execution process on CI, the above configuration is going to be valid when the test is executed on headless mode. + +For test developing or test debugging purposes, you need to modify the configuration but without committing and pushing the changes in `x-pack/test/security_solution_cypress/serverless_config.ts`. + ## Development Best Practices Below you will a set of best practices that should be followed when writing Cypress tests. +### For serverless +Reuse just those tests that have the SAME exact behaviour and steps. Take the necessity of adding a conditional to the test to make it pass in order to, perform a different set of steps, setup, or assertions, as a signal for the need of a serverless +specific test. + ### Avoid forced actions Cypress action commands like `click()`, `type()` and etc allow to pass `force` flag which is set to `false` by default. Avoid passing the `force` flag as it leads to swallowing some UI bugs. If it's impossible to perform an action without forcing it make sure to add an explanation comment and create a ticket to don't forget to fix it later on. The same is applicable to adding an extra `click()` before `type()` command. `type()` clicks an input once and types after so an extra `click()` usually means there is a problem. diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/create_runtime_field.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/create_runtime_field.cy.ts index 60f75e3a89a44..b9a514b8c2215 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/create_runtime_field.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/create_runtime_field.cy.ts @@ -25,33 +25,37 @@ import { GET_TIMELINE_HEADER } from '../../screens/timeline'; const alertRunTimeField = 'field.name.alert.page'; const timelineRuntimeField = 'field.name.timeline'; -describe('Create DataView runtime field', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - deleteRuntimeField('security-solution-default', alertRunTimeField); - deleteRuntimeField('security-solution-default', timelineRuntimeField); - }); - - beforeEach(() => { - login(); - }); - - it('adds field to alert table', () => { - visit(ALERTS_URL); - createRule(getNewRule()); - refreshPage(); - waitForAlertsToPopulate(); - openAlertsFieldBrowser(); - createField(alertRunTimeField); - cy.get(GET_DATA_GRID_HEADER(alertRunTimeField)).should('exist'); - }); - - it('adds field to timeline', () => { - visit(HOSTS_URL); - openTimelineUsingToggle(); - populateTimeline(); - openTimelineFieldsBrowser(); - - createField(timelineRuntimeField); - cy.get(GET_TIMELINE_HEADER(timelineRuntimeField)).should('exist'); - }); -}); +describe( + 'Create DataView runtime field', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + deleteRuntimeField('security-solution-default', alertRunTimeField); + deleteRuntimeField('security-solution-default', timelineRuntimeField); + }); + + beforeEach(() => { + login(); + }); + + it('adds field to alert table', () => { + visit(ALERTS_URL); + createRule(getNewRule()); + refreshPage(); + waitForAlertsToPopulate(); + openAlertsFieldBrowser(); + createField(alertRunTimeField); + cy.get(GET_DATA_GRID_HEADER(alertRunTimeField)).should('exist'); + }); + + it('adds field to timeline', () => { + visit(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + openTimelineFieldsBrowser(); + + createField(timelineRuntimeField); + cy.get(GET_TIMELINE_HEADER(timelineRuntimeField)).should('exist'); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer.cy.ts index 1a776101e4f8b..77ce5b56d30ae 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer.cy.ts @@ -5,40 +5,30 @@ * 2.0. */ -import { - DEFAULT_ALERTS_INDEX, - DEFAULT_INDEX_PATTERN, -} from '@kbn/security-solution-plugin/common/constants'; +import { DEFAULT_INDEX_PATTERN } from '@kbn/security-solution-plugin/common/constants'; import { login, loginWithUser, visit, visitWithUser } from '../../tasks/login'; -import { HOSTS_URL, TIMELINES_URL } from '../../urls/navigation'; +import { HOSTS_URL } from '../../urls/navigation'; import { addIndexToDefault, - clickAlertCheckbox, deselectSourcererOptions, isDataViewSelection, isHostsStatValue, isKibanaDataViewOption, - isNotSourcererOption, isNotSourcererSelection, isSourcererOptions, isSourcererSelection, openAdvancedSettings, openDataViewSelection, openSourcerer, - refreshUntilAlertsIndexExists, resetSourcerer, saveSourcerer, } from '../../tasks/sourcerer'; import { postDataView } from '../../tasks/common'; -import { openTimelineUsingToggle } from '../../tasks/security_main'; import { createUsersAndRoles, secReadCasesAll, secReadCasesAllUser } from '../../tasks/privileges'; import { TOASTER } from '../../screens/configure_cases'; import { SOURCERER } from '../../screens/sourcerer'; -import { createTimeline } from '../../tasks/api_calls/timelines'; -import { getTimeline, getTimelineModifiedSourcerer } from '../../objects/timeline'; -import { closeTimeline, openTimelineById } from '../../tasks/timeline'; const usersToCreate = [secReadCasesAllUser]; const rolesToCreate = [secReadCasesAll]; @@ -50,7 +40,8 @@ describe('Sourcerer', () => { cy.task('esArchiverResetKibana'); dataViews.forEach((dataView: string) => postDataView(dataView)); }); - describe('permissions', { tags: '@ess' }, () => { + + describe('permissions', { tags: ['@ess', '@brokenInServerless'] }, () => { before(() => { createUsersAndRoles(usersToCreate, rolesToCreate); }); @@ -138,130 +129,3 @@ describe('Sourcerer', () => { ); }); }); -describe('Timeline scope', { tags: '@brokenInServerless' }, () => { - beforeEach(() => { - cy.clearLocalStorage(); - login(); - visit(TIMELINES_URL); - }); - - it('correctly loads SIEM data view', () => { - openTimelineUsingToggle(); - openSourcerer('timeline'); - isDataViewSelection(siemDataViewTitle); - openAdvancedSettings(); - isSourcererSelection(`auditbeat-*`); - isSourcererSelection(`${DEFAULT_ALERTS_INDEX}-default`); - isSourcererOptions(DEFAULT_INDEX_PATTERN.filter((pattern) => pattern !== 'auditbeat-*')); - isNotSourcererOption(`${DEFAULT_ALERTS_INDEX}-default`); - }); - - describe('Modified badge', () => { - it('Selecting new data view does not add a modified badge', () => { - openTimelineUsingToggle(); - cy.get(SOURCERER.badgeModified).should(`not.exist`); - openSourcerer('timeline'); - cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); - openDataViewSelection(); - isKibanaDataViewOption(dataViews); - cy.get(SOURCERER.selectListDefaultOption).should(`contain`, siemDataViewTitle); - cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); - isDataViewSelection(dataViews[1]); - saveSourcerer(); - cy.get(SOURCERER.badgeModified).should(`not.exist`); - openSourcerer('timeline'); - cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); - }); - - it('shows modified badge when index patterns change and removes when reset', () => { - openTimelineUsingToggle(); - openSourcerer('timeline'); - openDataViewSelection(); - cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); - isDataViewSelection(dataViews[1]); - openAdvancedSettings(); - const patterns = dataViews[1].split(','); - deselectSourcererOptions([patterns[0]]); - saveSourcerer(); - cy.get(SOURCERER.badgeModified).should(`exist`); - openSourcerer('timeline'); - cy.get(SOURCERER.badgeModifiedOption).should(`exist`); - resetSourcerer(); - saveSourcerer(); - cy.get(SOURCERER.badgeModified).should(`not.exist`); - openSourcerer('timeline'); - cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); - isDataViewSelection(siemDataViewTitle); - }); - }); - describe('Alerts checkbox', () => { - before(() => { - login(); - createTimeline(getTimeline()).then((response) => - cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId') - ); - createTimeline(getTimelineModifiedSourcerer()).then((response) => - cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('auditbeatTimelineId') - ); - }); - - beforeEach(() => { - login(); - visit(TIMELINES_URL); - refreshUntilAlertsIndexExists(); - }); - - it('Modifies timeline to alerts only, and switches to different saved timeline without issue', function () { - openTimelineById(this.timelineId).then(() => { - cy.get(SOURCERER.badgeAlerts).should(`not.exist`); - cy.get(SOURCERER.badgeModified).should(`not.exist`); - openSourcerer('timeline'); - clickAlertCheckbox(); - saveSourcerer(); - cy.get(SOURCERER.badgeAlerts).should(`exist`); - cy.get(SOURCERER.badgeModified).should(`not.exist`); - closeTimeline(); - - openTimelineById(this.auditbeatTimelineId).then(() => { - cy.get(SOURCERER.badgeModified).should(`exist`); - cy.get(SOURCERER.badgeAlerts).should(`not.exist`); - openSourcerer('timeline'); - openAdvancedSettings(); - isSourcererSelection(`auditbeat-*`); - }); - }); - }); - - const defaultPatterns = [`auditbeat-*`, `${DEFAULT_ALERTS_INDEX}-default`]; - it('alerts checkbox behaves as expected', () => { - isDataViewSelection(siemDataViewTitle); - defaultPatterns.forEach((pattern) => isSourcererSelection(pattern)); - openDataViewSelection(); - cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); - isDataViewSelection(dataViews[1]); - dataViews[1] - .split(',') - .filter((pattern) => pattern !== 'fakebeat-*' && pattern !== 'siem-read*') - .forEach((pattern) => isSourcererSelection(pattern)); - - clickAlertCheckbox(); - isNotSourcererSelection(`auditbeat-*`); - isSourcererSelection(`${DEFAULT_ALERTS_INDEX}-default`); - cy.get(SOURCERER.alertCheckbox).uncheck({ force: true }); - defaultPatterns.forEach((pattern) => isSourcererSelection(pattern)); - }); - - it('shows alerts badge when index patterns change and removes when reset', () => { - clickAlertCheckbox(); - saveSourcerer(); - cy.get(SOURCERER.badgeAlerts).should(`exist`); - openSourcerer('timeline'); - cy.get(SOURCERER.badgeAlertsOption).should(`exist`); - resetSourcerer(); - saveSourcerer(); - cy.get(SOURCERER.badgeAlerts).should(`not.exist`); - openSourcerer('timeline'); - cy.get(SOURCERER.badgeAlertsOption).should(`not.exist`); - }); - }); -}); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer_timeline.cy.ts new file mode 100644 index 0000000000000..1476e82421cda --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer_timeline.cy.ts @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + DEFAULT_ALERTS_INDEX, + DEFAULT_INDEX_PATTERN, +} from '@kbn/security-solution-plugin/common/constants'; + +import { login, visit } from '../../tasks/login'; + +import { TIMELINES_URL } from '../../urls/navigation'; +import { + clickAlertCheckbox, + deselectSourcererOptions, + isDataViewSelection, + isKibanaDataViewOption, + isNotSourcererOption, + isNotSourcererSelection, + isSourcererOptions, + isSourcererSelection, + openAdvancedSettings, + openDataViewSelection, + openSourcerer, + refreshUntilAlertsIndexExists, + resetSourcerer, + saveSourcerer, +} from '../../tasks/sourcerer'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { SOURCERER } from '../../screens/sourcerer'; +import { createTimeline } from '../../tasks/api_calls/timelines'; +import { getTimeline, getTimelineModifiedSourcerer } from '../../objects/timeline'; +import { closeTimeline, openTimelineById } from '../../tasks/timeline'; + +const siemDataViewTitle = 'Security Default Data View'; +const dataViews = ['auditbeat-*,fakebeat-*', 'auditbeat-*,*beat*,siem-read*,.kibana*,fakebeat-*']; + +describe('Timeline scope', { tags: '@brokenInServerless' }, () => { + beforeEach(() => { + cy.clearLocalStorage(); + login(); + visit(TIMELINES_URL); + }); + + it('correctly loads SIEM data view', () => { + openTimelineUsingToggle(); + openSourcerer('timeline'); + isDataViewSelection(siemDataViewTitle); + openAdvancedSettings(); + isSourcererSelection(`auditbeat-*`); + isSourcererSelection(`${DEFAULT_ALERTS_INDEX}-default`); + isSourcererOptions(DEFAULT_INDEX_PATTERN.filter((pattern) => pattern !== 'auditbeat-*')); + isNotSourcererOption(`${DEFAULT_ALERTS_INDEX}-default`); + }); + + describe('Modified badge', () => { + it('Selecting new data view does not add a modified badge', () => { + openTimelineUsingToggle(); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); + openDataViewSelection(); + isKibanaDataViewOption(dataViews); + cy.get(SOURCERER.selectListDefaultOption).should(`contain`, siemDataViewTitle); + cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); + isDataViewSelection(dataViews[1]); + saveSourcerer(); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); + }); + + it('shows modified badge when index patterns change and removes when reset', () => { + openTimelineUsingToggle(); + openSourcerer('timeline'); + openDataViewSelection(); + cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); + isDataViewSelection(dataViews[1]); + openAdvancedSettings(); + const patterns = dataViews[1].split(','); + deselectSourcererOptions([patterns[0]]); + saveSourcerer(); + cy.get(SOURCERER.badgeModified).should(`exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeModifiedOption).should(`exist`); + resetSourcerer(); + saveSourcerer(); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); + isDataViewSelection(siemDataViewTitle); + }); + }); + describe('Alerts checkbox', () => { + before(() => { + login(); + createTimeline(getTimeline()).then((response) => + cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId') + ); + createTimeline(getTimelineModifiedSourcerer()).then((response) => + cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('auditbeatTimelineId') + ); + }); + + beforeEach(() => { + login(); + visit(TIMELINES_URL); + refreshUntilAlertsIndexExists(); + }); + + it('Modifies timeline to alerts only, and switches to different saved timeline without issue', function () { + openTimelineById(this.timelineId).then(() => { + cy.get(SOURCERER.badgeAlerts).should(`not.exist`); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + openSourcerer('timeline'); + clickAlertCheckbox(); + saveSourcerer(); + cy.get(SOURCERER.badgeAlerts).should(`exist`); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + closeTimeline(); + + openTimelineById(this.auditbeatTimelineId).then(() => { + cy.get(SOURCERER.badgeModified).should(`exist`); + cy.get(SOURCERER.badgeAlerts).should(`not.exist`); + openSourcerer('timeline'); + openAdvancedSettings(); + isSourcererSelection(`auditbeat-*`); + }); + }); + }); + + const defaultPatterns = [`auditbeat-*`, `${DEFAULT_ALERTS_INDEX}-default`]; + it('alerts checkbox behaves as expected', () => { + isDataViewSelection(siemDataViewTitle); + defaultPatterns.forEach((pattern) => isSourcererSelection(pattern)); + openDataViewSelection(); + cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); + isDataViewSelection(dataViews[1]); + dataViews[1] + .split(',') + .filter((pattern) => pattern !== 'fakebeat-*' && pattern !== 'siem-read*') + .forEach((pattern) => isSourcererSelection(pattern)); + + clickAlertCheckbox(); + isNotSourcererSelection(`auditbeat-*`); + isSourcererSelection(`${DEFAULT_ALERTS_INDEX}-default`); + cy.get(SOURCERER.alertCheckbox).uncheck({ force: true }); + defaultPatterns.forEach((pattern) => isSourcererSelection(pattern)); + }); + + it('shows alerts badge when index patterns change and removes when reset', () => { + clickAlertCheckbox(); + saveSourcerer(); + cy.get(SOURCERER.badgeAlerts).should(`exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeAlertsOption).should(`exist`); + resetSourcerer(); + saveSourcerer(); + cy.get(SOURCERER.badgeAlerts).should(`not.exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeAlertsOption).should(`not.exist`); + }); + }); +}); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer_timeline.ts b/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer_timeline.ts new file mode 100644 index 0000000000000..1476e82421cda --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/data_sources/sourcerer_timeline.ts @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + DEFAULT_ALERTS_INDEX, + DEFAULT_INDEX_PATTERN, +} from '@kbn/security-solution-plugin/common/constants'; + +import { login, visit } from '../../tasks/login'; + +import { TIMELINES_URL } from '../../urls/navigation'; +import { + clickAlertCheckbox, + deselectSourcererOptions, + isDataViewSelection, + isKibanaDataViewOption, + isNotSourcererOption, + isNotSourcererSelection, + isSourcererOptions, + isSourcererSelection, + openAdvancedSettings, + openDataViewSelection, + openSourcerer, + refreshUntilAlertsIndexExists, + resetSourcerer, + saveSourcerer, +} from '../../tasks/sourcerer'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { SOURCERER } from '../../screens/sourcerer'; +import { createTimeline } from '../../tasks/api_calls/timelines'; +import { getTimeline, getTimelineModifiedSourcerer } from '../../objects/timeline'; +import { closeTimeline, openTimelineById } from '../../tasks/timeline'; + +const siemDataViewTitle = 'Security Default Data View'; +const dataViews = ['auditbeat-*,fakebeat-*', 'auditbeat-*,*beat*,siem-read*,.kibana*,fakebeat-*']; + +describe('Timeline scope', { tags: '@brokenInServerless' }, () => { + beforeEach(() => { + cy.clearLocalStorage(); + login(); + visit(TIMELINES_URL); + }); + + it('correctly loads SIEM data view', () => { + openTimelineUsingToggle(); + openSourcerer('timeline'); + isDataViewSelection(siemDataViewTitle); + openAdvancedSettings(); + isSourcererSelection(`auditbeat-*`); + isSourcererSelection(`${DEFAULT_ALERTS_INDEX}-default`); + isSourcererOptions(DEFAULT_INDEX_PATTERN.filter((pattern) => pattern !== 'auditbeat-*')); + isNotSourcererOption(`${DEFAULT_ALERTS_INDEX}-default`); + }); + + describe('Modified badge', () => { + it('Selecting new data view does not add a modified badge', () => { + openTimelineUsingToggle(); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); + openDataViewSelection(); + isKibanaDataViewOption(dataViews); + cy.get(SOURCERER.selectListDefaultOption).should(`contain`, siemDataViewTitle); + cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); + isDataViewSelection(dataViews[1]); + saveSourcerer(); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); + }); + + it('shows modified badge when index patterns change and removes when reset', () => { + openTimelineUsingToggle(); + openSourcerer('timeline'); + openDataViewSelection(); + cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); + isDataViewSelection(dataViews[1]); + openAdvancedSettings(); + const patterns = dataViews[1].split(','); + deselectSourcererOptions([patterns[0]]); + saveSourcerer(); + cy.get(SOURCERER.badgeModified).should(`exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeModifiedOption).should(`exist`); + resetSourcerer(); + saveSourcerer(); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeModifiedOption).should(`not.exist`); + isDataViewSelection(siemDataViewTitle); + }); + }); + describe('Alerts checkbox', () => { + before(() => { + login(); + createTimeline(getTimeline()).then((response) => + cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId') + ); + createTimeline(getTimelineModifiedSourcerer()).then((response) => + cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('auditbeatTimelineId') + ); + }); + + beforeEach(() => { + login(); + visit(TIMELINES_URL); + refreshUntilAlertsIndexExists(); + }); + + it('Modifies timeline to alerts only, and switches to different saved timeline without issue', function () { + openTimelineById(this.timelineId).then(() => { + cy.get(SOURCERER.badgeAlerts).should(`not.exist`); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + openSourcerer('timeline'); + clickAlertCheckbox(); + saveSourcerer(); + cy.get(SOURCERER.badgeAlerts).should(`exist`); + cy.get(SOURCERER.badgeModified).should(`not.exist`); + closeTimeline(); + + openTimelineById(this.auditbeatTimelineId).then(() => { + cy.get(SOURCERER.badgeModified).should(`exist`); + cy.get(SOURCERER.badgeAlerts).should(`not.exist`); + openSourcerer('timeline'); + openAdvancedSettings(); + isSourcererSelection(`auditbeat-*`); + }); + }); + }); + + const defaultPatterns = [`auditbeat-*`, `${DEFAULT_ALERTS_INDEX}-default`]; + it('alerts checkbox behaves as expected', () => { + isDataViewSelection(siemDataViewTitle); + defaultPatterns.forEach((pattern) => isSourcererSelection(pattern)); + openDataViewSelection(); + cy.get(SOURCERER.selectListOption).contains(dataViews[1]).click(); + isDataViewSelection(dataViews[1]); + dataViews[1] + .split(',') + .filter((pattern) => pattern !== 'fakebeat-*' && pattern !== 'siem-read*') + .forEach((pattern) => isSourcererSelection(pattern)); + + clickAlertCheckbox(); + isNotSourcererSelection(`auditbeat-*`); + isSourcererSelection(`${DEFAULT_ALERTS_INDEX}-default`); + cy.get(SOURCERER.alertCheckbox).uncheck({ force: true }); + defaultPatterns.forEach((pattern) => isSourcererSelection(pattern)); + }); + + it('shows alerts badge when index patterns change and removes when reset', () => { + clickAlertCheckbox(); + saveSourcerer(); + cy.get(SOURCERER.badgeAlerts).should(`exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeAlertsOption).should(`exist`); + resetSourcerer(); + saveSourcerer(); + cy.get(SOURCERER.badgeAlerts).should(`not.exist`); + openSourcerer('timeline'); + cy.get(SOURCERER.badgeAlertsOption).should(`not.exist`); + }); + }); +}); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alert_tags.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alert_tags.cy.ts index 52a6afc2e4f25..1013a4d7d53d4 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alert_tags.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alert_tags.cy.ts @@ -24,7 +24,7 @@ import { UNSELECTED_ALERT_TAG, } from '../../screens/alerts'; -describe('Alert tagging', { tags: ['@ess', '@serverless'] }, () => { +describe('Alert tagging', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); cy.task('esArchiverResetKibana'); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_charts.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_charts.cy.ts index 656239c7308d3..313d2a625ad9f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_charts.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_charts.cy.ts @@ -24,46 +24,50 @@ import { } from '../../screens/search_bar'; import { TOASTER } from '../../screens/alerts_detection_rules'; -describe('Histogram legend hover actions', { tags: ['@ess', '@serverless'] }, () => { - const ruleConfigs = getNewRule(); +describe( + 'Histogram legend hover actions', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + const ruleConfigs = getNewRule(); - before(() => { - cleanKibana(); - }); + before(() => { + cleanKibana(); + }); - beforeEach(() => { - login(); - createRule(getNewRule({ rule_id: 'new custom rule' })); - visit(ALERTS_URL); - selectAlertsHistogram(); - }); + beforeEach(() => { + login(); + createRule(getNewRule({ rule_id: 'new custom rule' })); + visit(ALERTS_URL); + selectAlertsHistogram(); + }); - it('Filter in/out should add a filter to KQL bar', function () { - const expectedNumberOfAlerts = 2; - clickAlertsHistogramLegend(); - clickAlertsHistogramLegendFilterFor(ruleConfigs.name); - cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should( - 'have.text', - `kibana.alert.rule.name: ${ruleConfigs.name}` - ); - cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); + it('Filter in/out should add a filter to KQL bar', function () { + const expectedNumberOfAlerts = 2; + clickAlertsHistogramLegend(); + clickAlertsHistogramLegendFilterFor(ruleConfigs.name); + cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should( + 'have.text', + `kibana.alert.rule.name: ${ruleConfigs.name}` + ); + cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); - clickAlertsHistogramLegend(); - clickAlertsHistogramLegendFilterOut(ruleConfigs.name); - cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should( - 'have.text', - `NOT kibana.alert.rule.name: ${ruleConfigs.name}` - ); - cy.get(ALERTS_COUNT).should('not.exist'); + clickAlertsHistogramLegend(); + clickAlertsHistogramLegendFilterOut(ruleConfigs.name); + cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should( + 'have.text', + `NOT kibana.alert.rule.name: ${ruleConfigs.name}` + ); + cy.get(ALERTS_COUNT).should('not.exist'); - cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM_DELETE).click(); - cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should('not.exist'); - }); + cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM_DELETE).click(); + cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should('not.exist'); + }); - it('Add To Timeline', function () { - clickAlertsHistogramLegend(); - clickAlertsHistogramLegendAddToTimeline(ruleConfigs.name); + it('Add To Timeline', function () { + clickAlertsHistogramLegend(); + clickAlertsHistogramLegendAddToTimeline(ruleConfigs.name); - cy.get(TOASTER).should('have.text', `Added ${ruleConfigs.name} to timeline`); - }); -}); + cy.get(TOASTER).should('have.text', `Added ${ruleConfigs.name} to timeline`); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/cti_enrichments.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/cti_enrichments.cy.ts index ebf5f97bbc790..2057a7db3363f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/cti_enrichments.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/cti_enrichments.cy.ts @@ -28,9 +28,10 @@ import { openJsonView, openThreatIndicatorDetails } from '../../tasks/alerts_det import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; import { addsFieldsToTimeline } from '../../tasks/rule_details'; -describe('CTI Enrichment', { tags: ['@ess', '@serverless'] }, () => { +describe('CTI Enrichment', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); + // illegal_argument_exception: unknown setting [index.lifecycle.rollover_alias] cy.task('esArchiverLoad', { archiveName: 'threat_indicator' }); cy.task('esArchiverLoad', { archiveName: 'suspicious_source_event' }); login(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/enrichments.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/enrichments.cy.ts index d427a7e3d43a4..102405eb95a61 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/enrichments.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/enrichments.cy.ts @@ -30,7 +30,7 @@ import { login, visit } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; -describe('Enrichment', { tags: ['@ess', '@serverless'] }, () => { +describe('Enrichment', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); cy.task('esArchiverLoad', { archiveName: 'risk_users' }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/ransomware_detection.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/ransomware_detection.cy.ts index 236a23da8e71d..a38ac3b2fb842 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/ransomware_detection.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/ransomware_detection.cy.ts @@ -14,51 +14,57 @@ import { TIMELINE_QUERY, TIMELINE_VIEW_IN_ANALYZER } from '../../screens/timelin import { selectAlertsHistogram } from '../../tasks/alerts'; import { createTimeline } from '../../tasks/timelines'; -describe('Ransomware Detection Alerts', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cy.task('esArchiverLoad', { - archiveName: 'ransomware_detection', - }); - }); - - describe('Ransomware display in Alerts Section', () => { - beforeEach(() => { - login(); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - }); - - describe('Alerts table', () => { - it('shows Ransomware Alerts', () => { - cy.get(ALERT_RULE_NAME).should('have.text', 'Ransomware Detection Alert'); +describe( + 'Ransomware Detection Alerts', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cy.task('esArchiverLoad', { + archiveName: 'ransomware_detection', + useCreate: true, + docsOnly: true, }); }); - describe('Trend Chart', () => { + describe('Ransomware display in Alerts Section', () => { beforeEach(() => { - selectAlertsHistogram(); + login(); + visit(ALERTS_URL); + waitForAlertsToPopulate(); }); - it('shows Ransomware Detection Alert in the trend chart', () => { - cy.get(ALERTS_HISTOGRAM_SERIES).should('have.text', 'Ransomware Detection Alert'); + describe('Alerts table', () => { + it('shows Ransomware Alerts', () => { + cy.get(ALERT_RULE_NAME).should('have.text', 'Ransomware Detection Alert'); + }); }); - }); - }); - describe('Ransomware in Timelines', () => { - before(() => { - login(); - visit(TIMELINES_URL); - createTimeline(); + describe('Trend Chart', () => { + beforeEach(() => { + selectAlertsHistogram(); + }); + + it('shows Ransomware Detection Alert in the trend chart', () => { + cy.get(ALERTS_HISTOGRAM_SERIES).should('have.text', 'Ransomware Detection Alert'); + }); + }); }); - it('Renders ransomware entries in timelines table', () => { - cy.get(TIMELINE_QUERY).type('event.code: "ransomware"{enter}'); + describe('Ransomware in Timelines', () => { + before(() => { + login(); + visit(TIMELINES_URL); + createTimeline(); + }); + + it('Renders ransomware entries in timelines table', () => { + cy.get(TIMELINE_QUERY).type('event.code: "ransomware"{enter}'); - // Wait for grid to load, it should have an analyzer icon - cy.get(TIMELINE_VIEW_IN_ANALYZER).should('exist'); + // Wait for grid to load, it should have an analyzer icon + cy.get(TIMELINE_VIEW_IN_ANALYZER).should('exist'); - cy.get(MESSAGE).should('have.text', 'Ransomware Detection Alert'); + cy.get(MESSAGE).should('have.text', 'Ransomware Detection Alert'); + }); }); - }); -}); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/ransomware_prevention.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/ransomware_prevention.cy.ts index ea80743898686..c71c3634246a7 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/ransomware_prevention.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/ransomware_prevention.cy.ts @@ -15,57 +15,63 @@ import { selectAlertsHistogram } from '../../tasks/alerts'; import { createTimeline } from '../../tasks/timelines'; import { cleanKibana } from '../../tasks/common'; -describe('Ransomware Prevention Alerts', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cleanKibana(); - cy.task('esArchiverLoad', { - archiveName: 'ransomware_prevention', +describe( + 'Ransomware Prevention Alerts', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cleanKibana(); + cy.task('esArchiverLoad', { + archiveName: 'ransomware_prevention', + useCreate: true, + docsOnly: true, + }); }); - }); - - after(() => { - cy.task('esArchiverUnload', 'ransomware_prevention'); - }); - describe('Ransomware display in Alerts Section', () => { - beforeEach(() => { - login(); - visit(ALERTS_URL); - waitForAlertsToPopulate(); + after(() => { + cy.task('esArchiverUnload', 'ransomware_prevention'); }); - describe('Alerts table', () => { - it('shows Ransomware Alerts', () => { - cy.get(ALERT_RULE_NAME).should('have.text', 'Ransomware Prevention Alert'); + describe('Ransomware display in Alerts Section', () => { + beforeEach(() => { + login(); + visit(ALERTS_URL); + waitForAlertsToPopulate(); }); - }); - describe('Trend Chart', () => { - beforeEach(() => { - selectAlertsHistogram(); + describe('Alerts table', () => { + it('shows Ransomware Alerts', () => { + cy.get(ALERT_RULE_NAME).should('have.text', 'Ransomware Prevention Alert'); + }); }); - it('shows Ransomware Prevention Alert in the trend chart', () => { - cy.get(ALERTS_HISTOGRAM_SERIES).should('have.text', 'Ransomware Prevention Alert'); + describe('Trend Chart', () => { + beforeEach(() => { + selectAlertsHistogram(); + }); + + it('shows Ransomware Prevention Alert in the trend chart', () => { + cy.get(ALERTS_HISTOGRAM_SERIES).should('have.text', 'Ransomware Prevention Alert'); + }); }); }); - }); - describe('Ransomware in Timelines', () => { - beforeEach(() => { - login(); - visit(TIMELINES_URL); + describe('Ransomware in Timelines', () => { + beforeEach(() => { + login(); + visit(TIMELINES_URL); - createTimeline(); - }); + createTimeline(); + }); - it('Renders ransomware entries in timelines table', () => { - cy.get(TIMELINE_QUERY).type('event.code: "ransomware"{enter}'); + it('Renders ransomware entries in timelines table', () => { + cy.get(TIMELINE_QUERY).type('event.code: "ransomware"{enter}'); - // Wait for grid to load, it should have an analyzer icon - cy.get(TIMELINE_VIEW_IN_ANALYZER).should('exist'); + // Wait for grid to load, it should have an analyzer icon + cy.get(TIMELINE_VIEW_IN_ANALYZER).should('exist'); - cy.get(MESSAGE).should('have.text', 'Ransomware Prevention Alert'); + cy.get(MESSAGE).should('have.text', 'Ransomware Prevention Alert'); + }); }); - }); -}); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule.cy.ts index b62e2f8a28ad6..7bb89b35ca83a 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule.cy.ts @@ -106,11 +106,10 @@ import { goToScheduleStepTab, importSavedQuery, waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, } from '../../../tasks/create_new_rule'; import { saveEditedRule } from '../../../tasks/edit_rule'; import { login, visit, visitSecurityDetectionRulesPage } from '../../../tasks/login'; -import { enablesRule, getDetails } from '../../../tasks/rule_details'; +import { enablesRule, getDetails, waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; import { RULE_CREATION } from '../../../urls/navigation'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule_data_view.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule_data_view.cy.ts index d1a0eb1d5eea2..2c2f44024e7a2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule_data_view.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule_data_view.cy.ts @@ -60,11 +60,10 @@ import { fillDefineCustomRuleAndContinue, fillScheduleRuleAndContinue, waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, } from '../../../tasks/create_new_rule'; import { login, visit } from '../../../tasks/login'; -import { getDetails } from '../../../tasks/rule_details'; +import { getDetails, waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; import { RULE_CREATION } from '../../../urls/navigation'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/event_correlation_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/event_correlation_rule.cy.ts index a767fe7b533e2..a558ac6ccedb7 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/event_correlation_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/event_correlation_rule.cy.ts @@ -41,7 +41,7 @@ import { TIMELINE_TEMPLATE_DETAILS, } from '../../../screens/rule_details'; -import { getDetails } from '../../../tasks/rule_details'; +import { getDetails, waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; import { expectNumberOfRules, goToRuleDetails, @@ -55,7 +55,6 @@ import { fillScheduleRuleAndContinue, selectEqlRuleType, waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, } from '../../../tasks/create_new_rule'; import { login, visit } from '../../../tasks/login'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/indicator_match_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/indicator_match_rule.cy.ts index 6174d07e8ad5d..9e896fa6861fd 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/indicator_match_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/indicator_match_rule.cy.ts @@ -92,7 +92,6 @@ import { getIndicatorOrButton, selectIndicatorMatchType, waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, } from '../../../tasks/create_new_rule'; import { SCHEDULE_INTERVAL_AMOUNT_INPUT, @@ -102,7 +101,11 @@ import { } from '../../../screens/create_new_rule'; import { goBackToRuleDetails } from '../../../tasks/edit_rule'; import { login, visit, visitWithoutDateRange } from '../../../tasks/login'; -import { goBackToRulesTable, getDetails } from '../../../tasks/rule_details'; +import { + goBackToRulesTable, + getDetails, + waitForTheRuleToBeExecuted, +} from '../../../tasks/rule_details'; import { DETECTIONS_RULE_MANAGEMENT_URL, RULE_CREATION } from '../../../urls/navigation'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/new_terms_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/new_terms_rule.cy.ts index e81dfd3d057d4..2b840111b97bc 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/new_terms_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/new_terms_rule.cy.ts @@ -43,7 +43,7 @@ import { NEW_TERMS_FIELDS_DETAILS, } from '../../../screens/rule_details'; -import { getDetails } from '../../../tasks/rule_details'; +import { getDetails, waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; import { expectNumberOfRules, goToRuleDetails } from '../../../tasks/alerts_detection_rules'; import { cleanKibana, deleteAlertsAndRules } from '../../../tasks/common'; import { @@ -53,7 +53,6 @@ import { fillScheduleRuleAndContinue, selectNewTermsRuleType, waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, } from '../../../tasks/create_new_rule'; import { login, visit } from '../../../tasks/login'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/override.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/override.cy.ts index e63a78977de6b..4aa1bb9e56724 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/override.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/override.cy.ts @@ -48,16 +48,16 @@ import { } from '../../../screens/rule_details'; import { expectNumberOfRules, goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { deleteAlertsAndRules } from '../../../tasks/common'; import { createAndEnableRule, fillAboutRuleWithOverrideAndContinue, fillDefineCustomRuleAndContinue, fillScheduleRuleAndContinue, waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, } from '../../../tasks/create_new_rule'; import { login, visitWithoutDateRange } from '../../../tasks/login'; -import { getDetails } from '../../../tasks/rule_details'; +import { getDetails, waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; import { RULE_CREATION } from '../../../urls/navigation'; @@ -71,6 +71,7 @@ describe('Detection rules, override', { tags: ['@ess', '@brokenInServerless'] }, beforeEach(() => { login(); + deleteAlertsAndRules(); }); it('Creates and enables a new custom rule with override option', function () { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/threshold_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/threshold_rule.cy.ts index c3d3deb58d520..078be723c39ac 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/threshold_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/threshold_rule.cy.ts @@ -43,7 +43,7 @@ import { TIMELINE_TEMPLATE_DETAILS, } from '../../../screens/rule_details'; -import { getDetails } from '../../../tasks/rule_details'; +import { getDetails, waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; import { expectNumberOfRules, goToRuleDetails } from '../../../tasks/alerts_detection_rules'; import { cleanKibana, deleteAlertsAndRules } from '../../../tasks/common'; import { @@ -53,7 +53,6 @@ import { fillScheduleRuleAndContinue, selectThresholdRuleType, waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, } from '../../../tasks/create_new_rule'; import { login, visitWithoutDateRange } from '../../../tasks/login'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts index 0d41e51852203..f5aaef1b4e841 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts @@ -5,239 +5,433 @@ * 2.0. */ -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../../urls/navigation'; - +import { omit } from 'lodash'; +import { PerformRuleInstallationResponseBody } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { filterBy, openTable } from '../../../../tasks/rule_details_flyout'; +import { generateEvent } from '../../../../objects/event'; +import { createDocument, deleteDataStream } from '../../../../tasks/api_calls/elasticsearch'; +import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { FIELD } from '../../../../screens/alerts_details'; -import { INTEGRATIONS, INTEGRATIONS_STATUS } from '../../../../screens/rule_details'; +import { INTEGRATION_LINK, INTEGRATION_STATUS } from '../../../../screens/rule_details'; import { INTEGRATIONS_POPOVER, INTEGRATIONS_POPOVER_TITLE, RULE_NAME, } from '../../../../screens/alerts_detection_rules'; - +import { + installPrebuiltRuleAssets, + installAllPrebuiltRulesRequest, + SAMPLE_PREBUILT_RULE, +} from '../../../../tasks/api_calls/prebuilt_rules'; import { cleanFleet } from '../../../../tasks/api_calls/fleet'; -import { importRule } from '../../../../tasks/api_calls/rules'; import { disableRelatedIntegrations, enableRelatedIntegrations, } from '../../../../tasks/api_calls/kibana_advanced_settings'; - -import { cleanKibana } from '../../../../tasks/common'; -import { login, visit } from '../../../../tasks/login'; +import { deleteAlertsAndRules } from '../../../../tasks/common'; +import { + login, + visitSecurityDetectionRulesPage, + visitWithoutDateRange, +} from '../../../../tasks/login'; import { expandFirstAlert } from '../../../../tasks/alerts'; -import { filterBy, openTable } from '../../../../tasks/alerts_details'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -import { installAwsCloudFrontWithPolicy } from '../../../../tasks/integrations'; import { - enableRule, - goToTheRuleDetailsOf, + installIntegrations, + PackagePolicyWithoutAgentPolicyId, +} from '../../../../tasks/integrations'; +import { + disableAutoRefresh, openIntegrationsPopover, - waitForRulesTableToShow, - waitForRuleToUpdate, } from '../../../../tasks/alerts_detection_rules'; - -/* -Note that the rule we are using for testing purposes has the following characteristics, changing that may affect the coverage. - -- Single-integration - - Package: system -- Multi-integration package - - Package: aws - - Integration: cloudtrail - - Integration: cloudfront -- Not existing package: - - Package: unknown -- Not existing integration & existing package: - - Package: aws - - Integration: unknown -*/ +import { ruleDetailsUrl } from '../../../../urls/navigation'; +import { enablesRule, waitForPageToBeLoaded } from '../../../../tasks/rule_details'; describe('Related integrations', { tags: ['@ess', '@brokenInServerless'] }, () => { - before(() => { - cleanKibana(); - login(); - importRule('related_integrations.ndjson'); + const DATA_STREAM_NAME = 'logs-related-integrations-test'; + const PREBUILT_RULE_NAME = 'Prebuilt rule with related integrations'; + const RULE_RELATED_INTEGRATIONS: IntegrationDefinition[] = [ + { + package: 'aws', + version: '1.17.0', + integration: 'cloudfront', + installed: true, + enabled: true, + }, + { + package: 'aws', + version: '1.17.0', + integration: 'cloudtrail', + installed: true, + enabled: false, + }, + { package: 'aws', version: '1.17.0', integration: 'unknown', installed: false, enabled: false }, + { package: 'system', version: '1.17.0', installed: true, enabled: true }, + ]; + const PREBUILT_RULE = createRuleAssetSavedObject({ + name: PREBUILT_RULE_NAME, + index: [DATA_STREAM_NAME], + query: '*:*', + rule_id: 'rule_1', + related_integrations: RULE_RELATED_INTEGRATIONS.map((x) => omit(x, ['installed', 'enabled'])), }); - context('integrations not installed', () => { - const rule = { - name: 'Related integrations rule', - integrations: ['Aws Cloudfront', 'Aws Cloudtrail', 'Aws Unknown', 'System'], - enabledIntegrations: '0', - }; - - before(() => { - cleanFleet(); - }); - - beforeEach(() => { - login(); - visit(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToShow(); - }); + beforeEach(() => { + login(); + cleanFleet(); + deleteAlertsAndRules(); + addAndInstallPrebuiltRules([PREBUILT_RULE]); + }); - it('should display a badge with the installed integrations on the rule management page', () => { - cy.get(INTEGRATIONS_POPOVER).should( - 'have.text', - `${rule.enabledIntegrations}/${rule.integrations.length} integrations` - ); - }); + describe('integrations not installed', () => { + describe('rules management table', () => { + beforeEach(() => { + visitSecurityDetectionRulesPage(); + disableAutoRefresh(); + }); - it('should display a popover when clicking the badge with the installed integrations on the rule management page', () => { - openIntegrationsPopover(); + it('should display a badge with the installed integrations', () => { + cy.get(INTEGRATIONS_POPOVER).should( + 'have.text', + `0/${RULE_RELATED_INTEGRATIONS.length} integrations` + ); + }); - cy.get(INTEGRATIONS_POPOVER_TITLE).should( - 'have.text', - `[${rule.integrations.length}] Related integrations available` - ); - cy.get(INTEGRATIONS).should('have.length', rule.integrations.length); - cy.get(INTEGRATIONS_STATUS).should('have.length', rule.integrations.length); + it('should display a popover when clicking the badge with the installed integrations', () => { + openIntegrationsPopover(); - rule.integrations.forEach((integration, index) => { - cy.get(INTEGRATIONS).eq(index).should('contain', integration); - cy.get(INTEGRATIONS_STATUS).eq(index).should('have.text', 'Not installed'); + cy.get(INTEGRATIONS_POPOVER_TITLE).should( + 'have.text', + `[${RULE_RELATED_INTEGRATIONS.length}] Related integrations available` + ); + cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); + + RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { + matchCase: false, + }); + cy.get(INTEGRATION_STATUS).eq(index).should('have.text', 'Not installed'); + }); }); }); - it('should display the integrations on the definition section', () => { - goToTheRuleDetailsOf(rule.name); + describe('rule details', () => { + beforeEach(() => { + visitFirstInstalledPrebuiltRuleDetailsPage(); + }); - cy.get(INTEGRATIONS).should('have.length', rule.integrations.length); - cy.get(INTEGRATIONS_STATUS).should('have.length', rule.integrations.length); + it('should display the integrations in the definition section', () => { + cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); - rule.integrations.forEach((integration, index) => { - cy.get(INTEGRATIONS).eq(index).should('contain', integration); - cy.get(INTEGRATIONS_STATUS).eq(index).should('have.text', 'Not installed'); + RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { + matchCase: false, + }); + cy.get(INTEGRATION_STATUS).eq(index).should('have.text', 'Not installed'); + }); }); }); }); - context.skip( - 'installed integrations: Amazon CloudFront, AWS CloudTrail, System, enabled integrations: Amazon CloudFront, Aws Cloudfront, System', - () => { - const rule = { - name: 'Related integrations rule', - integrations: [ - { name: 'AWS Cloudfront', installed: true, enabled: true }, - { name: 'AWS CloudTrail', installed: true, enabled: false }, - { name: 'Aws Unknown', installed: false, enabled: false }, - { name: 'System', installed: true, enabled: true }, + describe('integrations installed (AWS CloudFront (enabled), AWS CloudTrail (disabled), System (enabled))', () => { + beforeEach(() => { + installIntegrations({ + packages: [ + { name: 'aws', version: '1.17.0' }, + { name: 'system', version: '1.17.0' }, ], - enabledIntegrations: '2', - }; - - before(() => { - cleanFleet().then(() => { - installAwsCloudFrontWithPolicy(); - }); + agentPolicy: { + name: 'Agent policy', + namespace: 'default', + monitoring_enabled: ['logs'], + inactivity_timeout: 1209600, + }, + packagePolicy: AWS_PACKAGE_POLICY, }); + }); + describe('rules management table', () => { beforeEach(() => { - login(); - visit(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToShow(); + visitSecurityDetectionRulesPage(); + disableAutoRefresh(); }); - it('should display a badge with the installed integrations on the rule management page', () => { + it('should display a badge with the installed integrations', () => { + const enabledIntegrations = RULE_RELATED_INTEGRATIONS.filter((x) => x.enabled).length; + const totalIntegrations = RULE_RELATED_INTEGRATIONS.length; + cy.get(INTEGRATIONS_POPOVER).should( 'have.text', - `${rule.enabledIntegrations}/${rule.integrations.length} integrations` + `${enabledIntegrations}/${totalIntegrations} integrations` ); }); - it('should display a popover when clicking the badge with the installed integrations on the rule management page', () => { + it('should display a popover when clicking the badge with the installed integrations', () => { openIntegrationsPopover(); cy.get(INTEGRATIONS_POPOVER_TITLE).should( 'have.text', - `[${rule.integrations.length}] Related integrations available` + `[${RULE_RELATED_INTEGRATIONS.length}] Related integrations available` ); - cy.get(INTEGRATIONS).should('have.length', rule.integrations.length); - cy.get(INTEGRATIONS_STATUS).should('have.length', rule.integrations.length); - - rule.integrations.forEach((integration, index) => { - let expectedStatus = integration.installed ? 'Installed' : 'Not installed'; - if (integration.enabled) expectedStatus += ': enabled'; - - cy.get(INTEGRATIONS).eq(index).should('contain', integration.name); - cy.get(INTEGRATIONS_STATUS).eq(index).should('have.text', expectedStatus); + cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); + + RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { + matchCase: false, + }); + cy.get(INTEGRATION_STATUS) + .eq(index) + .should('have.text', getIntegrationStatus(integration)); }); }); + }); - it('should display the integrations on the definition section', () => { - goToTheRuleDetailsOf(rule.name); - - cy.get(INTEGRATIONS).should('have.length', rule.integrations.length); - cy.get(INTEGRATIONS_STATUS).should('have.length', rule.integrations.length); - - rule.integrations.forEach((integration, index) => { - let expectedStatus = integration.installed ? 'Installed' : 'Not installed'; - if (integration.enabled) expectedStatus += ': enabled'; + // Flaky in serverless tests + // @brokenInServerless tag is not working so a skip was needed + describe.skip('rule details', { tags: ['@brokenInServerless'] }, () => { + beforeEach(() => { + visitFirstInstalledPrebuiltRuleDetailsPage(); + }); - cy.get(INTEGRATIONS).eq(index).should('contain', integration.name); - cy.get(INTEGRATIONS_STATUS).eq(index).should('have.text', expectedStatus); + it('should display the integrations in the definition section', () => { + cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); + + RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { + matchCase: false, + }); + cy.get(INTEGRATION_STATUS) + .eq(index) + .should('have.text', getIntegrationStatus(integration)); }); }); it('the alerts generated should have a "kibana.alert.rule.parameters.related_integrations" field containing the integrations', () => { - const firstRule = 0; - const relatedIntegrationsField = 'kibana.alert.rule.parameters.related_integrations'; - const expectedRelatedIntegrationsText = - '{"package":"system","version":"1.17.0"}{"package":"aws","integration":"cloudtrail","version":"1.17.0"}{"package":"aws","integration":"cloudfront","version":"1.17.0"}{"package":"aws","integration":"unknown","version":"1.17.0"}'; - - enableRule(firstRule); - waitForRuleToUpdate(); - goToTheRuleDetailsOf(rule.name); + const RELATED_INTEGRATION_FIELD = 'kibana.alert.rule.parameters.related_integrations'; + + deleteDataStream(DATA_STREAM_NAME); + createDocument(DATA_STREAM_NAME, generateEvent()); + + waitForPageToBeLoaded(PREBUILT_RULE_NAME); + enablesRule(); waitForAlertsToPopulate(); expandFirstAlert(); openTable(); - filterBy(relatedIntegrationsField); - cy.get(FIELD(relatedIntegrationsField)).should( - 'have.text', - expectedRelatedIntegrationsText - ); + filterBy(RELATED_INTEGRATION_FIELD); + + cy.get(FIELD(RELATED_INTEGRATION_FIELD)) + .invoke('text') + .then((stringValue) => { + // Integrations are displayed in the flyout as a string with a format like so: + // '{"package":"aws","version":"1.17.0","integration":"unknown"}{"package":"mock","version":"1.1.0"}{"package":"system","version":"1.17.0"}' + // We need to parse it to an array of valid objects before we can compare it to the expected value + // Otherwise, the test might fail because of the order of the properties in the objects in the string + const jsonStringArray = stringValue.split('}{'); + + const validJsonStringArray = createValidJsonStringArray(jsonStringArray); + + const parsedIntegrations = validJsonStringArray.map((jsonString) => + JSON.parse(jsonString) + ); + + RULE_RELATED_INTEGRATIONS.forEach((integration) => { + expect(parsedIntegrations).to.deep.include({ + package: integration.package, + version: integration.version, + ...(integration.integration ? { integration: integration.integration } : {}), + }); + }); + }); }); - } - ); - - context('related Integrations Advanced Setting is disabled', () => { - const rule = { - name: 'Related integrations rule', - integrations: ['Aws Cloudfront', 'Aws Cloudtrail', 'Aws Unknown', 'System'], - enabledIntegrations: '0', - }; + }); + }); + describe('related Integrations Advanced Setting is disabled', () => { before(() => { - cleanFleet().then(() => { - disableRelatedIntegrations(); - }); + disableRelatedIntegrations(); }); after(() => { enableRelatedIntegrations(); }); - beforeEach(() => { - login(); - visit(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToShow(); - }); + describe('rules management table', () => { + beforeEach(() => { + visitSecurityDetectionRulesPage(); + disableAutoRefresh(); + }); - it('should not display a badge with the installed integrations on the rule management page', () => { - cy.get(RULE_NAME).should('have.text', rule.name); - cy.get(INTEGRATIONS).should('not.exist'); + it('should not display a badge with the installed integrations', () => { + cy.get(RULE_NAME).should('have.text', PREBUILT_RULE_NAME); + cy.get(INTEGRATION_LINK).should('not.exist'); + }); }); - it('should display the integrations on the definition section', () => { - goToTheRuleDetailsOf(rule.name); + describe('rule details', () => { + beforeEach(() => { + visitFirstInstalledPrebuiltRuleDetailsPage(); + }); - cy.get(INTEGRATIONS).should('have.length', rule.integrations.length); - cy.get(INTEGRATIONS_STATUS).should('have.length', rule.integrations.length); + it('should display the integrations in the definition section', () => { + cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); - rule.integrations.forEach((integration, index) => { - cy.get(INTEGRATIONS).eq(index).should('contain', integration); - cy.get(INTEGRATIONS_STATUS).eq(index).should('have.text', 'Not installed'); + RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { + matchCase: false, + }); + cy.get(INTEGRATION_STATUS).eq(index).should('have.text', 'Not installed'); + }); }); }); }); }); + +const INSTALLED_PREBUILT_RULES_RESPONSE_ALIAS = 'prebuiltRules'; + +function addAndInstallPrebuiltRules(rules: Array): void { + installPrebuiltRuleAssets(rules); + installAllPrebuiltRulesRequest().as(INSTALLED_PREBUILT_RULES_RESPONSE_ALIAS); +} + +function visitFirstInstalledPrebuiltRuleDetailsPage(): void { + cy.get>( + `@${INSTALLED_PREBUILT_RULES_RESPONSE_ALIAS}` + ).then((response) => visitWithoutDateRange(ruleDetailsUrl(response.body.results.created[0].id))); +} + +interface IntegrationDefinition { + package: string; + version: string; + installed: boolean; + enabled: boolean; + integration?: string; +} + +function getIntegrationName(integration: IntegrationDefinition): string { + return `${integration.package} ${integration.integration ?? ''}`.trim(); +} + +function getIntegrationStatus(integration: IntegrationDefinition): string { + return `${integration.installed ? 'Installed' : 'Not installed'}${ + integration.enabled ? ': enabled' : '' + }`.trim(); +} + +/** + * AWS package policy has been generated by Kibana. Instead of copying the whole output the policy below + * contains only required for testing inputs. + */ +const AWS_PACKAGE_POLICY: PackagePolicyWithoutAgentPolicyId = { + package: { + name: 'aws', + version: '1.17.0', + }, + name: 'aws-1', + namespace: 'default', + inputs: { + 'cloudtrail-aws-s3': { + enabled: false, + streams: { + 'aws.cloudtrail': { + enabled: true, + vars: { + fips_enabled: false, + tags: ['forwarded', 'aws-cloudtrail'], + preserve_original_event: false, + cloudtrail_regex: '/CloudTrail/', + cloudtrail_digest_regex: '/CloudTrail-Digest/', + cloudtrail_insight_regex: '/CloudTrail-Insight/', + max_number_of_messages: 5, + }, + }, + }, + }, + 'elb-aws-s3': { + enabled: false, + streams: { + 'aws.elb_logs': { + enabled: true, + vars: { + fips_enabled: false, + tags: ['forwarded', 'aws-elb-logs'], + preserve_original_event: false, + max_number_of_messages: 5, + }, + }, + }, + }, + 'firewall-aws-s3': { + enabled: false, + streams: { + 'aws.firewall_logs': { + enabled: true, + vars: { + fips_enabled: false, + tags: ['forwarded', 'aws-firewall-logs'], + preserve_original_event: false, + max_number_of_messages: 5, + }, + }, + }, + }, + 's3-aws-s3': { + enabled: false, + streams: { + 'aws.s3access': { + enabled: true, + vars: { + fips_enabled: false, + tags: ['forwarded', 'aws-s3access'], + preserve_original_event: false, + max_number_of_messages: 5, + }, + }, + }, + }, + 'waf-aws-s3': { + enabled: false, + streams: { + 'aws.waf': { + enabled: true, + vars: { + fips_enabled: false, + tags: ['forwarded', 'aws-waf'], + preserve_original_event: false, + max_number_of_messages: 5, + }, + }, + }, + }, + 'cloudfront-aws-s3': { + enabled: true, + streams: { + 'aws.cloudfront_logs': { + enabled: true, + vars: { + queue_url: 'https://example.com', + fips_enabled: false, + tags: ['forwarded', 'aws-cloudfront'], + preserve_original_event: false, + max_number_of_messages: 5, + }, + }, + }, + }, + }, +}; + +const createValidJsonStringArray = (jsonStringArray: string[]) => + jsonStringArray.map((jsonString, index) => { + if (index === 0) { + return `${jsonString}}`; + } else if (index === jsonStringArray.length - 1) { + return `{${jsonString}`; + } else { + return `{${jsonString}}`; + } + }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_duplicate_rules.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_duplicate_rules.cy.ts index 447f399c24a20..b95741083a065 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_duplicate_rules.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_duplicate_rules.cy.ts @@ -52,83 +52,88 @@ const EXPIRED_EXCEPTION_ITEM_NAME = 'Sample exception item'; const NON_EXPIRED_EXCEPTION_ITEM_NAME = 'Sample exception item with future expiration'; -describe('Detection rules, bulk duplicate', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cleanKibana(); - }); - - beforeEach(() => { - login(); - // Make sure persisted rules table state is cleared - resetRulesTableState(); - deleteAlertsAndRules(); - cy.task('esArchiverResetKibana'); - createRule( - getNewRule({ name: RULE_NAME, ...defaultRuleData, rule_id: '1', enabled: false }) - ).then((response) => { - createRuleExceptionItem(response.body.id, [ - { - description: 'Exception item for rule default exception list', - entries: [ - { - field: 'user.name', - operator: 'included', - type: 'match', - value: 'some value', - }, - ], - name: EXPIRED_EXCEPTION_ITEM_NAME, - type: 'simple', - expire_time: expiredDate, - }, - { - description: 'Exception item for rule default exception list', - entries: [ - { - field: 'user.name', - operator: 'included', - type: 'match', - value: 'some value', - }, - ], - name: NON_EXPIRED_EXCEPTION_ITEM_NAME, - type: 'simple', - expire_time: futureDate, - }, - ]); +// Flaky on serverless +describe( + 'Detection rules, bulk duplicate', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cleanKibana(); }); - visitSecurityDetectionRulesPage(); - disableAutoRefresh(); - }); - - it('Duplicates rules', () => { - selectAllRules(); - duplicateSelectedRulesWithoutExceptions(); - expectManagementTableRules([`${RULE_NAME} [Duplicate]`]); - }); + beforeEach(() => { + login(); + // Make sure persisted rules table state is cleared + resetRulesTableState(); + deleteAlertsAndRules(); + cy.task('esArchiverResetKibana'); + createRule( + getNewRule({ name: RULE_NAME, ...defaultRuleData, rule_id: '1', enabled: false }) + ).then((response) => { + createRuleExceptionItem(response.body.id, [ + { + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'user.name', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: EXPIRED_EXCEPTION_ITEM_NAME, + type: 'simple', + expire_time: expiredDate, + }, + { + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'user.name', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: NON_EXPIRED_EXCEPTION_ITEM_NAME, + type: 'simple', + expire_time: futureDate, + }, + ]); + }); + + visitSecurityDetectionRulesPage(); + disableAutoRefresh(); + }); - describe('With exceptions', () => { - it('Duplicates rules with expired exceptions', () => { + it('Duplicates rules', () => { selectAllRules(); - duplicateSelectedRulesWithExceptions(); + duplicateSelectedRulesWithoutExceptions(); expectManagementTableRules([`${RULE_NAME} [Duplicate]`]); - goToTheRuleDetailsOf(`${RULE_NAME} [Duplicate]`); - goToExceptionsTab(); - assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [NON_EXPIRED_EXCEPTION_ITEM_NAME]); - viewExpiredExceptionItems(); - assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [EXPIRED_EXCEPTION_ITEM_NAME]); }); - it('Duplicates rules with exceptions, excluding expired exceptions', () => { - selectAllRules(); - duplicateSelectedRulesWithNonExpiredExceptions(); - expectManagementTableRules([`${RULE_NAME} [Duplicate]`]); - goToTheRuleDetailsOf(`${RULE_NAME} [Duplicate]`); - goToExceptionsTab(); - assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [NON_EXPIRED_EXCEPTION_ITEM_NAME]); - viewExpiredExceptionItems(); - assertNumberOfExceptionItemsExists(0); + describe('With exceptions', () => { + it('Duplicates rules with expired exceptions', () => { + selectAllRules(); + duplicateSelectedRulesWithExceptions(); + expectManagementTableRules([`${RULE_NAME} [Duplicate]`]); + goToTheRuleDetailsOf(`${RULE_NAME} [Duplicate]`); + goToExceptionsTab(); + assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [NON_EXPIRED_EXCEPTION_ITEM_NAME]); + viewExpiredExceptionItems(); + assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [EXPIRED_EXCEPTION_ITEM_NAME]); + }); + + it('Duplicates rules with exceptions, excluding expired exceptions', () => { + selectAllRules(); + duplicateSelectedRulesWithNonExpiredExceptions(); + expectManagementTableRules([`${RULE_NAME} [Duplicate]`]); + goToTheRuleDetailsOf(`${RULE_NAME} [Duplicate]`); + goToExceptionsTab(); + assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [NON_EXPIRED_EXCEPTION_ITEM_NAME]); + viewExpiredExceptionItems(); + assertNumberOfExceptionItemsExists(0); + }); }); - }); -}); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts index 3771d4a5858ac..6f5dc717c8607 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts @@ -47,7 +47,8 @@ import { TOOLTIP } from '../../../../../screens/common'; const RULES_TO_IMPORT_FILENAME = 'cypress/fixtures/7_16_rules.ndjson'; -describe('rule snoozing', { tags: ['@ess', '@serverless'] }, () => { +// Flaky in serverless tests +describe('rule snoozing', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_filtering.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_filtering.cy.ts index eaa67b859b6c0..97ccd8494e3dd 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_filtering.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_filtering.cy.ts @@ -28,7 +28,8 @@ import { import { disableAutoRefresh } from '../../../../tasks/alerts_detection_rules'; import { getNewRule } from '../../../../objects/rule'; -describe('Rules table: filtering', { tags: ['@ess', '@serverless'] }, () => { +// Flaky in serverless tests +describe('Rules table: filtering', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); }); @@ -41,8 +42,10 @@ describe('Rules table: filtering', { tags: ['@ess', '@serverless'] }, () => { cy.task('esArchiverResetKibana'); }); - describe('Last response filter', () => { - it('Filters rules by last response', function () { + describe.skip('Last response filter', () => { + // Flaky in serverless tests + // @brokenInServerless tag is not working so a skip was needed + it('Filters rules by last response', { tags: ['@brokenInServerless'] }, function () { deleteIndex('test_index'); createIndex('test_index', { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_links.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_links.cy.ts index d4465760ee97f..d2fbd5433f872 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_links.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_links.cy.ts @@ -12,7 +12,8 @@ import { cleanKibana, deleteAlertsAndRules } from '../../../../tasks/common'; import { login, visitWithoutDateRange } from '../../../../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../../urls/navigation'; -describe('Rules table: links', { tags: ['@ess', '@serverless'] }, () => { +// Flaky in serverless tests +describe('Rules table: links', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_persistent_state.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_persistent_state.cy.ts index 7bf4d9e6eef24..675fa704f5430 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_persistent_state.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_persistent_state.cy.ts @@ -110,167 +110,172 @@ describe('Rules table: persistent state', { tags: ['@ess', '@serverless'] }, () resetRulesTableState(); }); - describe('while on a happy path', () => { - it('activates management tab by default', () => { - visit(SECURITY_DETECTIONS_RULES_URL); + // Flaky on serverless + describe( + 'while on a happy path', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + it('activates management tab by default', () => { + visit(SECURITY_DETECTIONS_RULES_URL); - expectRulesManagementTab(); - }); - - it('leads to displaying a rule according to the specified filters', () => { - visitRulesTableWithState({ - searchTerm: 'rule', - tags: ['tag-b'], - source: 'custom', - enabled: false, - field: 'name', - order: 'asc', - perPage: 5, - page: 2, - }); - - expectManagementTableRules(['rule 6']); - }); - - it('loads from the url', () => { - visitRulesTableWithState({ - searchTerm: 'rule', - tags: ['tag-b'], - source: 'custom', - enabled: false, - field: 'name', - order: 'asc', - perPage: 5, - page: 2, + expectRulesManagementTab(); }); - expectRulesManagementTab(); - expectFilterSearchTerm('rule'); - expectFilterByTags(['tag-b']); - expectFilterByCustomRules(); - expectFilterByDisabledRules(); - expectTableSorting('Rule', 'asc'); - expectRowsPerPage(5); - expectTablePage(2); - }); + it('leads to displaying a rule according to the specified filters', () => { + visitRulesTableWithState({ + searchTerm: 'rule', + tags: ['tag-b'], + source: 'custom', + enabled: false, + field: 'name', + order: 'asc', + perPage: 5, + page: 2, + }); - it('loads from the session storage', () => { - setStorageState({ - searchTerm: 'test', - tags: ['tag-a'], - source: 'prebuilt', - enabled: true, - field: 'severity', - order: 'desc', - perPage: 10, + expectManagementTableRules(['rule 6']); }); - visit(SECURITY_DETECTIONS_RULES_MANAGEMENT_URL); - - expectRulesManagementTab(); - expectFilterSearchTerm('test'); - expectFilterByTags(['tag-a']); - expectFilterByPrebuiltRules(); - expectFilterByEnabledRules(); - expectTableSorting('Severity', 'desc'); - }); - - it('prefers url state over storage state', () => { - setStorageState({ - searchTerm: 'test', - tags: ['tag-c'], - source: 'prebuilt', - enabled: true, - field: 'severity', - order: 'desc', - perPage: 10, - }); + it('loads from the url', () => { + visitRulesTableWithState({ + searchTerm: 'rule', + tags: ['tag-b'], + source: 'custom', + enabled: false, + field: 'name', + order: 'asc', + perPage: 5, + page: 2, + }); - visitRulesTableWithState({ - searchTerm: 'rule', - tags: ['tag-b'], - source: 'custom', - enabled: false, - field: 'name', - order: 'asc', - perPage: 5, - page: 2, + expectRulesManagementTab(); + expectFilterSearchTerm('rule'); + expectFilterByTags(['tag-b']); + expectFilterByCustomRules(); + expectFilterByDisabledRules(); + expectTableSorting('Rule', 'asc'); + expectRowsPerPage(5); + expectTablePage(2); }); - expectRulesManagementTab(); - expectRulesTableState(); - expectTablePage(2); - }); + it('loads from the session storage', () => { + setStorageState({ + searchTerm: 'test', + tags: ['tag-a'], + source: 'prebuilt', + enabled: true, + field: 'severity', + order: 'desc', + perPage: 10, + }); - describe('and on the rules management tab', () => { - beforeEach(() => { - login(); visit(SECURITY_DETECTIONS_RULES_MANAGEMENT_URL); + + expectRulesManagementTab(); + expectFilterSearchTerm('test'); + expectFilterByTags(['tag-a']); + expectFilterByPrebuiltRules(); + expectFilterByEnabledRules(); + expectTableSorting('Severity', 'desc'); }); - it('persists after reloading the page', () => { - changeRulesTableState(); - goToTablePage(2); + it('prefers url state over storage state', () => { + setStorageState({ + searchTerm: 'test', + tags: ['tag-c'], + source: 'prebuilt', + enabled: true, + field: 'severity', + order: 'desc', + perPage: 10, + }); - cy.reload(); + visitRulesTableWithState({ + searchTerm: 'rule', + tags: ['tag-b'], + source: 'custom', + enabled: false, + field: 'name', + order: 'asc', + perPage: 5, + page: 2, + }); expectRulesManagementTab(); expectRulesTableState(); expectTablePage(2); }); - it('persists after navigating back from a rule details page', () => { - changeRulesTableState(); - goToTablePage(2); + describe('and on the rules management tab', () => { + beforeEach(() => { + login(); + visit(SECURITY_DETECTIONS_RULES_MANAGEMENT_URL); + }); - goToRuleDetails(); - cy.go('back'); + it('persists after reloading the page', () => { + changeRulesTableState(); + goToTablePage(2); - expectRulesManagementTab(); - expectRulesTableState(); - expectTablePage(2); - }); + cy.reload(); - it('persists after navigating to another page inside Security Solution', () => { - changeRulesTableState(); - goToTablePage(2); + expectRulesManagementTab(); + expectRulesTableState(); + expectTablePage(2); + }); - visit(DASHBOARDS_URL); - visit(SECURITY_DETECTIONS_RULES_MANAGEMENT_URL); + it('persists after navigating back from a rule details page', () => { + changeRulesTableState(); + goToTablePage(2); - expectRulesManagementTab(); - expectRulesTableState(); - expectTablePage(1); - }); + goToRuleDetails(); + cy.go('back'); - it('persists after navigating to another page outside Security Solution', () => { - changeRulesTableState(); - goToTablePage(2); + expectRulesManagementTab(); + expectRulesTableState(); + expectTablePage(2); + }); - visit(KIBANA_HOME); - visit(SECURITY_DETECTIONS_RULES_MANAGEMENT_URL); + it('persists after navigating to another page inside Security Solution', () => { + changeRulesTableState(); + goToTablePage(2); - expectRulesManagementTab(); - expectRulesTableState(); - expectTablePage(1); - }); - }); + visit(DASHBOARDS_URL); + visit(SECURITY_DETECTIONS_RULES_MANAGEMENT_URL); - describe('and on the rules monitoring tab', () => { - beforeEach(() => { - login(); - visit(SECURITY_DETECTIONS_RULES_MONITORING_URL); + expectRulesManagementTab(); + expectRulesTableState(); + expectTablePage(1); + }); + + it('persists after navigating to another page outside Security Solution', () => { + changeRulesTableState(); + goToTablePage(2); + + visit(KIBANA_HOME); + visit(SECURITY_DETECTIONS_RULES_MANAGEMENT_URL); + + expectRulesManagementTab(); + expectRulesTableState(); + expectTablePage(1); + }); }); - it('persists the selected tab', () => { - changeRulesTableState(); + describe('and on the rules monitoring tab', () => { + beforeEach(() => { + login(); + visit(SECURITY_DETECTIONS_RULES_MONITORING_URL); + }); - cy.reload(); + it('persists the selected tab', () => { + changeRulesTableState(); - expectRulesMonitoringTab(); + cy.reload(); + + expectRulesMonitoringTab(); + }); }); - }); - }); + } + ); describe('upon state format upgrade', async () => { beforeEach(() => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_selection.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_selection.cy.ts index ae28214315a40..782e70dec8379 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_selection.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rules_table/rules_table_selection.cy.ts @@ -33,7 +33,8 @@ const RULE_2 = createRuleAssetSavedObject({ rule_id: 'rule_2', }); -describe('Rules table: selection', { tags: ['@ess', '@serverless'] }, () => { +// FLAKY: https://github.com/elastic/kibana/issues/165643 +describe.skip('Rules table: selection', { tags: ['@ess', '@serverless'] }, () => { before(() => { cleanKibana(); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts index 3aa68a9f8029a..ad104df5789ca 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts @@ -50,7 +50,8 @@ describe('value lists', () => { closeValueListsModal(); }); - describe('create list types', () => { + // Flaky in serverless tests + describe('create list types', { tags: ['@brokenInServerless'] }, () => { beforeEach(() => { openValueListsModal(); }); @@ -108,7 +109,8 @@ describe('value lists', () => { }); }); - describe('delete list types', () => { + // Flaky in serverless tests + describe('delete list types', { tags: ['@brokenInServerless'] }, () => { it('deletes a "keyword" list from an uploaded file', () => { importValueList(TEXT_LIST_FILE_NAME, 'keyword'); openValueListsModal(); @@ -154,7 +156,8 @@ describe('value lists', () => { }); }); - describe('export list types', () => { + // Flaky in serverless tests + describe('export list types', { tags: ['@brokenInServerless'] }, () => { it('exports a "keyword" list from an uploaded file', () => { cy.intercept('POST', `/api/lists/items/_export?list_id=${TEXT_LIST_FILE_NAME}`).as( 'exportList' diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts index 920ddd62909b3..e9c07294e62d3 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts @@ -17,10 +17,7 @@ import { login, visitWithoutDateRange } from '../../../tasks/login'; import { getEndpointRule } from '../../../objects/rule'; import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; import { createRule } from '../../../tasks/api_calls/rules'; -import { - waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, -} from '../../../tasks/create_new_rule'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; import { addExceptionEntryFieldValueAndSelectSuggestion, @@ -38,7 +35,7 @@ import { EXCEPTION_CARD_ITEM_NAME, EXCEPTION_ITEM_VIEWER_CONTAINER, } from '../../../screens/exceptions'; -import { goToEndpointExceptionsTab } from '../../../tasks/rule_details'; +import { goToEndpointExceptionsTab, waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; // See https://github.com/elastic/kibana/issues/163967 describe.skip( diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/entry/use_value_list.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/entry/use_value_list.cy.ts index e45484e5fb834..b1e2c81b4c420 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/entry/use_value_list.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/entry/use_value_list.cy.ts @@ -50,83 +50,88 @@ const goToRulesAndOpenValueListModal = () => { openValueListsModal(); }; -describe('Use Value list in exception entry', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cleanKibana(); - login(); - cy.task('esArchiverLoad', { archiveName: 'exceptions' }); - createRule({ - ...getNewRule(), - query: 'user.name:*', - index: ['exceptions*'], - exceptions_list: [], - rule_id: '2', - }); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - }); - beforeEach(() => { - createListsIndex(); - }); - - afterEach(() => { - cy.task('esArchiverUnload', 'exceptions'); - }); - - it('Should use value list in exception entry, and validate deleting value list prompt', () => { - const ITEM_NAME = 'Exception item with value list'; - const ITEM_FIELD = 'agent.name'; - - goToRulesAndOpenValueListModal(); - - // Add new value list of type keyword - const listName = 'value_list.txt'; - selectValueListType('keyword'); - selectValueListsFile(listName); - uploadValueList(); - - cy.get(VALUE_LISTS_TABLE) - .find(VALUE_LISTS_ROW) - .should(($row) => { - expect($row.text()).to.contain(listName); - expect($row.text()).to.contain('Keywords'); +// Flaky on serverless +describe( + 'Use Value list in exception entry', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cleanKibana(); + login(); + cy.task('esArchiverLoad', { archiveName: 'exceptions' }); + createRule({ + ...getNewRule(), + query: 'user.name:*', + index: ['exceptions*'], + exceptions_list: [], + rule_id: '2', }); - closeValueListsModal(); - goToRuleDetails(); - goToExceptionsTab(); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + }); + beforeEach(() => { + createListsIndex(); + }); + + afterEach(() => { + cy.task('esArchiverUnload', 'exceptions'); + }); - // open add exception modal - openExceptionFlyoutFromEmptyViewerPrompt(); + it('Should use value list in exception entry, and validate deleting value list prompt', () => { + const ITEM_NAME = 'Exception item with value list'; + const ITEM_FIELD = 'agent.name'; - // add exception item name - addExceptionFlyoutItemName(ITEM_NAME); + goToRulesAndOpenValueListModal(); - addExceptionEntryFieldValue(ITEM_FIELD, 0); - addExceptionEntryOperatorValue('is in list', 0); + // Add new value list of type keyword + const listName = 'value_list.txt'; + selectValueListType('keyword'); + selectValueListsFile(listName); + uploadValueList(); - addExceptionEntryFieldMatchIncludedValue('value_list.txt', 0); + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).to.contain(listName); + expect($row.text()).to.contain('Keywords'); + }); + closeValueListsModal(); + goToRuleDetails(); + goToExceptionsTab(); - // The Close all alerts that match attributes in this exception option is disabled - cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); - cy.get(CLOSE_ALERTS_CHECKBOX).should('have.attr', 'disabled'); + // open add exception modal + openExceptionFlyoutFromEmptyViewerPrompt(); - // Create exception - submitNewExceptionItem(); + // add exception item name + addExceptionFlyoutItemName(ITEM_NAME); - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME); - cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should( - 'have.text', - ` ${ITEM_FIELD}included in value_list.txt` - ); + addExceptionEntryFieldValue(ITEM_FIELD, 0); + addExceptionEntryOperatorValue('is in list', 0); - // Go back to value list to delete the existing one - goToRulesAndOpenValueListModal(); + addExceptionEntryFieldMatchIncludedValue('value_list.txt', 0); - deleteValueListsFile(listName); + // The Close all alerts that match attributes in this exception option is disabled + cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); + cy.get(CLOSE_ALERTS_CHECKBOX).should('have.attr', 'disabled'); - // Toast should be shown because of exception reference - cy.get(EXCEPTIONS_TABLE_MODAL).should('exist'); - }); -}); + // Create exception + submitNewExceptionItem(); + + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should( + 'have.text', + ` ${ITEM_FIELD}included in value_list.txt` + ); + + // Go back to value list to delete the existing one + goToRulesAndOpenValueListModal(); + + deleteValueListsFile(listName); + + // Toast should be shown because of exception reference + cy.get(EXCEPTIONS_TABLE_MODAL).should('exist'); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/duplicate_lists.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/duplicate_lists.cy.ts index 33fd21bceddcc..b296ff05e3b5b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/duplicate_lists.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/duplicate_lists.cy.ts @@ -40,7 +40,8 @@ const getExceptionList2 = () => ({ list_id: 'exception_list_2', }); -describe('Duplicate List', { tags: ['@ess', '@serverless'] }, () => { +// Flaky in serverless tests +describe('Duplicate List', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { beforeEach(() => { cy.task('esArchiverResetKibana'); login(); @@ -64,9 +65,7 @@ describe('Duplicate List', { tags: ['@ess', '@serverless'] }, () => { ); // Create exception list not used by any rules - createExceptionList(getExceptionList1(), getExceptionList1().list_id).as( - 'exceptionListResponse' - ); + createExceptionList(getExceptionList1(), getExceptionList1().list_id); // Create exception list associated with a rule createExceptionList(getExceptionList2(), getExceptionList2().list_id); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/filter_table.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/filter_table.cy.ts index 2d74476d41a1f..8fc3acdb2c7ec 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/filter_table.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/filter_table.cy.ts @@ -56,9 +56,7 @@ describe('Filter Lists', { tags: ['@ess', '@serverless'] }, () => { ); // Create exception list not used by any rules - createExceptionList(getExceptionList1(), getExceptionList1().list_id).as( - 'exceptionListResponse' - ); + createExceptionList(getExceptionList1(), getExceptionList1().list_id); login(); visitWithoutDateRange(EXCEPTIONS_URL); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/import_lists.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/import_lists.cy.ts index ac9c0be83b5c1..ac10e916d762f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/import_lists.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/import_lists.cy.ts @@ -20,7 +20,8 @@ import { import { login, visitWithoutDateRange } from '../../../../tasks/login'; import { EXCEPTIONS_URL } from '../../../../urls/navigation'; -describe('Import Lists', { tags: ['@ess', '@serverless'] }, () => { +// Flaky in serverless +describe('Import Lists', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { const LIST_TO_IMPORT_FILENAME = 'cypress/fixtures/7_16_exception_list.ndjson'; before(() => { cy.task('esArchiverResetKibana'); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/manage_lists.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/manage_lists.cy.ts index 59488f6a92132..238d39bcd6e17 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/manage_lists.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/manage_lists.cy.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { getExceptionList, expectedExportedExceptionList } from '../../../../objects/exception'; +import { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { expectedExportedExceptionList, getExceptionList } from '../../../../objects/exception'; import { getNewRule } from '../../../../objects/rule'; import { createRule } from '../../../../tasks/api_calls/rules'; @@ -13,13 +14,13 @@ import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../.. import { EXCEPTIONS_URL } from '../../../../urls/navigation'; import { + assertNumberLinkedRules, + createSharedExceptionList, deleteExceptionListWithoutRuleReferenceByListId, deleteExceptionListWithRuleReferenceByListId, exportExceptionList, - waitForExceptionsTableToBeLoaded, - createSharedExceptionList, linkRulesToExceptionList, - assertNumberLinkedRules, + waitForExceptionsTableToBeLoaded, } from '../../../../tasks/exceptions_table'; import { EXCEPTIONS_LIST_MANAGEMENT_NAME, @@ -44,12 +45,15 @@ const getExceptionList2 = () => ({ list_id: 'exception_list_2', }); +let exceptionListResponse: Cypress.Response; + describe( 'Manage lists from "Shared Exception Lists" page', { tags: ['@ess', '@serverless'] }, () => { describe('Create/Export/Delete List', () => { before(() => { + cy.task('esArchiverResetKibana'); createRule(getNewRule({ name: 'Another rule' })); // Create exception list associated with a rule @@ -69,9 +73,9 @@ describe( ); // Create exception list not used by any rules - createExceptionList(getExceptionList1(), getExceptionList1().list_id).as( - 'exceptionListResponse' - ); + createExceptionList(getExceptionList1(), getExceptionList1().list_id).then((response) => { + exceptionListResponse = response; + }); }); beforeEach(() => { @@ -88,7 +92,7 @@ describe( cy.wait('@export').then(({ response }) => { cy.wrap(response?.body).should( 'eql', - expectedExportedExceptionList(this.exceptionListResponse) + expectedExportedExceptionList(exceptionListResponse) ); cy.get(TOASTER).should( diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts index 4762c88c551c1..bb84c0a8004e4 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts @@ -24,7 +24,7 @@ const loadDetectionsPage = (role: ROLES) => { waitForAlertsToPopulate(); }; -describe('Alerts timeline', { tags: '@ess' }, () => { +describe('Alerts timeline', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { // First we login as a privileged user to create alerts. cleanKibana(); @@ -34,7 +34,7 @@ describe('Alerts timeline', { tags: '@ess' }, () => { waitForAlertsToPopulate(); }); - context('Privileges: read only', { tags: '@ess' }, () => { + context('Privileges: read only', () => { beforeEach(() => { loadDetectionsPage(ROLES.reader); }); @@ -52,7 +52,7 @@ describe('Alerts timeline', { tags: '@ess' }, () => { }); }); - context('Privileges: can crud', { tags: '@ess' }, () => { + context('Privileges: can crud', () => { beforeEach(() => { loadDetectionsPage(ROLES.platform_engineer); cy.get(LOADING_INDICATOR).should('not.exist'); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts index ee654719a4390..4de348c5bbb4a 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts @@ -53,7 +53,7 @@ import { loginWithUser, visit, visitWithoutDateRange } from '../../../tasks/logi import { CASES_URL, OVERVIEW_URL } from '../../../urls/navigation'; -describe('Cases', { tags: ['@ess', '@serverless'] }, () => { +describe('Cases', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); createTimeline(getCase1().timeline).then((response) => diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/privileges.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/privileges.cy.ts index 57000344f119f..e4d35947d142c 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/privileges.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/privileges.cy.ts @@ -48,7 +48,7 @@ const testCase: TestCaseWithoutTimeline = { owner: 'securitySolution', }; -describe('Cases privileges', { tags: '@ess' }, () => { +describe('Cases privileges', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); createUsersAndRoles(usersToCreate, rolesToCreate); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/enable_risk_score.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/enable_risk_score.cy.ts index e509125fb7ab5..47a9218981ba7 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/enable_risk_score.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/enable_risk_score.cy.ts @@ -55,7 +55,7 @@ describe('Enable risk scores', { tags: ['@ess', '@serverless'] }, () => { cy.get(ENABLE_HOST_RISK_SCORE_BUTTON).should('exist'); }); - it('should install host risk score successfully', () => { + it('should install host risk score successfully', { tags: ['@brokenInServerless'] }, () => { interceptInstallRiskScoreModule(); clickEnableRiskScore(RiskScoreEntity.host); waitForInstallRiskScoreModule(); @@ -89,7 +89,7 @@ describe('Enable risk scores', { tags: ['@ess', '@serverless'] }, () => { cy.get(ENABLE_USER_RISK_SCORE_BUTTON).should('exist'); }); - it('should install user risk score successfully', () => { + it('should install user risk score successfully', { tags: ['@brokenInServerless'] }, () => { interceptInstallRiskScoreModule(); clickEnableRiskScore(RiskScoreEntity.user); waitForInstallRiskScoreModule(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/entity_analytics_serverless_splash_screen.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/entity_analytics_serverless_splash_screen.cy.ts new file mode 100644 index 0000000000000..7ae72fa4e3f4d --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/entity_analytics_serverless_splash_screen.cy.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { login, visit } from '../../../tasks/login'; + +import { ENTITY_ANALYTICS_URL } from '../../../urls/navigation'; + +import { PAYWALL_DESCRIPTION } from '../../../screens/entity_analytics_serverless_splash'; + +describe( + 'Entity Analytics Dashboard in Serverless', + { + tags: '@serverless', + env: { + ftrConfig: { + productTypes: [ + { product_line: 'security', product_tier: 'essentials' }, + { product_line: 'endpoint', product_tier: 'essentials' }, + ], + }, + }, + }, + () => { + beforeEach(() => { + login(); + visit(ENTITY_ANALYTICS_URL); + }); + + it('should display a splash screen when visited with Security essentials PLI ', () => { + cy.get(PAYWALL_DESCRIPTION).should( + 'have.text', + 'Entity risk scoring capability is available in our Security Complete license tier' + ); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/upgrade_risk_score.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/upgrade_risk_score.cy.ts index 0d327b36f5179..5b8ba030ade12 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/upgrade_risk_score.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/upgrade_risk_score.cy.ts @@ -38,7 +38,8 @@ import { ENTITY_ANALYTICS_URL } from '../../../urls/navigation'; const spaceId = 'default'; -describe('Upgrade risk scores', { tags: ['@ess', '@serverless'] }, () => { +// Flaky on serverless +describe('Upgrade risk scores', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); login(); @@ -59,31 +60,39 @@ describe('Upgrade risk scores', { tags: ['@ess', '@serverless'] }, () => { cy.get(UPGRADE_USER_RISK_SCORE_BUTTON).should('be.visible'); }); - it('should show a confirmation modal for upgrading host risk score and display a link to host risk score Elastic doc', () => { - clickUpgradeRiskScore(RiskScoreEntity.host); - cy.get(UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.host)).should('exist'); - - cy.get(UPGRADE_CANCELLATION_BUTTON) - .get(`${UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.host)} a`) - .then((link) => { - expect(link.prop('href')).to.eql( - `https://www.elastic.co/guide/en/security/current/${RiskScoreEntity.host}-risk-score.html` - ); - }); - }); + it( + 'should show a confirmation modal for upgrading host risk score and display a link to host risk score Elastic doc', + { tags: ['@brokenInServerless'] }, + () => { + clickUpgradeRiskScore(RiskScoreEntity.host); + cy.get(UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.host)).should('exist'); - it('should show a confirmation modal for upgrading user risk score and display a link to user risk score Elastic doc', () => { - clickUpgradeRiskScore(RiskScoreEntity.user); - cy.get(UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.user)).should('exist'); + cy.get(UPGRADE_CANCELLATION_BUTTON) + .get(`${UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.host)} a`) + .then((link) => { + expect(link.prop('href')).to.eql( + `https://www.elastic.co/guide/en/security/current/${RiskScoreEntity.host}-risk-score.html` + ); + }); + } + ); + + it( + 'should show a confirmation modal for upgrading user risk score and display a link to user risk score Elastic doc', + { tags: ['@brokenInServerless'] }, + () => { + clickUpgradeRiskScore(RiskScoreEntity.user); + cy.get(UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.user)).should('exist'); - cy.get(UPGRADE_CANCELLATION_BUTTON) - .get(`${UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.user)} a`) - .then((link) => { - expect(link.prop('href')).to.eql( - `https://www.elastic.co/guide/en/security/current/${RiskScoreEntity.user}-risk-score.html` - ); - }); - }); + cy.get(UPGRADE_CANCELLATION_BUTTON) + .get(`${UPGRADE_CONFIRMATION_MODAL(RiskScoreEntity.user)} a`) + .then((link) => { + expect(link.prop('href')).to.eql( + `https://www.elastic.co/guide/en/security/current/${RiskScoreEntity.user}-risk-score.html` + ); + }); + } + ); }); const versions: Array<'8.3' | '8.4'> = ['8.3', '8.4']; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/host_details/risk_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/host_details/risk_tab.cy.ts index aed7de2110ba6..f534145c48333 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/host_details/risk_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/host_details/risk_tab.cy.ts @@ -10,9 +10,10 @@ import { login, visitHostDetailsPage } from '../../../tasks/login'; import { cleanKibana, waitForTableToLoad } from '../../../tasks/common'; import { TABLE_CELL, TABLE_ROWS } from '../../../screens/alerts_details'; -describe('risk tab', { tags: ['@ess', '@serverless'] }, () => { +describe('risk tab', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); + // illegal_argument_exception: unknown setting [index.lifecycle.rollover_alias] cy.task('esArchiverLoad', { archiveName: 'risk_hosts' }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/host_risk_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/host_risk_tab.cy.ts index 1a64f741ebeff..9f22ece6865e6 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/host_risk_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/host_risk_tab.cy.ts @@ -53,7 +53,8 @@ describe('risk tab', { tags: ['@ess', '@brokenInServerless'] }, () => { removeCriticalFilterAndCloseRiskTableFilter(); }); - it('should be able to change items count per page', () => { + // Flaky + it.skip('should be able to change items count per page', () => { selectFiveItemsPerPageOption(); cy.get(HOST_BY_RISK_TABLE_HOSTNAME_CELL).should('have.length', 5); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/hosts_risk_column.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/hosts_risk_column.cy.ts index 77593f429b1d4..798870515bad1 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/hosts_risk_column.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/hosts_risk_column.cy.ts @@ -12,9 +12,10 @@ import { cleanKibana } from '../../../tasks/common'; import { TABLE_CELL } from '../../../screens/alerts_details'; import { kqlSearch } from '../../../tasks/security_header'; -describe('All hosts table', { tags: ['@ess', '@serverless'] }, () => { +describe('All hosts table', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); + // illegal_argument_exception: unknown setting [index.lifecycle.name] cy.task('esArchiverLoad', { archiveName: 'risk_hosts' }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/inspect/inspect_button.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/inspect/inspect_button.cy.ts index fd9fc56c95fc4..216a4087e1403 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/inspect/inspect_button.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/inspect/inspect_button.cy.ts @@ -22,8 +22,9 @@ import { selectDataView } from '../../tasks/sourcerer'; const DATA_VIEW = 'auditbeat-*'; -describe('Inspect Explore pages', { tags: ['@ess', '@serverless'] }, () => { +describe('Inspect Explore pages', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { + // illegal_argument_exception: unknown setting [index.lifecycle.name] cy.task('esArchiverLoad', { archiveName: 'risk_users' }); cy.task('esArchiverLoad', { archiveName: 'risk_hosts' }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alert_table_action_column.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alert_table_action_column.cy.ts index 975fdb3ee1d59..2b3232ada9ec4 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alert_table_action_column.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alert_table_action_column.cy.ts @@ -15,31 +15,37 @@ import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; import { login, visit } from '../../../tasks/login'; import { ALERTS_URL } from '../../../urls/navigation'; -describe('Alerts Table Action column', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cleanKibana(); - cy.task('esArchiverLoad', { - archiveName: 'process_ancestry', +describe( + 'Alerts Table Action column', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cleanKibana(); + cy.task('esArchiverLoad', { + archiveName: 'process_ancestry', + useCreate: true, + docsOnly: true, + }); }); - }); - beforeEach(() => { - login(); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - }); + beforeEach(() => { + login(); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); - after(() => { - cy.task('esArchiverUnload', 'process_ancestry'); - }); + after(() => { + cy.task('esArchiverUnload', 'process_ancestry'); + }); - it('should have session viewer button visible & open session viewer on click', () => { - openSessionViewerFromAlertTable(); - cy.get(OVERLAY_CONTAINER).should('be.visible'); - }); + it('should have session viewer button visible & open session viewer on click', () => { + openSessionViewerFromAlertTable(); + cy.get(OVERLAY_CONTAINER).should('be.visible'); + }); - it('should have analyzer button visible & open analyzer on click', () => { - openAnalyzerForFirstAlertInTimeline(); - cy.get(OVERLAY_CONTAINER).should('be.visible'); - }); -}); + it('should have analyzer button visible & open analyzer on click', () => { + openAnalyzerForFirstAlertInTimeline(); + cy.get(OVERLAY_CONTAINER).should('be.visible'); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_details.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_details.cy.ts index 61bfbc8ee0958..1604d1fdbe692 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_details.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_details.cy.ts @@ -36,7 +36,7 @@ import { ALERT_SUMMARY_SEVERITY_DONUT_CHART } from '../../../screens/alerts'; import { getLocalstorageEntryAsObject } from '../../../helpers/common'; import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; -describe('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => { +describe('Alert details flyout', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { describe('Basic functions', () => { beforeEach(() => { cleanKibana(); @@ -135,7 +135,7 @@ describe('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => { describe('Url state management', () => { before(() => { cleanKibana(); - cy.task('esArchiverLoad', { archiveName: 'query_alert' }); + cy.task('esArchiverLoad', { archiveName: 'query_alert', useCreate: true, docsOnly: true }); }); beforeEach(() => { @@ -181,7 +181,7 @@ describe('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => { describe('Localstorage management', () => { before(() => { cleanKibana(); - cy.task('esArchiverLoad', { archiveName: 'query_alert' }); + cy.task('esArchiverLoad', { archiveName: 'query_alert', useCreate: true, docsOnly: true }); }); beforeEach(() => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/building_block_alerts.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/building_block_alerts.cy.ts index 0ed530adad451..d57ab19a0f4fe 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/building_block_alerts.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/building_block_alerts.cy.ts @@ -12,11 +12,9 @@ import { OVERVIEW } from '../../../screens/security_header'; import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; import { createRule } from '../../../tasks/api_calls/rules'; import { cleanKibana } from '../../../tasks/common'; -import { - waitForAlertsToPopulate, - waitForTheRuleToBeExecuted, -} from '../../../tasks/create_new_rule'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; import { navigateFromHeaderTo } from '../../../tasks/security_header'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts index 0e702f5ae720e..db3e8d0dde7fa 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts @@ -47,7 +47,7 @@ describe( cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_THREAT_INTELLIGENCE_BUTTON) .should('be.visible') - .and('have.text', 'Threat Intelligence'); + .and('have.text', 'Threat intelligence'); cy.get(INDICATOR_MATCH_ENRICHMENT_SECTION).should('be.visible'); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_json_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_json_tab.cy.ts index 01c11a671cb64..193de317bed54 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_json_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_json_tab.cy.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { scrollWithinDocumentDetailsExpandableFlyoutRightSection } from '../../../../tasks/expandable_flyout/alert_details_right_panel_json_tab'; import { openJsonTab } from '../../../../tasks/expandable_flyout/alert_details_right_panel'; import { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_flyout/common'; import { DOCUMENT_DETAILS_FLYOUT_JSON_TAB_CONTENT } from '../../../../screens/expandable_flyout/alert_details_right_panel_json_tab'; @@ -31,10 +30,7 @@ describe( }); it('should display the json component', () => { - // the json component is rendered within a dom element with overflow, so Cypress isn't finding it - // this next line is a hack that vertically scrolls down to ensure Cypress finds it - scrollWithinDocumentDetailsExpandableFlyoutRightSection(0, 7000); - cy.get(DOCUMENT_DETAILS_FLYOUT_JSON_TAB_CONTENT).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_JSON_TAB_CONTENT).should('exist'); }); } ); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index dfd09cf4ca117..969934eb5fee1 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -101,7 +101,7 @@ describe( .within(() => { cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON) .should('be.visible') - .and('have.text', 'Rule summary'); + .and('have.text', 'Show rule summary'); }); cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS) .should('be.visible') @@ -257,7 +257,7 @@ describe( ).scrollIntoView(); cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER) .should('be.visible') - .and('have.text', 'Threat Intelligence'); + .and('have.text', 'Threat intelligence'); cy.get( DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT ).scrollIntoView(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts index 22a80ba6fda9c..59df434ff0290 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts @@ -27,76 +27,80 @@ import { } from '../../../screens/alerts_details'; import { verifyInsightCount } from '../../../tasks/alerts_details'; -describe('Investigate in timeline', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cleanKibana(); - createRule(getNewRule()); - }); - - describe('From alerts table', () => { - beforeEach(() => { - login(); - visit(ALERTS_URL); - waitForAlertsToPopulate(); +describe( + 'Investigate in timeline', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cleanKibana(); + createRule(getNewRule()); }); - it('should open new timeline from alerts table', () => { - investigateFirstAlertInTimeline(); - cy.get(PROVIDER_BADGE) - .first() - .invoke('text') - .then((eventId) => { - cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', eventId); - }); - }); - }); + describe('From alerts table', () => { + beforeEach(() => { + login(); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); - describe('From alerts details flyout', () => { - beforeEach(() => { - login(); - disableExpandableFlyout(); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlert(); + it('should open new timeline from alerts table', () => { + investigateFirstAlertInTimeline(); + cy.get(PROVIDER_BADGE) + .first() + .invoke('text') + .then((eventId) => { + cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', eventId); + }); + }); }); - it('should open a new timeline from a prevalence field', () => { - // Only one alert matches the exact process args in this case - const alertCount = 1; + describe('From alerts details flyout', () => { + beforeEach(() => { + login(); + disableExpandableFlyout(); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlert(); + }); + + it('should open a new timeline from a prevalence field', () => { + // Only one alert matches the exact process args in this case + const alertCount = 1; - // Click on the last button that lets us investigate in timeline. - // We expect this to be the `process.args` row. - cy.get(ALERT_FLYOUT) - .find(SUMMARY_VIEW_INVESTIGATE_IN_TIMELINE_BUTTON) - .last() - .should('have.text', alertCount) - .click(); + // Click on the last button that lets us investigate in timeline. + // We expect this to be the `process.args` row. + cy.get(ALERT_FLYOUT) + .find(SUMMARY_VIEW_INVESTIGATE_IN_TIMELINE_BUTTON) + .last() + .should('have.text', alertCount) + .click(); - // Make sure a new timeline is created and opened - cy.get(TIMELINE_TITLE).should('have.text', 'Untitled timeline'); + // Make sure a new timeline is created and opened + cy.get(TIMELINE_TITLE).should('have.text', 'Untitled timeline'); - // The alert count in this timeline should match the count shown on the alert flyout - cy.get(QUERY_TAB_BUTTON).should('contain.text', alertCount); + // The alert count in this timeline should match the count shown on the alert flyout + cy.get(QUERY_TAB_BUTTON).should('contain.text', alertCount); - // The correct filter is applied to the timeline query - cy.get(FILTER_BADGE).should( - 'have.text', - ' {"bool":{"must":[{"term":{"process.args":"-zsh"}},{"term":{"process.args":"unique"}}]}}' - ); - }); + // The correct filter is applied to the timeline query + cy.get(FILTER_BADGE).should( + 'have.text', + ' {"bool":{"must":[{"term":{"process.args":"-zsh"}},{"term":{"process.args":"unique"}}]}}' + ); + }); - it('should open a new timeline from an insights module', () => { - verifyInsightCount({ - tableSelector: INSIGHTS_RELATED_ALERTS_BY_SESSION, - investigateSelector: INSIGHTS_INVESTIGATE_IN_TIMELINE_BUTTON, + it('should open a new timeline from an insights module', () => { + verifyInsightCount({ + tableSelector: INSIGHTS_RELATED_ALERTS_BY_SESSION, + investigateSelector: INSIGHTS_INVESTIGATE_IN_TIMELINE_BUTTON, + }); }); - }); - it('should open a new timeline with alert ids from the process ancestry', () => { - verifyInsightCount({ - tableSelector: INSIGHTS_RELATED_ALERTS_BY_ANCESTRY, - investigateSelector: INSIGHTS_INVESTIGATE_ANCESTRY_ALERTS_IN_TIMELINE_BUTTON, + it('should open a new timeline with alert ids from the process ancestry', () => { + verifyInsightCount({ + tableSelector: INSIGHTS_RELATED_ALERTS_BY_ANCESTRY, + investigateSelector: INSIGHTS_INVESTIGATE_ANCESTRY_ALERTS_IN_TIMELINE_BUTTON, + }); }); }); - }); -}); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/resolver.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/resolver.cy.ts index 722e196706859..0132acaad5986 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/resolver.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/resolver.cy.ts @@ -17,29 +17,33 @@ import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; import { login, visit } from '../../../tasks/login'; import { ALERTS_URL } from '../../../urls/navigation'; -describe('Analyze events view for alerts', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cleanKibana(); - createRule(getNewRule()); - }); +describe( + 'Analyze events view for alerts', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cleanKibana(); + createRule(getNewRule()); + }); - beforeEach(() => { - login(); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - }); + beforeEach(() => { + login(); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); - it('should render when button is clicked', () => { - openAnalyzerForFirstAlertInTimeline(); - cy.get(ANALYZER_NODE).first().should('be.visible'); - }); + it('should render when button is clicked', () => { + openAnalyzerForFirstAlertInTimeline(); + cy.get(ANALYZER_NODE).first().should('be.visible'); + }); - it('should display a toast indicating the date range of found events when a time range has 0 events in it', () => { - const dateContainingZeroEvents = 'Jul 27, 2022 @ 00:00:00.000'; - setStartDate(dateContainingZeroEvents); - waitForAlertsToPopulate(); - openAnalyzerForFirstAlertInTimeline(); - cy.get(TOASTER).should('be.visible'); - cy.get(ANALYZER_NODE).first().should('be.visible'); - }); -}); + it('should display a toast indicating the date range of found events when a time range has 0 events in it', () => { + const dateContainingZeroEvents = 'Jul 27, 2022 @ 00:00:00.000'; + setStartDate(dateContainingZeroEvents); + waitForAlertsToPopulate(); + openAnalyzerForFirstAlertInTimeline(); + cy.get(TOASTER).should('be.visible'); + cy.get(ANALYZER_NODE).first().should('be.visible'); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/bulk_add_to_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/bulk_add_to_timeline.cy.ts index 3ba223097023a..4bfb052f58a17 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/bulk_add_to_timeline.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/bulk_add_to_timeline.cy.ts @@ -33,7 +33,7 @@ describe('Bulk Investigate in Timeline', { tags: ['@ess', '@serverless'] }, () = cy.task('esArchiverUnload', 'bulk_process'); }); - context('Alerts', () => { + context('Alerts', { tags: ['@brokenInServerless'] }, () => { before(() => { createRule(getNewRule()); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/data_providers.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/data_providers.cy.ts index c3add90e6531f..0caaefd1c5e69 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/data_providers.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/data_providers.cy.ts @@ -29,61 +29,66 @@ import { getTimeline } from '../../../objects/timeline'; import { HOSTS_URL } from '../../../urls/navigation'; import { cleanKibana, scrollToBottom } from '../../../tasks/common'; -describe('timeline data providers', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cleanKibana(); - }); +// Failing in serverless +describe( + 'timeline data providers', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cleanKibana(); + }); - beforeEach(() => { - login(); - visit(HOSTS_URL); - waitForAllHostsToBeLoaded(); - scrollToBottom(); - createNewTimeline(); - addNameAndDescriptionToTimeline(getTimeline()); - populateTimeline(); - }); + beforeEach(() => { + login(); + visit(HOSTS_URL); + waitForAllHostsToBeLoaded(); + scrollToBottom(); + createNewTimeline(); + addNameAndDescriptionToTimeline(getTimeline()); + populateTimeline(); + }); - it('displays the data provider action menu when Enter is pressed', () => { - addDataProvider({ field: 'host.name', operator: 'exists' }); + it('displays the data provider action menu when Enter is pressed', () => { + addDataProvider({ field: 'host.name', operator: 'exists' }); - cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('not.exist'); - cy.get(`${TIMELINE_FLYOUT_HEADER} ${TIMELINE_DROPPED_DATA_PROVIDERS}`).focus(); - cy.get(`${TIMELINE_FLYOUT_HEADER} ${TIMELINE_DROPPED_DATA_PROVIDERS}`) - .first() - .parent() - .type('{enter}'); + cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('not.exist'); + cy.get(`${TIMELINE_FLYOUT_HEADER} ${TIMELINE_DROPPED_DATA_PROVIDERS}`).focus(); + cy.get(`${TIMELINE_FLYOUT_HEADER} ${TIMELINE_DROPPED_DATA_PROVIDERS}`) + .first() + .parent() + .type('{enter}'); + + cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('exist'); + }); - cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('exist'); - }); + it.skip( + 'persists timeline when data provider is updated by dragging a field from data grid', + { tags: ['@brokenInServerless'] }, + () => { + updateDataProviderbyDraggingField('host.name', 0); + waitForTimelineChanges(); + cy.reload(); + cy.get(`${GET_TIMELINE_GRID_CELL('host.name')}`) + .first() + .then((hostname) => { + cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER).contains(`host.name: "${hostname.text()}"`); + }); + } + ); - it.skip( - 'persists timeline when data provider is updated by dragging a field from data grid', - { tags: ['@brokenInServerless'] }, - () => { - updateDataProviderbyDraggingField('host.name', 0); + it('persists timeline when a field is added by hover action "Add To Timeline" in data provider ', () => { + addDataProvider({ field: 'host.name', operator: 'exists' }); + waitForTimelineChanges(); + updateDataProviderByFieldHoverAction('host.name', 0); waitForTimelineChanges(); cy.reload(); cy.get(`${GET_TIMELINE_GRID_CELL('host.name')}`) .first() .then((hostname) => { - cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER).contains(`host.name: "${hostname.text()}"`); - }); - } - ); - - it('persists timeline when a field is added by hover action "Add To Timeline" in data provider ', () => { - addDataProvider({ field: 'host.name', operator: 'exists' }); - waitForTimelineChanges(); - updateDataProviderByFieldHoverAction('host.name', 0); - waitForTimelineChanges(); - cy.reload(); - cy.get(`${GET_TIMELINE_GRID_CELL('host.name')}`) - .first() - .then((hostname) => { - cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER).should((dataProviderContainer) => { - expect(dataProviderContainer).to.contain(`host.name: "${hostname.text()}"`); + cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER).should((dataProviderContainer) => { + expect(dataProviderContainer).to.contain(`host.name: "${hostname.text()}"`); + }); }); - }); - }); -}); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/discover/cell_actions.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/discover/cell_actions.cy.ts index 7eb818ef9205f..0325117e6c017 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/discover/cell_actions.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/discover/cell_actions.cy.ts @@ -52,7 +52,7 @@ describe( ); }); }); - it('Filter out', () => { + it('Filter out', { tags: ['@brokenInServerless'] }, () => { cy.get(GET_DISCOVER_DATA_GRID_CELL(TIMESTAMP_COLUMN_NAME, 0)).then((sub) => { const selectedTimestamp = sub.text(); cy.get(GET_DISCOVER_DATA_GRID_CELL(TIMESTAMP_COLUMN_NAME, 0)).realHover(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/fields_browser.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/fields_browser.cy.ts index 4806098a37d1c..09e0885a8542d 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/fields_browser.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/fields_browser.cy.ts @@ -49,7 +49,8 @@ const defaultHeaders = [ { id: 'user.name' }, ]; -describe('Fields Browser', { tags: ['@ess', '@serverless'] }, () => { +// Flaky in serverless tests +describe('Fields Browser', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/pagination.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/pagination.cy.ts index a8ab36c7bd837..0fa319ec40021 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/pagination.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/pagination.cy.ts @@ -22,8 +22,9 @@ import { populateTimeline } from '../../../tasks/timeline'; import { HOSTS_URL } from '../../../urls/navigation'; +// Flaky on serverless const defaultPageSize = 25; -describe('Pagination', { tags: ['@ess', '@serverless'] }, () => { +describe('Pagination', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { cleanKibana(); cy.task('esArchiverLoad', { archiveName: 'timeline' }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/toggle_column.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/toggle_column.cy.ts index 5cdb4462839c2..c2ca620a40554 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/toggle_column.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/toggle_column.cy.ts @@ -19,30 +19,34 @@ import { import { HOSTS_URL } from '../../../urls/navigation'; -describe('toggle column in timeline', { tags: ['@ess', '@serverless'] }, () => { - before(() => { - cleanKibana(); - cy.intercept('POST', '/api/timeline/_export?file_name=timelines_export.ndjson').as('export'); - }); - - beforeEach(() => { - login(); - visit(HOSTS_URL); - openTimelineUsingToggle(); - populateTimeline(); - }); - - it('removes the @timestamp field from the timeline when the user un-checks the toggle', () => { - expandFirstTimelineEventDetails(); - clickTimestampToggleField(); - - cy.get(TIMESTAMP_HEADER_FIELD).should('not.exist'); - }); - - it('adds the _id field to the timeline when the user checks the field', () => { - expandFirstTimelineEventDetails(); - clickIdToggleField(); - - cy.get(ID_HEADER_FIELD).should('exist'); - }); -}); +describe( + 'toggle column in timeline', + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, + () => { + before(() => { + cleanKibana(); + cy.intercept('POST', '/api/timeline/_export?file_name=timelines_export.ndjson').as('export'); + }); + + beforeEach(() => { + login(); + visit(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + }); + + it('removes the @timestamp field from the timeline when the user un-checks the toggle', () => { + expandFirstTimelineEventDetails(); + clickTimestampToggleField(); + + cy.get(TIMESTAMP_HEADER_FIELD).should('not.exist'); + }); + + it('adds the _id field to the timeline when the user checks the field', () => { + expandFirstTimelineEventDetails(); + clickIdToggleField(); + + cy.get(ID_HEADER_FIELD).should('exist'); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/overview/cti_link_panel.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/overview/cti_link_panel.cy.ts index eb6548a5e5834..d6936f86e9e89 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/overview/cti_link_panel.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/overview/cti_link_panel.cy.ts @@ -30,8 +30,9 @@ describe('CTI Link Panel', { tags: ['@ess', '@serverless'] }, () => { .and('match', /app\/integrations\/browse\/threat_intel/); }); - describe('enabled threat intel module', () => { + describe('enabled threat intel module', { tags: ['@brokenInServerless'] }, () => { before(() => { + // illegal_argument_exception: unknown setting [index.lifecycle.name] cy.task('esArchiverLoad', { archiveName: 'threat_indicator' }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/fixtures/related_integrations.ndjson b/x-pack/test/security_solution_cypress/cypress/fixtures/related_integrations.ndjson deleted file mode 100644 index f121d07f4610f..0000000000000 --- a/x-pack/test/security_solution_cypress/cypress/fixtures/related_integrations.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"id":"6cc39c80-da3a-11ec-9fce-65c1a0bee904","updated_at":"2022-05-23T01:48:23.422Z","updated_by":"elastic","created_at":"2022-05-23T01:48:20.940Z","created_by":"elastic","name":"Related integrations rule","tags":["Elastic","Endpoint Security"],"interval":"5m","enabled":false,"description":"Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.","risk_score":47,"severity":"medium","license":"Elastic License v2","output_index":".siem-signals-default","meta":{"from":"5m"},"rule_name_override":"message","timestamp_override":"event.ingested","author":["Elastic"],"false_positives":[],"from":"now-50000h","rule_id":"2c66bf23-6ae9-4eb2-859e-446bea181ae9","max_signals":10000,"risk_score_mapping":[{"field":"event.risk_score","operator":"equals","value":""}],"severity_mapping":[{"field":"event.severity","operator":"equals","severity":"low","value":"21"},{"field":"event.severity","operator":"equals","severity":"medium","value":"47"},{"field":"event.severity","operator":"equals","severity":"high","value":"73"},{"field":"event.severity","operator":"equals","severity":"critical","value":"99"}],"threat":[],"to":"now","references":[],"version":7,"exceptions_list":[{"id":"endpoint_list","list_id":"endpoint_list","namespace_type":"agnostic","type":"endpoint"}],"immutable":false,"related_integrations":[{"package":"system","version":"1.17.0"},{"package":"aws","integration":"cloudtrail","version":"1.17.0"},{"package":"aws","integration":"cloudfront","version":"1.17.0"},{"package":"aws","integration":"unknown","version":"1.17.0"}],"type":"query","language":"kuery","index":["auditbeat-*"],"query":"*:*","filters":[],"throttle":"no_actions","actions":[]} -{"exported_count":1,"exported_rules_count":1,"missing_rules":[],"missing_rules_count":0,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0} diff --git a/x-pack/test/security_solution_cypress/cypress/objects/event.ts b/x-pack/test/security_solution_cypress/cypress/objects/event.ts new file mode 100644 index 0000000000000..ea7a61f222eec --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/objects/event.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 { SecurityEvent } from './types'; + +export function generateEvent(extra: Record = {}): SecurityEvent { + return { + '@timestamp': Date.now(), + ecs: { version: '1.4.0' }, + event: { kind: 'event', category: 'process', type: 'start' }, + ...extra, + }; +} diff --git a/x-pack/test/security_solution_cypress/cypress/objects/exception.ts b/x-pack/test/security_solution_cypress/cypress/objects/exception.ts index cae710b4bd846..6e754d837dcf5 100644 --- a/x-pack/test/security_solution_cypress/cypress/objects/exception.ts +++ b/x-pack/test/security_solution_cypress/cypress/objects/exception.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; export interface Exception { field: string; @@ -58,8 +58,8 @@ export const getException = (): Exception => ({ }); export const expectedExportedExceptionList = ( - exceptionListResponse: Cypress.Response + exceptionListResponse: Cypress.Response ): string => { - const jsonrule = exceptionListResponse.body; - return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"system_indices_superuser","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"${jsonrule.list_id}","name":"${jsonrule.name}","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"${jsonrule.type}","updated_at":"${jsonrule.updated_at}","updated_by":"system_indices_superuser","version":1}\n{"exported_exception_list_count":1,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`; + const jsonRule = exceptionListResponse.body; + return `{"_version":"${jsonRule._version}","created_at":"${jsonRule.created_at}","created_by":"system_indices_superuser","description":"${jsonRule.description}","id":"${jsonRule.id}","immutable":false,"list_id":"${jsonRule.list_id}","name":"${jsonRule.name}","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonRule.tie_breaker_id}","type":"${jsonRule.type}","updated_at":"${jsonRule.updated_at}","updated_by":"system_indices_superuser","version":1}\n{"exported_exception_list_count":1,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`; }; diff --git a/x-pack/test/security_solution_cypress/cypress/objects/types.ts b/x-pack/test/security_solution_cypress/cypress/objects/types.ts index b61580dd0287d..7050a97de91c4 100644 --- a/x-pack/test/security_solution_cypress/cypress/objects/types.ts +++ b/x-pack/test/security_solution_cypress/cypress/objects/types.ts @@ -12,3 +12,16 @@ export type CreateRulePropsRewrites = Partial { return `${DISCOVER_TABLE} ${getDataTestSubjectSelector( 'dataGridRowCell' - )}[data-gridcell-column-id="${columnId}"][data-gridcell-row-index="${rowIndex}"] .dscDiscoverGrid__cellValue`; + )}[data-gridcell-column-id="${columnId}"][data-gridcell-row-index="${rowIndex}"] .unifiedDataTable__cellValue`; }; export const GET_DISCOVER_DATA_GRID_CELL_HEADER = (columnId: string) => diff --git a/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_serverless_splash.ts b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_serverless_splash.ts new file mode 100644 index 0000000000000..6849a6702f6ee --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_serverless_splash.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 const PAYWALL_DESCRIPTION = '[data-test-subj="paywallCardDescription"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts index db13c164b3778..0f7460ded36e5 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts @@ -58,9 +58,9 @@ export const INDICATOR_INDEX_QUERY = 'Indicator index query'; export const INDICATOR_MAPPING = 'Indicator mapping'; -export const INTEGRATIONS = '[data-test-subj="integrationLink"]'; +export const INTEGRATION_LINK = '[data-test-subj="integrationLink"]'; -export const INTEGRATIONS_STATUS = '[data-test-subj="statusBadge"]'; +export const INTEGRATION_STATUS = '[data-test-subj="statusBadge"]'; export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; @@ -79,7 +79,8 @@ export const NEW_TERMS_HISTORY_WINDOW_DETAILS = 'History Window Size'; export const FIELDS_BROWSER_BTN = '[data-test-subj="alertsTable"] [data-test-subj="show-field-browser"]'; -export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]'; +export const LAST_EXECUTION_STATUS_REFRESH_BUTTON = + '[data-test-subj="ruleLastExecutionStatusRefreshButton"]'; export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_details_flyout.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_details_flyout.ts new file mode 100644 index 0000000000000..ef7eff086e829 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_details_flyout.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 const TABLE_TAB = '[data-test-subj="securitySolutionDocumentDetailsFlyoutTableTab"]'; + +export const FILTER_INPUT = + '[data-test-subj="securitySolutionDocumentDetailsFlyoutBody"] [data-test-subj="search-input"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts b/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts index bf2277c95e289..0b1bc1cb68de8 100644 --- a/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts +++ b/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts @@ -5,10 +5,12 @@ * 2.0. */ +import Fs from 'fs'; +import * as Url from 'url'; import { EsArchiver } from '@kbn/es-archiver'; -import { KbnClient } from '@kbn/test'; -import { Client, HttpConnection } from '@elastic/elasticsearch'; +import { createEsClientForTesting, KbnClient, systemIndicesSuperuser } from '@kbn/test'; import { ToolingLog } from '@kbn/tooling-log'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; export const esArchiver = ( on: Cypress.PluginEvents, @@ -16,14 +18,20 @@ export const esArchiver = ( ): EsArchiver => { const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout }); - const client = new Client({ - node: config.env.ELASTICSEARCH_URL, - Connection: HttpConnection, + const isServerless = config.env.IS_SERVERLESS; + + const client = createEsClientForTesting({ + esUrl: Url.format(config.env.ELASTICSEARCH_URL), + // Use system indices user so tests can write to system indices + authOverride: !isServerless ? systemIndicesSuperuser : undefined, }); const kbnClient = new KbnClient({ log, url: config.env.CYPRESS_BASE_URL as string, + ...(config.env.ELASTICSEARCH_URL.includes('https') + ? { certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)] } + : {}), }); const esArchiverInstance = new EsArchiver({ diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/elasticsearch.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/elasticsearch.ts index bd4c2c4af873c..e5edbaf65bd0a 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/elasticsearch.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/elasticsearch.ts @@ -9,12 +9,34 @@ import { rootRequest } from '../common'; export const deleteIndex = (index: string) => { rootRequest({ method: 'DELETE', - url: `${Cypress.env('ELASTICSEARCH_URL')}/${index}`, + url: `${Cypress.env('ELASTICSEARCH_URL')}/${index}?refresh=wait_for`, headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, failOnStatusCode: false, }); }; +export const deleteDataStream = (dataStreamName: string) => { + rootRequest({ + method: 'DELETE', + url: `${Cypress.env('ELASTICSEARCH_URL')}/_data_stream/${dataStreamName}`, + headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + failOnStatusCode: false, + }); +}; + +export const deleteAllDocuments = (target: string) => + rootRequest({ + method: 'POST', + url: `${Cypress.env( + 'ELASTICSEARCH_URL' + )}/${target}/_delete_by_query?conflicts=proceed&scroll_size=10000&refresh`, + body: { + query: { + match_all: {}, + }, + }, + }); + export const createIndex = (indexName: string, properties: Record) => rootRequest({ method: 'PUT', @@ -29,7 +51,7 @@ export const createIndex = (indexName: string, properties: Record) => rootRequest({ method: 'POST', - url: `${Cypress.env('ELASTICSEARCH_URL')}/${indexName}/_doc`, + url: `${Cypress.env('ELASTICSEARCH_URL')}/${indexName}/_doc?refresh=wait_for`, body: document, }); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts index 756a5f25d540c..48b21115b9895 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + PerformRuleInstallationResponseBody, + PERFORM_RULE_INSTALLATION_URL, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; import { ELASTIC_SECURITY_RULE_ID } from '@kbn/security-solution-plugin/common/detection_engine/constants'; import type { PrePackagedRulesStatusResponse } from '@kbn/security-solution-plugin/public/detection_engine/rule_management/logic/types'; import { getPrebuiltRuleWithExceptionsMock } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules/mocks'; @@ -34,10 +38,10 @@ export const SAMPLE_PREBUILT_RULE = createRuleAssetSavedObject({ * `createNewRuleAsset` to create mocked prebuilt rules and install only those * instead of all rules available in the `security_detection_engine` package */ -export const installAllPrebuiltRulesRequest = () => { - return cy.request({ +export const installAllPrebuiltRulesRequest = () => + cy.request({ method: 'POST', - url: 'internal/detection_engine/prebuilt_rules/installation/_perform', + url: PERFORM_RULE_INSTALLATION_URL, headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution', @@ -47,7 +51,6 @@ export const installAllPrebuiltRulesRequest = () => { mode: 'ALL_RULES', }, }); -}; export const getAvailablePrebuiltRulesCount = () => { cy.log('Get prebuilt rules count'); @@ -195,6 +198,24 @@ export const preventPrebuiltRulesPackageInstallation = () => { cy.intercept('POST', '/api/fleet/epm/packages/security_detection_engine/*', {}); }; +/** + * Install prebuilt rule assets. After installing these assets become available to be installed + * as prebuilt rules. Prebuilt rule assets can be generated via `createRuleAssetSavedObject()` helper function. + * + * It's also important to take into account that business logic tries to fetch prebuilt rules Fleet package + * and you need to add `preventPrebuiltRulesPackageInstallation()` to `beforeEach` section (before visit commands) + * to avoid actually pulling a real Fleet package and have only provided prebuilt rule assets for testing. + */ +export const installPrebuiltRuleAssets = (ruleAssets: Array): void => { + cy.log('Create mocked available to install prebuilt rules', ruleAssets.length); + preventPrebuiltRulesPackageInstallation(); + // TODO: use this bulk method once the issue with Cypress is fixed + // bulkCreateRuleAssets({ rules }); + ruleAssets.forEach((rule) => { + createNewRuleAsset({ rule }); + }); +}; + /** * Prevent the installation of the `security_detection_engine` package from Fleet. * The create a `security-rule` asset for each rule provided in the `rules` array. @@ -207,21 +228,16 @@ export const preventPrebuiltRulesPackageInstallation = () => { * * @param {string} installToKibana - Flag to decide whether to install the rules as 'alerts' SO. Defaults to true. */ export const createAndInstallMockedPrebuiltRules = ({ - rules, + rules: ruleAssets, installToKibana = true, }: { rules: Array; installToKibana?: boolean; }) => { - cy.log('Install prebuilt rules', rules?.length); - preventPrebuiltRulesPackageInstallation(); - // TODO: use this bulk method once the issue with Cypress is fixed - // bulkCreateRuleAssets({ rules }); - rules.forEach((rule) => { - createNewRuleAsset({ rule }); - }); + installPrebuiltRuleAssets(ruleAssets); if (installToKibana) { + cy.log('Install prebuilt rules', ruleAssets.length); return installAllPrebuiltRulesRequest(); } }; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/common.ts b/x-pack/test/security_solution_cypress/cypress/tasks/common.ts index ba4cd07cc4a51..a3bcf265455f5 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/common.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/common.ts @@ -9,6 +9,7 @@ import { DATA_VIEW_PATH, INITIAL_REST_VERSION } from '@kbn/data-views-plugin/ser import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import { KIBANA_LOADING_ICON } from '../screens/security_header'; import { EUI_BASIC_TABLE_LOADING } from '../screens/common/controls'; +import { deleteAllDocuments } from './api_calls/elasticsearch'; const primaryButton = 0; @@ -134,17 +135,7 @@ export const deleteAlertsAndRules = () => { }, }); - rootRequest({ - method: 'POST', - url: `${Cypress.env( - 'ELASTICSEARCH_URL' - )}/.lists-*,.items-*,.alerts-security.alerts-*/_delete_by_query?conflicts=proceed&scroll_size=10000&refresh`, - body: { - query: { - match_all: {}, - }, - }, - }); + deleteAllDocuments('.lists-*,.items-*,.alerts-security.alerts-*'); }; export const deleteTimelines = () => { 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 dabd0b89e4fb1..3286291ae325d 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 @@ -69,13 +69,11 @@ import { MITRE_TACTIC, QUERY_BAR, REFERENCE_URLS_INPUT, - REFRESH_BUTTON, RISK_MAPPING_OVERRIDE_OPTION, RISK_OVERRIDE, RULE_DESCRIPTION_INPUT, RULE_NAME_INPUT, RULE_NAME_OVERRIDE, - RULE_STATUS, RULE_TIMESTAMP_OVERRIDE, RULES_CREATION_FORM, RULES_CREATION_PREVIEW_BUTTON, @@ -696,16 +694,6 @@ export const waitForAlertsToPopulate = (alertCountThreshold = 1) => { waitForAlerts(); }; -export const waitForTheRuleToBeExecuted = () => { - cy.waitUntil(() => { - cy.get(REFRESH_BUTTON).click({ force: true }); - return cy - .get(RULE_STATUS) - .invoke('text') - .then((ruleStatus) => ruleStatus === 'succeeded'); - }); -}; - export const selectAndLoadSavedQuery = (queryName: string, queryValue: string) => { cy.get(QUERY_BAR).find(SHOW_QUERY_BAR_BUTTON).click(); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_json_tab.ts b/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_json_tab.ts deleted file mode 100644 index 8affc2c7c4ce9..0000000000000 --- a/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_json_tab.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DOCUMENT_DETAILS_FLYOUT_RIGHT_PANEL_CONTENT } from '../../screens/expandable_flyout/alert_details_right_panel_json_tab'; - -/** - * Scroll to x-y positions within the right section of the document details expandable flyout - * // TODO revisit this as it seems very fragile: the first element found is the timeline flyout, which isn't visible but still exist in the DOM - */ -export const scrollWithinDocumentDetailsExpandableFlyoutRightSection = (x: number, y: number) => - cy.get(DOCUMENT_DETAILS_FLYOUT_RIGHT_PANEL_CONTENT).last().scrollTo(x, y); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/integrations.ts b/x-pack/test/security_solution_cypress/cypress/tasks/integrations.ts index e7c487971fba3..c7da263d42f42 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/integrations.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/integrations.ts @@ -5,23 +5,77 @@ * 2.0. */ +import { TypeOf } from '@kbn/config-schema'; import { - ADD_INTEGRATION_BTN, - INTEGRATION_ADDED_POP_UP, - QUEUE_URL, - SAVE_AND_CONTINUE_BTN, - SKIP_AGENT_INSTALLATION_BTN, -} from '../screens/integrations'; + AGENT_POLICY_API_ROUTES, + CreateAgentPolicyResponse, + EPM_API_ROUTES, + PACKAGE_POLICY_API_ROUTES, +} from '@kbn/fleet-plugin/common'; +import { + NewAgentPolicySchema, + SimplifiedCreatePackagePolicyRequestBodySchema, +} from '@kbn/fleet-plugin/server/types'; +import { rootRequest } from './common'; + +interface Package { + name: string; + version: string; +} -import { visit } from './login'; +export type AgentPolicy = TypeOf; +export type PackagePolicy = TypeOf; +export type PackagePolicyWithoutAgentPolicyId = Omit; + +/** + * Installs provided integrations by installing provided packages, creating an agent policy and adding a package policy. + * An agent policy is created with System integration enabled (with `?sys_monitoring=true` query param). + * + * Agent and package policies can be generated in Kibana by opening Fleet UI e.g. for AWS CloudFront the steps are following + * + * - open `app/integrations/detail/aws-1.17.0/overview?integration=cloudfront` + * - click the button `Add Amazon CloudFront` + * - fill in `Queue URL` + * - press `Preview API request` at the bottom + * - copy shown policies + */ +export function installIntegrations({ + packages, + agentPolicy, + packagePolicy, +}: { + packages: Package[]; + agentPolicy: AgentPolicy; + packagePolicy: Omit; +}): void { + // Bulk install provided packages + rootRequest({ + method: 'POST', + url: EPM_API_ROUTES.BULK_INSTALL_PATTERN, + body: { + packages, + force: true, + }, + headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + }); -export const installAwsCloudFrontWithPolicy = () => { - visit('app/integrations/detail/aws-1.17.0/overview?integration=cloudfront'); - cy.get(ADD_INTEGRATION_BTN).click(); - cy.get(SKIP_AGENT_INSTALLATION_BTN).click(); - cy.get(QUEUE_URL).type('http://www.example.com'); + // Install agent and package policies + rootRequest({ + method: 'POST', + url: `${AGENT_POLICY_API_ROUTES.CREATE_PATTERN}?sys_monitoring=true`, + body: agentPolicy, + headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + }).then((response) => { + const packagePolicyWithAgentPolicyId: PackagePolicy = { + ...packagePolicy, + policy_id: response.body.item.id, + }; - // Fleet installs an integration very slowly, so we have to increase the timeout here. - cy.get(SAVE_AND_CONTINUE_BTN).click(); - cy.get(INTEGRATION_ADDED_POP_UP, { timeout: 120000 }).should('exist'); -}; + rootRequest({ + method: 'POST', + url: PACKAGE_POLICY_API_ROUTES.CREATE_PATTERN, + body: packagePolicyWithAgentPolicyId, + headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + }); + }); +} diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts b/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts index e40f8c375e83a..871cbcb82ab7a 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts @@ -6,6 +6,7 @@ */ import type { Exception } from '../objects/exception'; +import { PAGE_CONTENT_SPINNER } from '../screens/common/page'; import { RULE_STATUS } from '../screens/create_new_rule'; import { ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN, @@ -17,7 +18,7 @@ import { ALERTS_TAB, EXCEPTIONS_TAB, FIELDS_BROWSER_BTN, - REFRESH_BUTTON, + LAST_EXECUTION_STATUS_REFRESH_BUTTON, REMOVE_EXCEPTION_BTN, RULE_SWITCH, DEFINITION_DETAILS, @@ -31,6 +32,7 @@ import { BACK_TO_RULES_TABLE, EXCEPTIONS_TAB_EXPIRED_FILTER, EXCEPTIONS_TAB_ACTIVE_FILTER, + RULE_NAME_HEADER, } from '../screens/rule_details'; import { addExceptionConditions, @@ -114,10 +116,22 @@ export const removeException = () => { cy.get(REMOVE_EXCEPTION_BTN).click(); }; +/** + * Waits for rule details page to be loaded + * + * @param ruleName rule's name + */ +export const waitForPageToBeLoaded = (ruleName: string): void => { + cy.get(PAGE_CONTENT_SPINNER).should('be.visible'); + cy.contains(RULE_NAME_HEADER, ruleName).should('be.visible'); + cy.get(PAGE_CONTENT_SPINNER).should('not.exist'); +}; + export const waitForTheRuleToBeExecuted = () => { cy.waitUntil(() => { - cy.log('Wating for the rule to be executed'); - cy.get(REFRESH_BUTTON).click({ force: true }); + cy.log('Waiting for the rule to be executed'); + cy.get(LAST_EXECUTION_STATUS_REFRESH_BUTTON).click(); + return cy .get(RULE_STATUS) .invoke('text') diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/rule_details_flyout.ts b/x-pack/test/security_solution_cypress/cypress/tasks/rule_details_flyout.ts new file mode 100644 index 0000000000000..b684ccf6cdfe7 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/tasks/rule_details_flyout.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 { FILTER_INPUT, TABLE_TAB } from '../screens/rule_details_flyout'; + +export const openTable = (): void => { + cy.get(TABLE_TAB).click(); +}; + +export const filterBy = (value: string): void => { + cy.get(FILTER_INPUT).type(value); +}; diff --git a/x-pack/test/security_solution_cypress/cypress/tsconfig.json b/x-pack/test/security_solution_cypress/cypress/tsconfig.json index 30634b2af8052..ff1d9c7551f75 100644 --- a/x-pack/test/security_solution_cypress/cypress/tsconfig.json +++ b/x-pack/test/security_solution_cypress/cypress/tsconfig.json @@ -43,6 +43,8 @@ "@kbn/fleet-plugin", "@kbn/cases-components", "@kbn/security-solution-plugin", + "@kbn/dev-utils", "@kbn/expandable-flyout", + "@kbn/config-schema", ] } diff --git a/x-pack/test/security_solution_cypress/es_archives/query_alert/data.json b/x-pack/test/security_solution_cypress/es_archives/query_alert/data.json index 551f3a376033d..94328064bd5e4 100644 --- a/x-pack/test/security_solution_cypress/es_archives/query_alert/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/query_alert/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "eabbdefc23da981f2b74ab58b82622a97bb9878caa11bc914e2adfacc94780f1", - "index": ".internal.alerts-security.alerts-default-000001", + "index": ".alerts-security.alerts-default", "source": { "@timestamp": "2023-04-27T11:03:57.906Z", "Endpoint": { @@ -416,4 +416,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/security_solution_cypress/es_archives/ransomware_detection/data.json b/x-pack/test/security_solution_cypress/es_archives/ransomware_detection/data.json index 5f9ca0b8dcc39..1ef06d3b5b16c 100644 --- a/x-pack/test/security_solution_cypress/es_archives/ransomware_detection/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/ransomware_detection/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "b69cded994ad2f2724fd7c3dba17a628f9a6281f2185c81be8f168e50ad5b535", - "index": ".internal.alerts-security.alerts-default-000001", + "index": ".alerts-security.alerts-default", "source": { "@timestamp": "2023-02-16T04:00:03.238Z", "Endpoint": { diff --git a/x-pack/test/security_solution_cypress/es_archives/ransomware_prevention/data.json b/x-pack/test/security_solution_cypress/es_archives/ransomware_prevention/data.json index 4ced1356a4a10..b0eef10b553a3 100644 --- a/x-pack/test/security_solution_cypress/es_archives/ransomware_prevention/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/ransomware_prevention/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "7e90faa23359be329585e2d224ab6fdbaad5caec4a267c08e415f54a4fb193be", - "index": ".internal.alerts-security.alerts-default-000001", + "index": ".alerts-security.alerts-default", "source": { "@timestamp": "2023-02-15T09:32:36.998Z", "Endpoint": { diff --git a/x-pack/test/security_solution_cypress/package.json b/x-pack/test/security_solution_cypress/package.json index d63c7640feede..140cd99b3df49 100644 --- a/x-pack/test/security_solution_cypress/package.json +++ b/x-pack/test/security_solution_cypress/package.json @@ -14,6 +14,7 @@ "cypress:investigations:run:ess": "yarn cypress:ess --spec './cypress/e2e/investigations/**/*.cy.ts'", "cypress:explore:run:ess": "yarn cypress:ess --spec './cypress/e2e/explore/**/*.cy.ts'", "cypress:changed-specs-only:ess": "yarn cypress:ess --changed-specs-only --env burn=2", + "cypress:burn:ess": "yarn cypress:ess --env burn=2", "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && yarn junit:transform && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/", "junit:transform": "node ../../plugins/security_solution/scripts/junit_transformer --pathPattern '../../../target/kibana-security-solution/cypress/results/*.xml' --rootDirectory ../../../ --reportName 'Security Solution Cypress' --writeInPlace", "cypress:serverless": "TZ=UTC node ../../plugins/security_solution/scripts/start_cypress_parallel --config-file ../../test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts --ftr-config-file ../../test/security_solution_cypress/serverless_config", @@ -21,6 +22,7 @@ "cypress:run:serverless": "yarn cypress:serverless --spec '**/cypress/e2e/!(investigations|explore)/**/*.cy.ts'", "cypress:investigations:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/investigations/**/*.cy.ts'", "cypress:explore:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/explore/**/*.cy.ts'", - "cypress:changed-specs-only:serverless": "yarn cypress:serverless --changed-specs-only --env burn=2" + "cypress:changed-specs-only:serverless": "yarn cypress:serverless --changed-specs-only --env burn=2", + "cypress:burn:serverless": "yarn cypress:serverless --env burn=2" } } diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts index b2917f829384f..3eb7046633c75 100644 --- a/x-pack/test/security_solution_cypress/serverless_config.ts +++ b/x-pack/test/security_solution_cypress/serverless_config.ts @@ -31,6 +31,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--csp.warnLegacyBrowsers=false', '--serverless=security', '--xpack.encryptedSavedObjects.encryptionKey="abcdefghijklmnopqrstuvwxyz123456"', + `--xpack.securitySolutionServerless.productTypes=${JSON.stringify([ + { product_line: 'security', product_tier: 'complete' }, + { product_line: 'endpoint', product_tier: 'complete' }, + { product_line: 'cloud', product_tier: 'complete' }, + ])}`, ], }, testRunner: SecuritySolutionConfigurableCypressTestRunner, diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts index 3bdfa503b0a09..c438b457c4b93 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts @@ -59,7 +59,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } }); - describe('from Timeline', () => { + // FLAKY: https://github.com/elastic/kibana/issues/165344 + describe.skip('from Timeline', () => { let timeline: TimelineResponse; before(async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts index dc2183d6f0fee..5a0f0fdf93ec1 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts @@ -182,6 +182,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Show event/alert details for the first one in the list await pageObjects.timeline.showEventDetails(); + // TODO: The index already exists error toast should not show up + // close and dismiss it if it does + if (await testSubjects.exists('globalToastList')) { + await testSubjects.click('toastCloseButton'); + } // Click responder from the take action button await testSubjects.click('take-action-dropdown-btn'); await testSubjects.clickWhenNotDisabled('endpointResponseActions-action-item'); diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index 1d48ba6b1a104..3474f55488eca 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -69,7 +69,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearchApplications', 'enterpriseSearchEsre', 'enterpriseSearchVectorSearch', - 'elasticsearch', + 'enterpriseSearchElasticsearch', 'appSearch', 'workplaceSearch', 'searchExperiences', @@ -99,7 +99,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearchApplications', 'enterpriseSearchEsre', 'enterpriseSearchVectorSearch', - 'elasticsearch', + 'enterpriseSearchElasticsearch', 'appSearch', 'observabilityAIAssistant', 'workplaceSearch', diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 464aa561332fc..5a56f31c2e7f1 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -56,6 +56,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'enterpriseSearchApplications', 'enterpriseSearchEsre', 'enterpriseSearchVectorSearch', + 'enterpriseSearchElasticsearch', 'appSearch', 'workplaceSearch' ) @@ -75,6 +76,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'enterpriseSearchApplications', 'enterpriseSearchEsre', 'enterpriseSearchVectorSearch', + 'enterpriseSearchElasticsearch', 'observabilityAIAssistant', 'appSearch', 'workplaceSearch', diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts index b9d45cfc44068..fc9871b07e59c 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -33,7 +33,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearchApplications', 'enterpriseSearchEsre', 'enterpriseSearchVectorSearch', - 'elasticsearch', + 'enterpriseSearchElasticsearch', 'appSearch', 'workplaceSearch', 'searchExperiences', diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts index d38c9eea7e2c8..a6b55c1f6b32c 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts @@ -25,6 +25,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'enterpriseSearchApplications', 'enterpriseSearchEsre', 'enterpriseSearchVectorSearch', + 'enterpriseSearchElasticsearch', 'appSearch', 'workplaceSearch', ]; diff --git a/x-pack/test_serverless/README.md b/x-pack/test_serverless/README.md index 84b1aaab676d6..6d7c6e350b9b2 100644 --- a/x-pack/test_serverless/README.md +++ b/x-pack/test_serverless/README.md @@ -5,6 +5,9 @@ The tests and helper methods (services, page objects) defined here in `serverless`, `serverless_observability`, `serverless_search` and `serverless_security` plugins. + For how to set up Docker for serverless ES images, please refer to + [packages/kbn-es/README](https://github.com/elastic/kibana/blob/main/packages/kbn-es/README.mdx). + ## Serverless testing structure and conventions ### Overview @@ -133,3 +136,21 @@ node scripts/functional_tests_server.js --config test_serverless/api_integration node scripts/functional_test_runner.js --config test_serverless/api_integration/test_suites/search/config.ts ``` + +## Run tests on MKI +There is no need to start servers locally, you just need to create MKI project and copy urls for Elasticsearch and Kibana. Make sure to update urls with username/password and port 443 for Elasticsearch. FTR has no control over MKI and can't update your projects so make sure your `config.ts` does not specify any custom arguments for Kibana or Elasticsearch. Otherwise, it will be ignored. You can run the tests from the `x-pack` directory: +``` +TEST_CLOUD=1 TEST_ES_URL="https://elastic:PASSWORD@ES_HOSTNAME:443" TEST_KIBANA_URL="https://elastic:PASSWORD@KIBANA_HOSTNAME" node scripts/functional_test_runner --config test_serverless/api_integration/test_suites/search/config.ts --exclude-tag=skipMKI +``` + +## Skipping tests for MKI run +The tests that are listed in the the regular `config.ts` generally should work in both Kibana CI and MKI. However some tests might not work properly against MKI projects by design. +Tag the tests with `skipMKI` to be excluded for MKI run. It works only for the `describe` block: +``` +describe("my test suite", async function() { + this.tags(['skipMKI']); + ... +}); +``` + +If you are running tests from your local against MKI projects, make sure to add `--exclude-tag=skipMKI` to your FTR command. diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts new file mode 100644 index 0000000000000..a8a6d2d44cd64 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts @@ -0,0 +1,244 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { unset } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { createEsQueryRule } from './helpers/alerting_api_helper'; +import { waitForAlertInIndex, waitForNumRuleRuns } from './helpers/alerting_wait_for_helpers'; +import { ObjectRemover } from '../../../../shared/lib'; + +const OPEN_OR_ACTIVE = new Set(['open', 'active']); + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esClient = getService('es'); + const objectRemover = new ObjectRemover(supertest); + + describe('Alert documents', () => { + const RULE_TYPE_ID = '.es-query'; + const ALERT_INDEX = '.alerts-stack.alerts-default'; + let ruleId: string; + + afterEach(async () => { + objectRemover.removeAll(); + }); + + it('should generate an alert document for an active alert', async () => { + const createdRule = await createEsQueryRule({ + supertest, + consumer: 'alerts', + name: 'always fire', + ruleTypeId: RULE_TYPE_ID, + params: { + size: 100, + thresholdComparator: '>', + threshold: [-1], + index: ['alert-test-data'], + timeField: 'date', + esQuery: JSON.stringify({ query: { match_all: {} } }), + timeWindowSize: 20, + timeWindowUnit: 's', + }, + }); + ruleId = createdRule.id; + expect(ruleId).not.to.be(undefined); + objectRemover.add('default', ruleId, 'rule', 'alerting'); + + // get the first alert document written + const testStart1 = new Date(); + await waitForNumRuleRuns({ + supertest, + numOfRuns: 1, + ruleId, + esClient, + testStart: testStart1, + }); + + const alResp1 = await waitForAlertInIndex({ + esClient, + filter: testStart1, + indexName: ALERT_INDEX, + ruleId, + num: 1, + }); + + const hits1 = alResp1.hits.hits[0]._source as Record; + + expect(new Date(hits1['@timestamp'])).to.be.a(Date); + // should be open, first time, but also seen sometimes active; timing? + expect(OPEN_OR_ACTIVE.has(hits1.event.action)).to.be(true); + expect(hits1.kibana.alert.flapping_history).to.be.an(Array); + expect(hits1.kibana.alert.maintenance_window_ids).to.be.an(Array); + expect(typeof hits1.kibana.alert.reason).to.be('string'); + expect(typeof hits1.kibana.alert.rule.execution.uuid).to.be('string'); + expect(typeof hits1.kibana.alert.duration).to.be('object'); + expect(new Date(hits1.kibana.alert.start)).to.be.a(Date); + expect(typeof hits1.kibana.alert.time_range).to.be('object'); + expect(typeof hits1.kibana.alert.uuid).to.be('string'); + expect(typeof hits1.kibana.alert.url).to.be('string'); + expect(typeof hits1.kibana.alert.duration.us).to.be('string'); + expect(typeof hits1.kibana.version).to.be('string'); + + // remove fields we aren't going to compare directly + const fields = [ + '@timestamp', + 'event.action', + 'kibana.alert.duration.us', + 'kibana.alert.flapping_history', + 'kibana.alert.maintenance_window_ids', + 'kibana.alert.reason', + 'kibana.alert.rule.execution.uuid', + 'kibana.alert.rule.duration', + 'kibana.alert.start', + 'kibana.alert.time_range', + 'kibana.alert.uuid', + 'kibana.alert.url', + 'kibana.version', + ]; + + for (const field of fields) { + unset(hits1, field); + } + + const expected = { + event: { + kind: 'signal', + }, + tags: [], + kibana: { + space_ids: ['default'], + alert: { + title: "rule 'always fire' matched query", + evaluation: { + conditions: 'Number of matching documents is greater than -1', + value: 0, + }, + action_group: 'query matched', + flapping: false, + duration: {}, + instance: { id: 'query matched' }, + status: 'active', + workflow_status: 'open', + rule: { + category: 'Elasticsearch query', + consumer: 'alerts', + name: 'always fire', + execution: {}, + parameters: { + size: 100, + thresholdComparator: '>', + threshold: [-1], + index: ['alert-test-data'], + timeField: 'date', + esQuery: '{"query":{"match_all":{}}}', + timeWindowSize: 20, + timeWindowUnit: 's', + excludeHitsFromPreviousRun: true, + aggType: 'count', + groupBy: 'all', + searchType: 'esQuery', + }, + producer: 'stackAlerts', + revision: 0, + rule_type_id: '.es-query', + tags: [], + uuid: ruleId, + }, + }, + }, + }; + + expect(hits1).to.eql(expected); + }); + + it('should update an alert document for an ongoing alert', async () => { + const createdRule = await createEsQueryRule({ + supertest, + consumer: 'alerts', + name: 'always fire', + ruleTypeId: RULE_TYPE_ID, + params: { + size: 100, + thresholdComparator: '>', + threshold: [-1], + index: ['alert-test-data'], + timeField: 'date', + esQuery: JSON.stringify({ query: { match_all: {} } }), + timeWindowSize: 20, + timeWindowUnit: 's', + }, + }); + ruleId = createdRule.id; + expect(ruleId).not.to.be(undefined); + objectRemover.add('default', ruleId, 'rule', 'alerting'); + + // get the first alert document written + const testStart1 = new Date(); + await waitForNumRuleRuns({ + supertest, + numOfRuns: 1, + ruleId, + esClient, + testStart: testStart1, + }); + + const alResp1 = await waitForAlertInIndex({ + esClient, + filter: testStart1, + indexName: ALERT_INDEX, + ruleId, + num: 1, + }); + + // wait for another run, get the updated alert document + const testStart2 = new Date(); + await waitForNumRuleRuns({ + supertest, + numOfRuns: 1, + ruleId, + esClient, + testStart: testStart2, + }); + + const alResp2 = await waitForAlertInIndex({ + esClient, + filter: testStart2, + indexName: ALERT_INDEX, + ruleId, + num: 1, + }); + + // check for differences we can check and expect + const hits1 = alResp1.hits.hits[0]._source as Record; + const hits2 = alResp2.hits.hits[0]._source as Record; + + expect(hits2['@timestamp']).to.be.greaterThan(hits1['@timestamp']); + expect(OPEN_OR_ACTIVE.has(hits1?.event?.action)).to.be(true); + expect(hits2?.event?.action).to.be('active'); + expect(parseInt(hits1?.kibana?.alert?.duration?.us, 10)).to.not.be.lessThan(0); + expect(hits2?.kibana?.alert?.duration?.us).not.to.be('0'); + + // remove fields we know will be different + const fields = [ + '@timestamp', + 'event.action', + 'kibana.alert.duration.us', + 'kibana.alert.flapping_history', + 'kibana.alert.reason', + 'kibana.alert.rule.execution.uuid', + ]; + + for (const field of fields) { + unset(hits1, field); + unset(hits2, field); + } + + expect(hits1).to.eql(hits2); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_wait_for_helpers.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_wait_for_helpers.ts index eaa5e7b8ee61f..bdca0ee15040a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_wait_for_helpers.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_wait_for_helpers.ts @@ -27,7 +27,7 @@ export async function waitForDocumentInIndex({ async () => { const response = await esClient.search({ index: indexName }); if (response.hits.hits.length < num) { - throw new Error('No hits found'); + throw new Error(`Only found ${response.hits.hits.length} / ${num} documents`); } return response; }, @@ -63,12 +63,16 @@ export async function createIndex({ export async function waitForAlertInIndex({ esClient, + filter, indexName, ruleId, + num = 1, }: { esClient: Client; + filter: Date; indexName: string; ruleId: string; + num: number; }): Promise>> { return pRetry( async () => { @@ -76,14 +80,27 @@ export async function waitForAlertInIndex({ index: indexName, body: { query: { - term: { - 'kibana.alert.rule.uuid': ruleId, + bool: { + must: [ + { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + { + range: { + '@timestamp': { + gte: filter.getTime().toString(), + }, + }, + }, + ], }, }, }, }); - if (response.hits.hits.length === 0) { - throw new Error('No hits found'); + if (response.hits.hits.length < num) { + throw new Error(`Only found ${response.hits.hits.length} / ${num} documents`); } return response; }, diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/index.ts index 4a78d448a7d20..3225ecb4f71ce 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/index.ts @@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Alerting APIs', function () { loadTestFile(require.resolve('./rules')); + loadTestFile(require.resolve('./alert_documents')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts index bfce78384f601..64c8fda9d6089 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts @@ -35,7 +35,8 @@ export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); - describe('Alerting rules', () => { + // Failing: See https://github.com/elastic/kibana/issues/164017 + describe.skip('Alerting rules', () => { const RULE_TYPE_ID = '.es-query'; const ALERT_ACTION_INDEX = 'alert-action-es-query'; let actionId: string; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/indices.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/indices.ts index f9700dfaff70a..cc165c517f6cf 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index_management/indices.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/indices.ts @@ -17,7 +17,8 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); const log = getService('log'); - describe('Indices', function () { + // FLAKY: https://github.com/elastic/kibana/issues/165565 + describe.skip('Indices', function () { const indexName = `index-${Math.random()}`; before(async () => { diff --git a/x-pack/test_serverless/api_integration/test_suites/common/rollups.ts b/x-pack/test_serverless/api_integration/test_suites/common/rollups.ts index 47dd58f242759..c1ec42564e481 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/rollups.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/rollups.ts @@ -16,7 +16,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('rollup data views - fields for wildcard', function () { + // Failing: See https://github.com/elastic/kibana/issues/165476 + describe.skip('rollup data views - fields for wildcard', function () { before(async () => { await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/scripted_fields.ts b/x-pack/test_serverless/api_integration/test_suites/common/scripted_fields.ts index f3969e07eea7e..7cc4089814c77 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/scripted_fields.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/scripted_fields.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('scripted fields disabled', function () { + // FLAKY: https://github.com/elastic/kibana/issues/165511 + describe.skip('scripted fields disabled', function () { before(async () => { await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/authentication.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/authentication.ts index 590adbe267b45..4c8353487adce 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/security/authentication.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/authentication.ts @@ -134,16 +134,18 @@ export default function ({ getService }: FtrProviderContext) { // expect success because we're using the internal header expect(body).toEqual({ authentication_provider: { name: '__http__', type: 'http' }, - authentication_realm: { name: 'reserved', type: 'reserved' }, + authentication_realm: { name: 'file1', type: 'file' }, authentication_type: 'realm', elastic_cloud_user: false, email: null, enabled: true, full_name: null, - lookup_realm: { name: 'reserved', type: 'reserved' }, - metadata: { _reserved: true }, + lookup_realm: { name: 'file1', type: 'file' }, + metadata: {}, + operator: true, roles: ['superuser'], - username: 'elastic', + // We use `elastic` for MKI, and `elastic_serverless` for any other testing environment. + username: expect.stringContaining('elastic'), }); expect(status).toBe(200); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/authentication_http.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/authentication_http.ts index 1555ebe352df9..50148f2892154 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/security/authentication_http.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/authentication_http.ts @@ -12,40 +12,45 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); describe('security/authentication/http', function () { - it('allows JWT HTTP authentication only for selected routes', async () => { - const jsonWebToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2tpYmFuYS5lbGFzdGljLmNvL2p3dC8iLCJzdWIiOiJlbGFzdGljLWFnZW50IiwiYXVkIjoiZWxhc3RpY3NlYXJjaCIsIm5hbWUiOiJFbGFzdGljIEFnZW50IiwiaWF0Ijo5NDY2ODQ4MDAsImV4cCI6NDA3MDkwODgwMH0.P7RHKZlLskS5DfVRqoVO4ivoIq9rXl2-GW6hhC9NvTSkwphYivcjpTVcyENZvxTTvJJNqcyx6rF3T-7otTTIHBOZIMhZauc5dob-sqcN_mT2htqm3BpSdlJlz60TBq6diOtlNhV212gQCEJMPZj0MNj7kZRj_GsECrTaU7FU0A3HAzkbdx15vQJMKZiFbbQCVI7-X2J0bZzQKIWfMHD-VgHFwOe6nomT-jbYIXtCBDd6fNj1zTKRl-_uzjVqNK-h8YW1h6tE4xvZmXyHQ1-9yNKZIWC7iEaPkBLaBKQulLU5MvW3AtVDUhzm6--5H1J85JH5QhRrnKYRon7ZW5q1AQ'; + describe('JWT', () => { + // When we run tests on MKI, JWT realm is configured differently, and we cannot handcraft valid JWTs. + this.tags(['skipMKI']); - // Check 5 routes that are currently known to accept JWT as a means of authentication. - for (const allowedPath of [ - '/api/status', - '/api/stats', - '/api/task_manager/_background_task_utilization', - '/internal/task_manager/_background_task_utilization', - '/api/task_manager/metrics', - ]) { + it('allows JWT HTTP authentication only for selected routes', async () => { + const jsonWebToken = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2tpYmFuYS5lbGFzdGljLmNvL2p3dC8iLCJzdWIiOiJlbGFzdGljLWFnZW50IiwiYXVkIjoiZWxhc3RpY3NlYXJjaCIsIm5hbWUiOiJFbGFzdGljIEFnZW50IiwiaWF0Ijo5NDY2ODQ4MDAsImV4cCI6NDA3MDkwODgwMH0.P7RHKZlLskS5DfVRqoVO4ivoIq9rXl2-GW6hhC9NvTSkwphYivcjpTVcyENZvxTTvJJNqcyx6rF3T-7otTTIHBOZIMhZauc5dob-sqcN_mT2htqm3BpSdlJlz60TBq6diOtlNhV212gQCEJMPZj0MNj7kZRj_GsECrTaU7FU0A3HAzkbdx15vQJMKZiFbbQCVI7-X2J0bZzQKIWfMHD-VgHFwOe6nomT-jbYIXtCBDd6fNj1zTKRl-_uzjVqNK-h8YW1h6tE4xvZmXyHQ1-9yNKZIWC7iEaPkBLaBKQulLU5MvW3AtVDUhzm6--5H1J85JH5QhRrnKYRon7ZW5q1AQ'; + + // Check 5 routes that are currently known to accept JWT as a means of authentication. + for (const allowedPath of [ + '/api/status', + '/api/stats', + '/api/task_manager/_background_task_utilization', + '/internal/task_manager/_background_task_utilization', + '/api/task_manager/metrics', + ]) { + await supertest + .get(allowedPath) + .set('Authorization', `Bearer ${jsonWebToken}`) + .set('ES-Client-Authentication', 'SharedSecret my_super_secret') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200); + } + + // Make sure it's not possible to use JWT to have interactive sessions. await supertest - .get(allowedPath) + .get('/') .set('Authorization', `Bearer ${jsonWebToken}`) .set('ES-Client-Authentication', 'SharedSecret my_super_secret') - .set(svlCommonApi.getInternalRequestHeader()) - .expect(200); - } - - // Make sure it's not possible to use JWT to have interactive sessions. - await supertest - .get('/') - .set('Authorization', `Bearer ${jsonWebToken}`) - .set('ES-Client-Authentication', 'SharedSecret my_super_secret') - .expect(401); + .expect(401); - // Make sure it's not possible to use JWT to access any other APIs. - await supertest - .get('/internal/security/me') - .set('Authorization', `Bearer ${jsonWebToken}`) - .set('ES-Client-Authentication', 'SharedSecret my_super_secret') - .set(svlCommonApi.getInternalRequestHeader()) - .expect(401); + // Make sure it's not possible to use JWT to access any other APIs. + await supertest + .get('/internal/security/me') + .set('Authorization', `Bearer ${jsonWebToken}`) + .set('ES-Client-Authentication', 'SharedSecret my_super_secret') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(401); + }); }); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/user_profiles.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/user_profiles.ts index 79555fd0953f6..64650c5f47548 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/security/user_profiles.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/user_profiles.ts @@ -16,7 +16,12 @@ export default function ({ getService }: FtrProviderContext) { describe('security/user_profiles', function () { describe('route access', () => { - describe('internal', () => { + // FLAKY: https://github.com/elastic/kibana/issues/165391 + describe.skip('internal', () => { + // When we run tests on MKI, SAML realm is configured differently, and we cannot handcraft SAML responses to + // log in as SAML users. + this.tags(['skipMKI']); + it('update', async () => { const { status } = await supertestWithoutAuth .post(`/internal/security/user_profile/_data`) diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/common/apm_api_supertest.ts index c31ee37a93b94..ac324b3fda087 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/common/apm_api_supertest.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/common/apm_api_supertest.ts @@ -4,15 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { ApmUsername } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/authentication'; -import { format, UrlObject } from 'url'; +import { format } from 'url'; import supertest from 'supertest'; import request from 'superagent'; import type { APIReturnType, APIClientRequestParamsOf, } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { kbnTestConfig } from '@kbn/test'; +import { Config, kbnTestConfig, kibanaTestSuperuserServerless } from '@kbn/test'; import type { APIEndpoint } from '@kbn/apm-plugin/server'; import { formatRequest } from '@kbn/server-route-repository'; import { InheritedFtrProviderContext } from '../../../../services'; @@ -93,19 +92,19 @@ Body: ${JSON.stringify(res.body)}` } } -async function getApmApiClient({ - kibanaServer, - username, -}: { - kibanaServer: UrlObject; - username: ApmUsername | 'elastic'; -}) { +async function getApmApiClient({ svlSharedConfig }: { svlSharedConfig: Config }) { + const kibanaServer = svlSharedConfig.get('servers.kibana'); + const cAuthorities = svlSharedConfig.get('servers.kibana.certificateAuthorities'); + + const username = kbnTestConfig.getUrlParts(kibanaTestSuperuserServerless).username; + const password = kbnTestConfig.getUrlParts(kibanaTestSuperuserServerless).password; + const url = format({ ...kibanaServer, - auth: `${username}:${kbnTestConfig.getUrlParts().password}`, + auth: `${username}:${password}`, }); - return createApmApiClient(supertest(url)); + return createApmApiClient(supertest.agent(url, { ca: cAuthorities })); } export interface SupertestReturnType { @@ -120,12 +119,10 @@ export async function getApmApiClientService({ getService, }: InheritedFtrProviderContext): Promise { const svlSharedConfig = getService('config'); - const kibanaServer = svlSharedConfig.get('servers.kibana'); return { slsUser: await getApmApiClient({ - kibanaServer, - username: 'elastic', + svlSharedConfig, }), }; } diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/feature_flags.ts b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/feature_flags.ts index 44e188c0402ec..279a1e0970b57 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/feature_flags.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/feature_flags.ts @@ -7,6 +7,7 @@ import expect from 'expect'; import { APMFtrContextProvider } from './common/services'; +import { ApmApiClient } from './common/apm_api_supertest'; const fleetMigrationResponse = { statusCode: 404, @@ -48,7 +49,7 @@ const SAMPLE_SOURCEMAP = { mappings: 'A,AAAB;;ABCDE;', }; -async function uploadSourcemap(apmApiClient: any) { +async function uploadSourcemap(apmApiClient: ApmApiClient) { const response = await apmApiClient.slsUser({ endpoint: 'POST /api/apm/sourcemaps 2023-10-31', type: 'form-data', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/api.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/api.ts index 5f196ef3e3372..33cbdf07bf602 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/api.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/api.ts @@ -112,7 +112,7 @@ export const deleteMappings = async (es: Client): Promise => { }); }; -export const defaultUser = { email: null, full_name: null, username: 'elastic' }; +export const defaultUser = { email: null, full_name: null, username: 'elastic_serverless' }; /** * A null filled user will occur when the security plugin is disabled */ diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index 36907484f13d3..847b85c9c2e2e 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Serverless observability API', function () { loadTestFile(require.resolve('./fleet/fleet')); loadTestFile(require.resolve('./telemetry/snapshot_telemetry')); + loadTestFile(require.resolve('./telemetry/telemetry_config')); loadTestFile(require.resolve('./apm_api_integration/feature_flags.ts')); loadTestFile(require.resolve('./cases')); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/telemetry/telemetry_config.ts b/x-pack/test_serverless/api_integration/test_suites/observability/telemetry/telemetry_config.ts new file mode 100644 index 0000000000000..6ef34b9a0966c --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/telemetry/telemetry_config.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function telemetryConfigTest({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/config API Telemetry config', () => { + const baseConfig = { + allowChangingOptInStatus: false, + optIn: true, + sendUsageFrom: 'server', + telemetryNotifyUserAboutOptInDefault: false, + labels: { + serverless: 'observability', + }, + }; + + it('GET should get the default config', async () => { + await supertest + .get('/api/telemetry/v2/config') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200, baseConfig); + }); + + it('GET should get updated labels after dynamically updating them', async () => { + await supertest + .put('/internal/core/_settings') + .set(svlCommonApi.getInternalRequestHeader()) + .set('elastic-api-version', '1') + .send({ 'telemetry.labels.journeyName': 'my-ftr-test' }) + .expect(200, { ok: true }); + + await supertest + .get('/api/telemetry/v2/config') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200, { + ...baseConfig, + labels: { + ...baseConfig.labels, + journeyName: 'my-ftr-test', + }, + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts index 28f896a5336a0..27c8471c4eff0 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts @@ -20,7 +20,9 @@ export default function ({ getService }: FtrProviderContext) { const dataViewApi = getService('dataViewApi'); const logger = getService('log'); - describe('Threshold rule - AVG - PCT - FIRED', () => { + // Blocked API: index_not_found_exception: no such index [.alerts-observability.threshold.alerts-default] + // Issue: https://github.com/elastic/kibana/issues/165138 + describe.skip('Threshold rule - AVG - PCT - FIRED', () => { const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; const ALERT_ACTION_INDEX = 'alert-action-threshold'; const DATA_VIEW_ID = 'data-view-id'; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts index 96694a15f49db..8050f0a672cb7 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts @@ -17,7 +17,9 @@ export default function ({ getService }: FtrProviderContext) { const alertingApi = getService('alertingApi'); const dataViewApi = getService('dataViewApi'); - describe('Threshold rule - AVG - PCT - NoData', () => { + // Blocked API: index_not_found_exception: no such index [.alerts-observability.threshold.alerts-default] + // Issue: https://github.com/elastic/kibana/issues/165138 + describe.skip('Threshold rule - AVG - PCT - NoData', () => { const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; const ALERT_ACTION_INDEX = 'alert-action-threshold'; const DATA_VIEW_ID = 'data-view-id-no-data'; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts index bedf17892d8c9..2fab6c3a9ebe2 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts @@ -26,7 +26,8 @@ export default function ({ getService }: FtrProviderContext) { const alertingApi = getService('alertingApi'); const dataViewApi = getService('dataViewApi'); - describe('Threshold rule - CUSTOM_EQ - AVG - BYTES - FIRED', () => { + // Issue: https://github.com/elastic/kibana/issues/165138 + describe.skip('Threshold rule - CUSTOM_EQ - AVG - BYTES - FIRED', () => { const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; const ALERT_ACTION_INDEX = 'alert-action-threshold'; const DATA_VIEW_ID = 'data-view-id'; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts index 81d74e4d29193..1d79f3aca06a7 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts @@ -20,7 +20,8 @@ export default function ({ getService }: FtrProviderContext) { const alertingApi = getService('alertingApi'); const dataViewApi = getService('dataViewApi'); - describe('Threshold rule - DOCUMENTS_COUNT - FIRED', () => { + // Issue: https://github.com/elastic/kibana/issues/165138 + describe.skip('Threshold rule - DOCUMENTS_COUNT - FIRED', () => { const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; const ALERT_ACTION_INDEX = 'alert-action-threshold'; const DATA_VIEW_ID = 'data-view-id'; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts index cf8278c106188..b8b0418a2124e 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts @@ -30,7 +30,8 @@ export default function ({ getService }: FtrProviderContext) { let alertId: string; let startedAt: string; - describe('Threshold rule - GROUP_BY - FIRED', () => { + // Issue: https://github.com/elastic/kibana/issues/165138 + describe.skip('Threshold rule - GROUP_BY - FIRED', () => { const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; const ALERT_ACTION_INDEX = 'alert-action-threshold'; const DATA_VIEW_ID = 'data-view-id'; diff --git a/x-pack/test_serverless/api_integration/test_suites/search/index.ts b/x-pack/test_serverless/api_integration/test_suites/search/index.ts index 78964aa73c786..ff29a499c6eab 100644 --- a/x-pack/test_serverless/api_integration/test_suites/search/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/search/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Serverless search API', function () { loadTestFile(require.resolve('./telemetry/snapshot_telemetry')); + loadTestFile(require.resolve('./telemetry/telemetry_config')); loadTestFile(require.resolve('./cases/find_cases')); loadTestFile(require.resolve('./cases/post_case')); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/search/telemetry/telemetry_config.ts b/x-pack/test_serverless/api_integration/test_suites/search/telemetry/telemetry_config.ts new file mode 100644 index 0000000000000..381c2aa0f5cae --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/search/telemetry/telemetry_config.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function telemetryConfigTest({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/config API Telemetry config', () => { + const baseConfig = { + allowChangingOptInStatus: false, + optIn: true, + sendUsageFrom: 'server', + telemetryNotifyUserAboutOptInDefault: false, + labels: { + serverless: 'search', + }, + }; + + it('GET should get the default config', async () => { + await supertest + .get('/api/telemetry/v2/config') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200, baseConfig); + }); + + it('GET should get updated labels after dynamically updating them', async () => { + await supertest + .put('/internal/core/_settings') + .set(svlCommonApi.getInternalRequestHeader()) + .set('elastic-api-version', '1') + .send({ 'telemetry.labels.journeyName': 'my-ftr-test' }) + .expect(200, { ok: true }); + + await supertest + .get('/api/telemetry/v2/config') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200, { + ...baseConfig, + labels: { + ...baseConfig.labels, + journeyName: 'my-ftr-test', + }, + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/api.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/api.ts index afba9c46c67c2..0d1a889adeadc 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/api.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/api.ts @@ -112,7 +112,7 @@ export const deleteMappings = async (es: Client): Promise => { }); }; -export const defaultUser = { email: null, full_name: null, username: 'elastic' }; +export const defaultUser = { email: null, full_name: null, username: 'elastic_serverless' }; /** * A null filled user will occur when the security plugin is disabled */ diff --git a/x-pack/test_serverless/api_integration/test_suites/security/index.ts b/x-pack/test_serverless/api_integration/test_suites/security/index.ts index eaf193c5f659c..eb00134311d79 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Serverless security API', function () { loadTestFile(require.resolve('./telemetry/snapshot_telemetry')); + loadTestFile(require.resolve('./telemetry/telemetry_config')); loadTestFile(require.resolve('./fleet/fleet')); loadTestFile(require.resolve('./cases')); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/telemetry/telemetry_config.ts b/x-pack/test_serverless/api_integration/test_suites/security/telemetry/telemetry_config.ts new file mode 100644 index 0000000000000..5df1da84a8dbf --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/telemetry/telemetry_config.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function telemetryConfigTest({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/config API Telemetry config', () => { + const baseConfig = { + allowChangingOptInStatus: false, + optIn: true, + sendUsageFrom: 'server', + telemetryNotifyUserAboutOptInDefault: false, + labels: { + serverless: 'security', + }, + }; + + it('GET should get the default config', async () => { + await supertest + .get('/api/telemetry/v2/config') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200, baseConfig); + }); + + it('GET should get updated labels after dynamically updating them', async () => { + await supertest + .put('/internal/core/_settings') + .set(svlCommonApi.getInternalRequestHeader()) + .set('elastic-api-version', '1') + .send({ 'telemetry.labels.journeyName': 'my-ftr-test' }) + .expect(200, { ok: true }); + + await supertest + .get('/api/telemetry/v2/config') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200, { + ...baseConfig, + labels: { + ...baseConfig.labels, + journeyName: 'my-ftr-test', + }, + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/config.base.ts b/x-pack/test_serverless/functional/config.base.ts index 640ae2402b544..02ddf326fef8d 100644 --- a/x-pack/test_serverless/functional/config.base.ts +++ b/x-pack/test_serverless/functional/config.base.ts @@ -27,6 +27,7 @@ export function createTestConfig(options: CreateTestConfigOptions) { serverArgs: [ ...svlSharedConfig.get('kbnTestServer.serverArgs'), `--serverless=${options.serverlessProject}`, + ...(options.kbnServerArgs ?? []), ], }, testFiles: options.testFiles, @@ -52,6 +53,9 @@ export function createTestConfig(options: CreateTestConfigOptions) { observability: { pathname: '/app/observability', }, + observabilityLogExplorer: { + pathname: '/app/observability-log-explorer', + }, management: { pathname: '/app/management', }, diff --git a/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram/data.json b/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram/data.json new file mode 100644 index 0000000000000..129ba80854225 --- /dev/null +++ b/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram/data.json @@ -0,0 +1,231 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:histogram-test", + "index": ".kibana_analytics_1", + "source": { + "index-pattern": { + "fieldAttrs": "{}", + "title": "histogram-test", + "sourceFilters": "[]", + "fields": "[]", + "fieldFormatMap": "{}", + "typeMeta": "{}", + "runtimeFieldMap": "{}", + "name": "histogram-test" + }, + "type": "index-pattern", + "references": [], + "managed": false, + "namespaces": [ + "default" + ], + "coreMigrationVersion": "8.8.0", + "typeMigrationVersion": "8.0.0", + "updated_at": "2023-08-05T05:41:10.360Z", + "created_at": "2023-08-05T05:41:10.360Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "5e69404d93193e4074f0ec1a", + "index": "histogram-test", + "source": { + "histogram-title": "incididunt reprehenderit mollit", + "histogram-content": { + "values": [ + 0.3, + 1, + 3, + 4.2, + 4.8 + ], + "counts": [ + 237, + 170, + 33, + 149, + 241 + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "5e69408f2fc61f57fd5bc762", + "index": "histogram-test", + "source": { + "histogram-title": "culpa cillum ullamco", + "histogram-content": { + "values": [ + 0.5, + 1, + 1.2, + 1.3, + 2.8, + 3.9, + 4.3 + ], + "counts": [ + 113, + 197, + 20, + 66, + 20, + 39, + 178 + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "5e6940b979b57ad343114cc3", + "index": "histogram-test", + "source": { + "histogram-title": "enim veniam et", + "histogram-content": { + "values": [ + 3.7, + 4.2 + ], + "counts": [ + 227, + 141 + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "5e6940d3e95de786eeb7586d", + "index": "histogram-test", + "source": { + "histogram-title": "est incididunt sunt", + "histogram-content": { + "values": [ + 1.8, + 2.4, + 2.6, + 4.9 + ], + "counts": [ + 92, + 101, + 122, + 244 + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "5e694119fb2f956a822b93b9", + "index": "histogram-test", + "source": { + "histogram-title": "qui qui tempor", + "histogram-content": { + "values": [ + 0.5, + 2.1, + 2.7, + 3, + 3.2, + 3.5, + 4.2, + 5 + ], + "counts": [ + 210, + 168, + 182, + 181, + 97, + 164, + 77, + 2 + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "5e694145ad3c741aa12d6e8e", + "index": "histogram-test", + "source": { + "histogram-title": "ullamco nisi sunt", + "histogram-content": { + "values": [ + 1.7, + 4.5, + 4.8 + ], + "counts": [ + 74, + 146, + 141 + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "5e694159d909d9d99b5e12d1", + "index": "histogram-test", + "source": { + "histogram-title": "magna eu incididunt", + "histogram-content": { + "values": [ + 1, + 3.4, + 4.8 + ], + "counts": [ + 103, + 205, + 11 + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "5e694159d909d9d99b5e12d1", + "index": "histogram-test", + "source": { + "histogram-title": "single value", + "histogram-content": { + "values": [ + 1 + ], + "counts": [ + 1 + ] + } + } + } +} diff --git a/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram/mappings.json b/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram/mappings.json new file mode 100644 index 0000000000000..70373f1dadbef --- /dev/null +++ b/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram/mappings.json @@ -0,0 +1,30 @@ +{ + "type": "index", + "value": { + "aliases": {}, + "index": "histogram-test", + "mappings": { + "properties": { + "histogram-title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "histogram-content": { + "type": "histogram" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/data_view_field_editor_example.ts b/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/data_view_field_editor_example.ts new file mode 100644 index 0000000000000..cd8e22e095543 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/data_view_field_editor_example.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +// TODO: Changed from PluginFunctionalProviderContext to FtrProviderContext in Serverless +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + + describe('', () => { + it('finds a data view', async () => { + await testSubjects.existOrFail('dataViewTitle'); + }); + + it('opens the field editor', async () => { + await testSubjects.click('addField'); + await testSubjects.existOrFail('flyoutTitle'); + await testSubjects.click('closeFlyoutButton'); + }); + + it('uses preconfigured options for a new field', async () => { + // find the checkbox label and click it - `testSubjects.setCheckbox()` is not working for our checkbox + const controlWrapper = await testSubjects.find('preconfiguredControlWrapper'); + const control = await find.descendantDisplayedByCssSelector('label', controlWrapper); + await control.click(); + + await testSubjects.click('addField'); + await testSubjects.existOrFail('flyoutTitle'); + + const nameField = await testSubjects.find('nameField'); + const nameInput = await find.descendantDisplayedByCssSelector( + '[data-test-subj=input]', + nameField + ); + + expect(await nameInput.getAttribute('value')).to.equal('demotestfield'); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/index.ts new file mode 100644 index 0000000000000..a840abb55f3a7 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/index.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// TODO: Changed from PluginFunctionalProviderContext to FtrProviderContext in Serverless +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const es = getService('es'); + const PageObjects = getPageObjects(['common', 'header', 'settings', 'svlCommonNavigation']); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const retry = getService('retry'); + const kibanaServer = getService('kibanaServer'); + + // FLAKY: https://github.com/elastic/kibana/issues/165384 + describe.skip('data view field editor example', function () { + before(async () => { + // TODO: emptyKibanaIndex fails in Serverless with + // "index_not_found_exception: no such index [.kibana_ingest]", + // so it was switched to `savedObjects.cleanStandardList()` + await kibanaServer.savedObjects.cleanStandardList(); + await browser.setWindowSize(1300, 900); + await es.transport.request({ + path: '/blogs/_doc', + method: 'POST', + body: { user: 'matt', message: 20 }, + }); + + // TODO: Navigation to Data View Management is different in Serverless + await PageObjects.common.navigateToApp('management'); + await retry.waitFor('data views link', async () => { + if (await testSubjects.exists('app-card-dataViews')) { + await testSubjects.click('app-card-dataViews'); + return true; + } + if (await find.existsByCssSelector('[href*="/dataViews"]')) { + await find.clickByCssSelector('[href*="/dataViews"]'); + return true; + } + return false; + }); + await PageObjects.settings.createIndexPattern('blogs', null); + await PageObjects.common.navigateToApp('dataViewFieldEditorExample'); + }); + + loadTestFile(require.resolve('./data_view_field_editor_example')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts b/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts new file mode 100644 index 0000000000000..cb0c5cc9c36ba --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.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 expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +const TEST_START_TIME = 'Sep 19, 2015 @ 06:31:44.000'; +const TEST_END_TIME = 'Sep 23, 2015 @ 18:31:44.000'; + +export default ({ getService, getPageObjects }: FtrProviderContext) => { + const PageObjects = getPageObjects(['common', 'timePicker', 'header']); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const dataGrid = getService('dataGrid'); + const defaultSettings = { defaultIndex: 'logstash-*' }; + + // Flaky in serverless tests (before hook) + // Failing: See https://github.com/elastic/kibana/issues/165396 + describe.skip('Customizations', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.common.navigateToApp('home'); + const currentUrl = await browser.getCurrentUrl(); + const customizationUrl = + currentUrl.substring(0, currentUrl.indexOf('/app/home')) + + '/app/discoverCustomizationExamples'; + await browser.get(customizationUrl); + await PageObjects.timePicker.setAbsoluteRange(TEST_START_TIME, TEST_END_TIME); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await kibanaServer.uiSettings.unset('defaultIndex'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + it('Top nav', async () => { + await testSubjects.existOrFail('customOptionsButton'); + await testSubjects.existOrFail('shareTopNavButton'); + await testSubjects.existOrFail('documentExplorerButton'); + await testSubjects.missingOrFail('discoverNewButton'); + await testSubjects.missingOrFail('discoverOpenButton'); + await testSubjects.click('customOptionsButton'); + await testSubjects.existOrFail('customOptionsPopover'); + await testSubjects.click('customOptionsButton'); + await testSubjects.missingOrFail('customOptionsPopover'); + }); + + it('Search bar', async () => { + await testSubjects.click('logsViewSelectorButton'); + await testSubjects.click('logsViewSelectorOption-ASavedSearch'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const { title, description } = await PageObjects.common.getSharedItemTitleAndDescription(); + const expected = { + title: 'A Saved Search', + description: 'A Saved Search Description', + }; + expect(title).to.eql(expected.title); + expect(description).to.eql(expected.description); + }); + + it('Search bar Prepend Filters exists and should apply filter properly', async () => { + // Validate custom filters are present + await testSubjects.existOrFail('customPrependedFilter'); + await testSubjects.click('customPrependedFilter'); + await testSubjects.existOrFail('optionsList-control-selection-exists'); + + // Retrieve option list popover + const optionsListControl = await testSubjects.find('optionsList-control-popover'); + const optionsItems = await optionsListControl.findAllByCssSelector( + '[data-test-subj*="optionsList-control-selection-"]' + ); + + // Retrieve second item in the options along with the count of documents + const item = optionsItems[1]; + const countBadge = await item.findByCssSelector( + '[data-test-subj="optionsList-document-count-badge"]' + ); + const documentsCount = parseInt(await countBadge.getVisibleText(), 10); + + // Click the item to apply filter + await item.click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + // Validate that filter is applied + const rows = await dataGrid.getDocTableRows(); + await expect(documentsCount).to.eql(rows.length); + }); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/index.ts new file mode 100644 index 0000000000000..ab89a5e4ec994 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Discover customization examples', () => { + loadTestFile(require.resolve('./customizations')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/field_formats/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/field_formats/index.ts new file mode 100644 index 0000000000000..ee14ac9060c40 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/field_formats/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + + // Flaky in serverless tests + describe.skip('Field formats example', function () { + before(async () => { + await PageObjects.common.navigateToApp('fieldFormatsExample'); + }); + + it('renders field formats example 1', async () => { + const formattedValues = await Promise.all( + ( + await testSubjects.findAll('example1 sample formatted') + ).map((wrapper) => wrapper.getVisibleText()) + ); + expect(formattedValues).to.eql(['1000.00B', '97.66KB', '95.37MB']); + }); + + it('renders field formats example 2', async () => { + const formattedValues = await Promise.all( + ( + await testSubjects.findAll('example2 sample formatted') + ).map((wrapper) => wrapper.getVisibleText()) + ); + expect(formattedValues).to.eql(['$1,000.00', '$100,000.00', '$100,000,000.00']); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/partial_results/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/partial_results/index.ts new file mode 100644 index 0000000000000..e93f275a0269d --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/partial_results/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + + // FLAKY: https://github.com/elastic/kibana/issues/165563 + describe.skip('Partial Results Example', function () { + before(async () => { + await PageObjects.common.navigateToApp('partialResultsExample'); + + const element = await testSubjects.find('example-help'); + + await element.click(); + await element.click(); + await element.click(); + }); + + it('should trace mouse events', async () => { + const events = await Promise.all( + ( + await testSubjects.findAll('example-column-event') + ).map((wrapper) => wrapper.getVisibleText()) + ); + expect(events).to.eql(['mousedown', 'mouseup', 'click']); + }); + + it('should keep track of the events number', async () => { + const counters = await Promise.all( + ( + await testSubjects.findAll('example-column-count') + ).map((wrapper) => wrapper.getVisibleText()) + ); + expect(counters).to.eql(['3', '3', '3']); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search/index.ts new file mode 100644 index 0000000000000..c9997f5f41ce9 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/search/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Search examples', () => { + loadTestFile(require.resolve('./warnings')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search/warnings.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search/warnings.ts new file mode 100644 index 0000000000000..b1e5be5f9f835 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/search/warnings.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; +import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; +import assert from 'assert'; +import type { WebElementWrapper } from '../../../../../../../test/functional/services/lib/web_element_wrapper'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'timePicker']); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const retry = getService('retry'); + const es = getService('es'); + const log = getService('log'); + const indexPatterns = getService('indexPatterns'); + const comboBox = getService('comboBox'); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + + // Failing: See https://github.com/elastic/kibana/issues/165623 + describe.skip('handling warnings with search source fetch', function () { + const dataViewTitle = 'sample-01,sample-01-rollup'; + const fromTime = 'Jun 17, 2022 @ 00:00:00.000'; + const toTime = 'Jun 23, 2022 @ 00:00:00.000'; + const testArchive = 'test/functional/fixtures/es_archiver/search/downsampled'; + const testIndex = 'sample-01'; + const testRollupIndex = 'sample-01-rollup'; + const testRollupField = 'kubernetes.container.memory.usage.bytes'; + const toastsSelector = '[data-test-subj=globalToastList] [data-test-subj=euiToastHeader]'; + const shardFailureType = 'unsupported_aggregation_on_downsampled_index'; + const shardFailureReason = `Field [${testRollupField}] of type [aggregate_metric_double] is not supported for aggregation [percentiles]`; + + const getTestJson = async (tabTestSubj: string, codeTestSubj: string) => { + log.info(`switch to ${tabTestSubj} tab...`); + await testSubjects.click(tabTestSubj); + const block = await testSubjects.find(codeTestSubj); + const testText = (await block.getVisibleText()).trim(); + return testText && JSON.parse(testText); + }; + + before(async () => { + // create rollup data + log.info(`loading ${testIndex} index...`); + await esArchiver.loadIfNeeded(testArchive); + log.info(`add write block to ${testIndex} index...`); + await es.indices.addBlock({ index: testIndex, block: 'write' }); + try { + log.info(`rolling up ${testIndex} index...`); + // es client currently does not have method for downsample + await es.transport.request({ + method: 'POST', + path: '/sample-01/_downsample/sample-01-rollup', + body: { fixed_interval: '1h' }, + }); + } catch (err) { + log.info(`ignoring resource_already_exists_exception...`); + if (!err.message.match(/resource_already_exists_exception/)) { + throw err; + } + } + + log.info(`creating ${dataViewTitle} data view...`); + await indexPatterns.create( + { + title: dataViewTitle, + timeFieldName: '@timestamp', + }, + { override: true } + ); + await kibanaServer.uiSettings.update({ + 'dateFormat:tz': 'UTC', + defaultIndex: '0ae0bc7a-e4ca-405c-ab67-f2b5913f2a51', + 'timepicker:timeDefaults': '{ "from": "now-1y", "to": "now" }', + }); + + await PageObjects.common.navigateToApp('searchExamples'); + }); + + beforeEach(async () => { + await comboBox.setCustom('dataViewSelector', dataViewTitle); + await comboBox.set('searchMetricField', testRollupField); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + }); + + after(async () => { + await es.indices.delete({ index: [testIndex, testRollupIndex] }); + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.uiSettings.replace({}); + }); + + afterEach(async () => { + await PageObjects.common.clearAllToasts(); + }); + + it('shows shard failure warning notifications by default', async () => { + await testSubjects.click('searchSourceWithOther'); + + // wait for response - toasts appear before the response is rendered + let response: estypes.SearchResponse | undefined; + await retry.try(async () => { + response = await getTestJson('responseTab', 'responseCodeBlock'); + expect(response).not.to.eql({}); + }); + + // toasts + const toasts = await find.allByCssSelector(toastsSelector); + expect(toasts.length).to.be(2); + const expects = ['2 of 4 shards failed', 'Query result']; + await asyncForEach(toasts, async (t, index) => { + expect(await t.getVisibleText()).to.eql(expects[index]); + }); + + // click "see full error" button in the toast + const [openShardModalButton] = await testSubjects.findAll('openShardFailureModalBtn'); + await openShardModalButton.click(); + const modalHeader = await testSubjects.find('shardFailureModalTitle'); + expect(await modalHeader.getVisibleText()).to.be('2 of 4 shards failed'); + // request + await testSubjects.click('shardFailuresModalRequestButton'); + const requestBlock = await testSubjects.find('shardsFailedModalRequestBlock'); + expect(await requestBlock.getVisibleText()).to.contain(testRollupField); + // response + await testSubjects.click('shardFailuresModalResponseButton'); + const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock'); + expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason); + + await testSubjects.click('closeShardFailureModal'); + + // response tab + assert(response && response._shards.failures); + expect(response._shards.total).to.be(4); + expect(response._shards.successful).to.be(2); + expect(response._shards.skipped).to.be(0); + expect(response._shards.failed).to.be(2); + expect(response._shards.failures.length).to.equal(1); + expect(response._shards.failures[0].index).to.equal(testRollupIndex); + expect(response._shards.failures[0].reason.type).to.equal(shardFailureType); + expect(response._shards.failures[0].reason.reason).to.equal(shardFailureReason); + + // warnings tab + const warnings = await getTestJson('warningsTab', 'warningsCodeBlock'); + expect(warnings).to.eql([]); + }); + + it('able to handle shard failure warnings and prevent default notifications', async () => { + await testSubjects.click('searchSourceWithoutOther'); + + // wait for toasts - toasts appear after the response is rendered + let toasts: WebElementWrapper[] = []; + await retry.try(async () => { + toasts = await find.allByCssSelector(toastsSelector); + expect(toasts.length).to.be(2); + }); + const expects = ['Query result', '2 of 4 shards failed']; + await asyncForEach(toasts, async (t, index) => { + expect(await t.getVisibleText()).to.eql(expects[index]); + }); + + // click "see full error" button in the toast + const [openShardModalButton] = await testSubjects.findAll('openShardFailureModalBtn'); + await openShardModalButton.click(); + const modalHeader = await testSubjects.find('shardFailureModalTitle'); + expect(await modalHeader.getVisibleText()).to.be('2 of 4 shards failed'); + // request + await testSubjects.click('shardFailuresModalRequestButton'); + const requestBlock = await testSubjects.find('shardsFailedModalRequestBlock'); + expect(await requestBlock.getVisibleText()).to.contain(testRollupField); + // response + await testSubjects.click('shardFailuresModalResponseButton'); + const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock'); + expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason); + + await testSubjects.click('closeShardFailureModal'); + + // response tab + const response = await getTestJson('responseTab', 'responseCodeBlock'); + expect(response._shards.total).to.be(4); + expect(response._shards.successful).to.be(2); + expect(response._shards.skipped).to.be(0); + expect(response._shards.failed).to.be(2); + expect(response._shards.failures.length).to.equal(1); + expect(response._shards.failures[0].index).to.equal(testRollupIndex); + expect(response._shards.failures[0].reason.type).to.equal(shardFailureType); + expect(response._shards.failures[0].reason.reason).to.equal(shardFailureReason); + + // warnings tab + const warnings = await getTestJson('warningsTab', 'warningsCodeBlock'); + expect(warnings).to.eql([ + { + type: 'shard_failure', + message: '2 of 4 shards failed', + reason: { reason: shardFailureReason, type: shardFailureType }, + text: 'The data might be incomplete or wrong.', + }, + ]); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/index.ts new file mode 100644 index 0000000000000..d0ea8f6131159 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// TODO: Changed from PluginFunctionalProviderContext to FtrProviderContext in Serverless +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + describe('search examples', function () { + before(async () => { + // TODO: emptyKibanaIndex fails in Serverless with + // "index_not_found_exception: no such index [.kibana_ingest]", + // so it was switched to `savedObjects.cleanStandardList()` + await kibanaServer.savedObjects.cleanStandardList(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' + ); // need at least one index pattern + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' + ); + }); + + // TODO: Removed `search_session_example` since + // search sessions are not supported in Serverless + loadTestFile(require.resolve('./search_example')); + // TODO: Removed `search_sessions_cache` since + // search sessions are not supported in Serverless + loadTestFile(require.resolve('./partial_results_example')); + // TODO: Removed `sql_search_example` since + // SQL is not supported in Serverless + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts new file mode 100644 index 0000000000000..93364f85cc94f --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + const retry = getService('retry'); + + describe('Partial results example', () => { + before(async () => { + await PageObjects.common.navigateToApp('searchExamples'); + await testSubjects.click('/search'); + }); + + it('should update a progress bar', async () => { + await testSubjects.click('responseTab'); + const progressBar = await testSubjects.find('progressBar'); + + const value = await progressBar.getAttribute('value'); + expect(value).to.be('0'); + + await testSubjects.click('requestFibonacci'); + + await retry.waitFor('update progress bar', async () => { + const newValue = await progressBar.getAttribute('value'); + return parseFloat(newValue) > 0; + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/search_example.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/search_example.ts new file mode 100644 index 0000000000000..5638afc5b106d --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/search_example.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 expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'timePicker']); + const retry = getService('retry'); + const comboBox = getService('comboBox'); + const toasts = getService('toasts'); + + // Failing: See https://github.com/elastic/kibana/issues/165730 + describe.skip('Search example', () => { + describe('with bfetch', () => { + testSearchExample(); + }); + + describe('no bfetch', () => { + const kibanaServer = getService('kibanaServer'); + before(async () => { + await kibanaServer.uiSettings.replace({ + 'bfetch:disable': true, + }); + }); + after(async () => { + await kibanaServer.uiSettings.unset('bfetch:disable'); + }); + + testSearchExample(); + }); + + const appId = 'searchExamples'; + + function testSearchExample() { + before(async function () { + await PageObjects.common.navigateToApp(appId, { insertTimestamp: false }); + await comboBox.setCustom('dataViewSelector', 'logstash-*'); + await comboBox.set('searchBucketField', 'geo.src'); + await comboBox.set('searchMetricField', 'memory'); + await PageObjects.timePicker.setAbsoluteRange( + 'Mar 1, 2015 @ 00:00:00.000', + 'Nov 1, 2015 @ 00:00:00.000' + ); + }); + + beforeEach(async () => { + await toasts.dismissAllToasts(); + await retry.waitFor('toasts gone', async () => { + return (await toasts.getToastCount()) === 0; + }); + }); + + it('should have an other bucket', async () => { + await testSubjects.click('searchSourceWithOther'); + await testSubjects.click('responseTab'); + const codeBlock = await testSubjects.find('responseCodeBlock'); + await retry.waitFor('get code block', async () => { + const visibleText = await codeBlock.getVisibleText(); + const parsedResponse = JSON.parse(visibleText); + const buckets = parsedResponse.aggregations[1].buckets; + return ( + buckets.length === 3 && buckets[2].key === '__other__' && buckets[2].doc_count === 9039 + ); + }); + }); + + it('should not have an other bucket', async () => { + await testSubjects.click('searchSourceWithoutOther'); + await testSubjects.click('responseTab'); + const codeBlock = await testSubjects.find('responseCodeBlock'); + await retry.waitFor('get code block', async () => { + const visibleText = await codeBlock.getVisibleText(); + const parsedResponse = JSON.parse(visibleText); + const buckets = parsedResponse.aggregations[1].buckets; + return buckets.length === 2; + }); + }); + + it('should handle warnings', async () => { + await testSubjects.click('searchWithWarning'); + await retry.waitFor('', async () => { + const toastCount = await toasts.getToastCount(); + return toastCount > 1; + }); + const warningToast = await toasts.getToastElement(2); + const textEl = await warningToast.findByTestSubject('euiToastBody'); + const text: string = await textEl.getVisibleText(); + expect(text).to.contain('Watch out!'); + }); + } + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/existing_fields.ts b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/existing_fields.ts new file mode 100644 index 0000000000000..184b7b5d788a0 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/existing_fields.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +const TEST_START_TIME = 'Jan 2, 2021 @ 00:00:00.000'; +const TEST_END_TIME = 'Jan 2, 2022 @ 00:00:00.000'; +const metaFields = ['_id', '_index', '_score']; + +const fieldsWithData = [ + 'ts', + 'filter_field', + 'textfield1', + 'textfield2', + 'mapping_runtime_field', + 'data_view_runtime_field', +]; + +export default ({ getService, getPageObjects }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const comboBox = getService('comboBox'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const monacoEditor = getService('monacoEditor'); + const PageObjects = getPageObjects(['common', 'timePicker', 'header', 'unifiedFieldList']); + const dataViewTitle = 'existence_index_*'; + + async function addDSLFilter(value: string) { + await testSubjects.click('addFilter'); + await testSubjects.click('editQueryDSL'); + await monacoEditor.waitCodeEditorReady('addFilterPopover'); + await monacoEditor.setCodeEditorValue(value); + await testSubjects.scrollIntoView('saveFilter'); + await testSubjects.clickWhenNotDisabled('saveFilter'); + await retry.try(async () => { + await testSubjects.waitForDeleted('saveFilter'); + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + + async function removeAllDSLFilters() { + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-removeAllFilters'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + } + + describe('Fields existence info', () => { + before(async () => { + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/index_patterns/constant_keyword' + ); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/index_patterns/constant_keyword.json' + ); + await PageObjects.common.navigateToApp('unifiedFieldListExamples'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitFor('combobox is ready', async () => { + return await testSubjects.exists('dataViewSelector'); + }); + await comboBox.setCustom('dataViewSelector', dataViewTitle); + await retry.waitFor('page is ready', async () => { + return await testSubjects.exists('globalQueryBar'); + }); + await PageObjects.timePicker.setAbsoluteRange(TEST_START_TIME, TEST_END_TIME); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + await PageObjects.unifiedFieldList.toggleSidebarSection('meta'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/constant_keyword' + ); + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/index_patterns/constant_keyword.json' + ); + await PageObjects.unifiedFieldList.cleanSidebarLocalStorage(); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + describe('existence', () => { + it('should find which fields exist in the sample documents', async () => { + const sidebarFields = await PageObjects.unifiedFieldList.getAllFieldNames(); + expect(sidebarFields.sort()).to.eql([...metaFields, ...fieldsWithData].sort()); + }); + + it('should return fields filtered by term query', async () => { + const expectedFieldNames = [ + 'ts', + 'filter_field', + 'textfield1', + // textfield2 and mapping_runtime_field are defined on the other index + 'data_view_runtime_field', + ]; + + await addDSLFilter(`{ + "bool": { + "filter": [{ "term": { "filter_field": "a" } }] + } + }`); + + const sidebarFields = await PageObjects.unifiedFieldList.getAllFieldNames(); + expect(sidebarFields.sort()).to.eql([...metaFields, ...expectedFieldNames].sort()); + + await removeAllDSLFilters(); + }); + + it('should return fields filtered by match_phrase query', async () => { + const expectedFieldNames = [ + 'ts', + 'filter_field', + 'textfield1', + // textfield2 and mapping_runtime_field are defined on the other index + 'data_view_runtime_field', + ]; + + await addDSLFilter(`{ + "bool": { + "filter": [{ "match_phrase": { "filter_field": "a" } }] + } + }`); + + const sidebarFields = await PageObjects.unifiedFieldList.getAllFieldNames(); + expect(sidebarFields.sort()).to.eql([...metaFields, ...expectedFieldNames].sort()); + + await removeAllDSLFilters(); + }); + + it('should return fields filtered by time range', async () => { + const expectedFieldNames = [ + 'ts', + 'filter_field', + 'textfield1', + // textfield2 and mapping_runtime_field are defined on the other index + 'data_view_runtime_field', + ]; + + await addDSLFilter(`{ + "bool": { + "filter": [{ "term": { "filter_field": "a" } }] + } + }`); + + await PageObjects.timePicker.setAbsoluteRange( + TEST_START_TIME, + 'Dec 12, 2021 @ 00:00:00.000' + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + + const sidebarFields = await PageObjects.unifiedFieldList.getAllFieldNames(); + expect(sidebarFields.sort()).to.eql([...metaFields, ...expectedFieldNames].sort()); + + await removeAllDSLFilters(); + }); + }); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/field_stats.ts b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/field_stats.ts new file mode 100644 index 0000000000000..1df48eed373de --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/field_stats.ts @@ -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 expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +const TEST_START_TIME = 'Sep 19, 2015 @ 06:31:44.000'; +const TEST_END_TIME = 'Sep 23, 2015 @ 18:31:44.000'; + +export default ({ getService, getPageObjects }: FtrProviderContext) => { + const PageObjects = getPageObjects(['common', 'timePicker', 'header', 'unifiedFieldList']); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const comboBox = getService('comboBox'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); + const dataViewTitle = 'logstash-2015.09.22'; + + describe('Field stats', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/visualize/default' + ); + // TODO: Loading this from `es_archives` in `test_serverless` + // instead since minor modifications were required + await esArchiver.loadIfNeeded( + 'x-pack/test_serverless/functional/es_archives/pre_calculated_histogram' + ); + await PageObjects.common.navigateToApp('unifiedFieldListExamples'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitFor('combobox is ready', async () => { + return await testSubjects.exists('dataViewSelector'); + }); + await comboBox.setCustom('dataViewSelector', dataViewTitle); + await retry.waitFor('page is ready', async () => { + return await testSubjects.exists('globalQueryBar'); + }); + await PageObjects.timePicker.setAbsoluteRange(TEST_START_TIME, TEST_END_TIME); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + // TODO: Loading this from `es_archives` in `test_serverless` + // instead since minor modifications were required + await esArchiver.unload( + 'x-pack/test_serverless/functional/es_archives/pre_calculated_histogram' + ); + await kibanaServer.savedObjects.cleanStandardList(); + await PageObjects.unifiedFieldList.cleanSidebarLocalStorage(); + }); + + describe('field distribution', () => { + before(async () => { + await PageObjects.unifiedFieldList.toggleSidebarSection('empty'); // it will allow to render more fields in Available fields section + }); + + it('should return an auto histogram for numbers and top values', async () => { + await PageObjects.unifiedFieldList.clickFieldListItem('bytes'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be( + 'topValuesAndDistribution' + ); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(4634); + expect(await PageObjects.unifiedFieldList.getFieldStatsTopValueBucketsVisibleText()).to.be( + '0\n3.2%\n3,954\n0.1%\n5,846\n0.1%\n6,497\n0.1%\n1,840\n0.1%\n4,206\n0.1%\n4,328\n0.1%\n4,669\n0.1%\n5,863\n0.1%\n6,631\n0.1%\nOther\n96.0%' + ); + }); + + it('should return an auto histogram for dates', async () => { + await PageObjects.unifiedFieldList.clickFieldListItem('@timestamp'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be( + 'timeDistribution' + ); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(4634); + }); + + it('should return top values for strings', async () => { + await PageObjects.unifiedFieldList.clickFieldListItem('geo.src'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be('topValues'); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(4634); + expect(await PageObjects.unifiedFieldList.getFieldStatsTopValueBucketsVisibleText()).to.be( + 'CN\n18.0%\nIN\n17.4%\nUS\n9.2%\nID\n3.4%\nBR\n3.1%\nPK\n2.5%\nBD\n2.3%\nNG\n2.0%\nRU\n1.8%\nJP\n1.6%\nOther\n38.8%' + ); + }); + + it('should return top values for ip fields', async () => { + await PageObjects.unifiedFieldList.clickFieldListItem('ip'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be('topValues'); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(4634); + expect(await PageObjects.unifiedFieldList.getFieldStatsTopValueBucketsVisibleText()).to.be( + '177.194.175.66\n0.3%\n18.55.141.62\n0.3%\n53.55.251.105\n0.3%\n21.111.249.239\n0.2%\n97.63.84.25\n0.2%\n100.99.207.174\n0.2%\n112.34.138.226\n0.2%\n194.68.89.92\n0.2%\n235.186.79.201\n0.2%\n57.79.108.136\n0.2%\nOther\n97.6%' + ); + }); + + // TODO: Scripted fields tests dropped since they're not supported in Serverless + + it('should return examples for non-aggregatable or geo fields', async () => { + await PageObjects.unifiedFieldList.clickFieldListItem('geo.coordinates'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be('exampleValues'); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(100); + // actual hits might vary + expect( + (await PageObjects.unifiedFieldList.getFieldStatsExampleBucketsVisibleText()).length + ).to.above(0); + }); + + it('should return top values for index pattern runtime string fields', async () => { + await PageObjects.unifiedFieldList.clickFieldListItem('runtime_string_field'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be('topValues'); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(4634); + expect(await PageObjects.unifiedFieldList.getFieldStatsTopValueBucketsVisibleText()).to.be( + 'hello world!\n100%' + ); + }); + + it('should apply filters and queries', async () => { + await filterBar.addFilter({ field: 'geo.src', operation: 'is', value: 'US' }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItem('bytes'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be( + 'topValuesAndDistribution' + ); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(425); + await filterBar.removeFilter('geo.src'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + it('should allow filtering on a runtime field other than the field in use', async () => { + await filterBar.addFilter({ field: 'runtime_string_field', operation: 'exists' }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItem('runtime_number_field'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be('topValues'); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(4634); + expect(await PageObjects.unifiedFieldList.getFieldStatsTopValueBucketsVisibleText()).to.be( + '5\n100%' + ); + await filterBar.removeFilter('runtime_string_field'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + }); + + describe('histogram', () => { + before(async () => { + await comboBox.setCustom('dataViewSelector', 'histogram-test'); + await retry.waitFor('page is ready', async () => { + return await testSubjects.exists('globalQueryBar'); + }); + await PageObjects.timePicker.setAbsoluteRange(TEST_START_TIME, TEST_END_TIME); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + }); + + it('should return an auto histogram for precalculated histograms', async () => { + await PageObjects.unifiedFieldList.clickFieldListItem('histogram-content'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be('histogram'); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(7); + }); + + it('should return a single-value histogram when filtering a precalculated histogram', async () => { + await filterBar.addFilter({ + field: 'histogram-title', + operation: 'is', + value: 'single value', + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItem('histogram-content'); + expect(await PageObjects.unifiedFieldList.getFieldStatsViewType()).to.be('histogram'); + expect(await PageObjects.unifiedFieldList.getFieldStatsDocsCount()).to.be(1); + await filterBar.removeFilter('histogram-title'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + }); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/index.ts new file mode 100644 index 0000000000000..249aac6f86e0b --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Unified Field List Examples', () => { + loadTestFile(require.resolve('./field_stats')); + loadTestFile(require.resolve('./existing_fields')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/home_page.ts b/x-pack/test_serverless/functional/test_suites/common/home_page.ts index c1b3dad37d292..9660b21d6e7d2 100644 --- a/x-pack/test_serverless/functional/test_suites/common/home_page.ts +++ b/x-pack/test_serverless/functional/test_suites/common/home_page.ts @@ -11,7 +11,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { const svlCommonPage = getPageObject('svlCommonPage'); const svlCommonNavigation = getService('svlCommonNavigation'); - describe('home page', function () { + // Failing: See https://github.com/elastic/kibana/issues/165386 + describe.skip('home page', function () { it('has project header', async () => { await svlCommonNavigation.navigateToKibanaHome(); await svlCommonPage.assertProjectHeaderExists(); diff --git a/x-pack/test_serverless/functional/test_suites/common/index.examples.ts b/x-pack/test_serverless/functional/test_suites/common/index.examples.ts new file mode 100644 index 0000000000000..58ee2e2ac4478 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/index.examples.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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('serverless examples UI', function () { + this.tags('skipMKI'); + loadTestFile(require.resolve('./examples/data_view_field_editor_example')); + loadTestFile(require.resolve('./examples/discover_customization_examples')); + loadTestFile(require.resolve('./examples/field_formats')); + loadTestFile(require.resolve('./examples/partial_results')); + loadTestFile(require.resolve('./examples/search')); + loadTestFile(require.resolve('./examples/search_examples')); + loadTestFile(require.resolve('./examples/unified_field_list_examples')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/index.ts b/x-pack/test_serverless/functional/test_suites/common/index.ts index 08e816c9bdfda..597a1b6f84963 100644 --- a/x-pack/test_serverless/functional/test_suites/common/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/index.ts @@ -13,6 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./management')); // platform security + loadTestFile(require.resolve('./security/api_keys')); loadTestFile(require.resolve('./security/navigation/avatar_menu')); // Management diff --git a/x-pack/test_serverless/functional/test_suites/common/management.ts b/x-pack/test_serverless/functional/test_suites/common/management.ts index fe2561854c49a..7ea23c7be4bb5 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management.ts @@ -11,7 +11,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { const commonPage = getPageObject('common'); const testSubjects = getService('testSubjects'); - describe('Management', function () { + // Flaky in serverless tests + describe.skip('Management', function () { describe('Disabled UIs', () => { const DISABLED_PLUGINS = [ { diff --git a/x-pack/test_serverless/functional/test_suites/common/security/api_keys.ts b/x-pack/test_serverless/functional/test_suites/common/security/api_keys.ts index ab650c4c43a25..32c12c9b713b4 100644 --- a/x-pack/test_serverless/functional/test_suites/common/security/api_keys.ts +++ b/x-pack/test_serverless/functional/test_suites/common/security/api_keys.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; -import type { estypes } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../../ftr_provider_context'; async function clearAllApiKeys(esClient: Client, logger: ToolingLog) { @@ -25,433 +24,39 @@ async function clearAllApiKeys(esClient: Client, logger: ToolingLog) { } export default ({ getPageObjects, getService }: FtrProviderContext) => { - const es = getService('es'); const pageObjects = getPageObjects(['common', 'apiKeys']); - const log = getService('log'); - const security = getService('security'); - const testSubjects = getService('testSubjects'); - const find = getService('find'); const browser = getService('browser'); - const retry = getService('retry'); - - const testRoles: Record = { - viewer: { - cluster: ['all'], - indices: [ - { - names: ['*'], - privileges: ['all'], - allow_restricted_indices: false, - }, - { - names: ['*'], - privileges: ['monitor', 'read', 'view_index_metadata', 'read_cross_cluster'], - allow_restricted_indices: true, - }, - ], - run_as: ['*'], - }, - }; - - const otherUser: estypes.SecurityPutUserRequest = { - username: 'other_user', - password: 'changeme', - roles: ['superuser'], - }; - - async function ensureApiKeysExist(apiKeysNames: string[]) { - await retry.try(async () => { - for (const apiKeyName of apiKeysNames) { - log.debug(`Checking if API key ("${apiKeyName}") exists.`); - await pageObjects.apiKeys.ensureApiKeyExists(apiKeyName); - log.debug(`API key ("${apiKeyName}") exists.`); - } - }); - } + const es = getService('es'); + const log = getService('log'); - describe('Home page', function () { - before(async () => { + // FLAKY: https://github.com/elastic/kibana/issues/165553 + describe.skip('API keys', () => { + after(async () => { await clearAllApiKeys(es, log); - await security.testUser.setRoles(['kibana_admin']); - await es.security.putUser(otherUser); + }); + it('should create and delete API keys correctly', async () => { await pageObjects.common.navigateToUrl('management', 'security/api_keys', { shouldUseHashForSubUrl: false, }); - }); - - after(async () => { - await es.security.deleteUser({ username: otherUser.username }); - await security.testUser.restoreDefaults(); - }); - - // https://www.elastic.co/guide/en/kibana/7.6/api-keys.html#api-keys-security-privileges - it('Hides management link if user is not authorized', async () => { - await testSubjects.missingOrFail('apiKeys'); - }); - - it('Loads the app', async () => { - await security.testUser.setRoles(['test_api_keys']); - log.debug('Checking for Create API key call to action'); - await find.existsByLinkText('Create API key'); - }); - - describe('creates API key', function () { - before(async () => { - await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); - await pageObjects.common.navigateToUrl('management', 'security/api_keys', { - shouldUseHashForSubUrl: false, - }); - - // Delete any API keys created outside of these tests - await pageObjects.apiKeys.bulkDeleteApiKeys(); - }); - - afterEach(async () => { - await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); - }); - - after(async () => { - await clearAllApiKeys(es, log); - }); - - it('when submitting form, close dialog and displays new api key', async () => { - const apiKeyName = 'Happy API Key'; - await pageObjects.apiKeys.clickOnPromptCreateApiKey(); - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys/create'); - expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Create API key'); - - await pageObjects.apiKeys.setApiKeyName(apiKeyName); - await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); - const newApiKeyCreation = await pageObjects.apiKeys.getNewApiKeyCreation(); - - expect(await browser.getCurrentUrl()).to.not.contain( - 'app/management/security/api_keys/flyout' - ); - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); - expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false); - expect(newApiKeyCreation).to.be(`Created API key '${apiKeyName}'`); - }); - - it('with optional expiration, redirects back and displays base64', async () => { - const apiKeyName = 'Happy expiration API key'; - await pageObjects.apiKeys.clickOnPromptCreateApiKey(); - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys/create'); - - await pageObjects.apiKeys.setApiKeyName(apiKeyName); - await pageObjects.apiKeys.toggleCustomExpiration(); - await pageObjects.apiKeys.setApiKeyCustomExpiration('12'); - await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); - const newApiKeyCreation = await pageObjects.apiKeys.getNewApiKeyCreation(); - - expect(await browser.getCurrentUrl()).to.not.contain( - 'app/management/security/api_keys/create' - ); - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); - expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false); - expect(newApiKeyCreation).to.be(`Created API key '${apiKeyName}'`); - }); - }); - - describe('Update API key', function () { - before(async () => { - await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); - await pageObjects.common.navigateToUrl('management', 'security/api_keys', { - shouldUseHashForSubUrl: false, - }); - - // Delete any API keys created outside these tests - await pageObjects.apiKeys.bulkDeleteApiKeys(); - }); - - afterEach(async () => { - await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); - }); - - after(async () => { - await clearAllApiKeys(es, log); - }); - - it('should create a new API key, click the name of the new row, fill out and submit form, and display success message', async () => { - // Create a key to updated - const apiKeyName = 'Happy API Key to Update'; - - await es.security.grantApiKey({ - api_key: { - name: apiKeyName, - expiration: '1d', - }, - grant_type: 'password', - run_as: 'elastic', - username: 'elastic', - password: 'changeme', - }); - - await browser.refresh(); - - log.debug('API key created, moving on to update'); - - // Update newly created API Key - await pageObjects.apiKeys.clickExistingApiKeyToOpenFlyout(apiKeyName); - - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); - - await pageObjects.apiKeys.waitForSubmitButtonOnApiKeyFlyoutEnabled(); - - expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Update API key'); - - // Verify name input box are disabled - const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); - expect(await apiKeyNameInput.isEnabled()).to.be(false); - - // Status should be displayed - const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); - expect(await apiKeyStatus).to.be('Expires in a day'); - - // Verify metadata is editable - const apiKeyMetadataSwitch = await pageObjects.apiKeys.getMetadataSwitch(); - expect(await apiKeyMetadataSwitch.isEnabled()).to.be(true); - - // Verify restrict privileges is editable - const apiKeyRestrictPrivilegesSwitch = - await pageObjects.apiKeys.getRestrictPrivilegesSwitch(); - expect(await apiKeyRestrictPrivilegesSwitch.isEnabled()).to.be(true); - - // Toggle restrict privileges so the code editor shows up - await apiKeyRestrictPrivilegesSwitch.click(); - - // Toggle metadata switch so the code editor shows up - await apiKeyMetadataSwitch.click(); - - // Check default value of metadata and set value - const restrictPrivilegesCodeEditorValue = - await pageObjects.apiKeys.getCodeEditorValueByIndex(0); - expect(restrictPrivilegesCodeEditorValue).to.be('{}'); - - // Check default value of metadata and set value - const metadataCodeEditorValue = await pageObjects.apiKeys.getCodeEditorValueByIndex(1); - expect(metadataCodeEditorValue).to.be('{}'); - - await pageObjects.apiKeys.setCodeEditorValueByIndex(0, JSON.stringify(testRoles)); - - await pageObjects.apiKeys.setCodeEditorValueByIndex(1, '{"name":"metadataTest"}'); - - // Submit values to update API key - await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); - - // Get success message - const updatedApiKeyToastText = await pageObjects.apiKeys.getApiKeyUpdateSuccessToast(); - expect(updatedApiKeyToastText).to.be(`Updated API key '${apiKeyName}'`); - - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); - expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false); - }); - }); - - describe('Readonly API key', function () { - before(async () => { - await security.role.create('read_security_role', { - elasticsearch: { - cluster: ['read_security'], - }, - kibana: [ - { - feature: { - infrastructure: ['read'], - }, - spaces: ['*'], - }, - ], - }); - - await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); - await pageObjects.common.navigateToUrl('management', 'security/api_keys', { - shouldUseHashForSubUrl: false, - }); - - // Delete any API keys created outside these tests - await pageObjects.apiKeys.bulkDeleteApiKeys(); - }); - - afterEach(async () => { - await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); - }); - - after(async () => { - await clearAllApiKeys(es, log); - }); - - // Serverless tests run as elastic (superuser) so unable to test `read_security` permissions - it.skip('should see readonly form elements', async () => { - // Create a key to updated - const apiKeyName = 'Happy API Key to View'; - - await es.security.grantApiKey({ - api_key: { - name: apiKeyName, - expiration: '1d', - metadata: { name: 'metadatatest' }, - role_descriptors: { ...testRoles }, - }, - grant_type: 'password', - run_as: 'elastic', - username: 'elastic', - password: 'changeme', - }); - - await browser.refresh(); - - log.debug('API key created, moving on to view'); - - // Set testUsers roles to have the `read_security` cluster privilege - await security.testUser.setRoles(['read_security_role']); - - // View newly created API Key - await pageObjects.apiKeys.clickExistingApiKeyToOpenFlyout(apiKeyName); - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); - expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); - - // Verify name input box are disabled - const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); - expect(await apiKeyNameInput.isEnabled()).to.be(false); - - // Status should be displayed - const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); - expect(await apiKeyStatus).to.be('Expires in a day'); - - const apiKeyMetadataSwitch = await pageObjects.apiKeys.getMetadataSwitch(); - const apiKeyRestrictPrivilegesSwitch = - await pageObjects.apiKeys.getRestrictPrivilegesSwitch(); - - // Verify metadata and restrict privileges switches are now disabled - expect(await apiKeyMetadataSwitch.isEnabled()).to.be(false); - expect(await apiKeyRestrictPrivilegesSwitch.isEnabled()).to.be(false); - - // Close flyout with cancel - await pageObjects.apiKeys.clickCancelButtonOnApiKeyFlyout(); - - // Undo `read_security_role` - await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); - }); - - it('should show the `API key details` flyout if the expiration date is passed', async () => { - const apiKeyName = 'expired-key'; - - await es.security.grantApiKey({ - api_key: { - name: apiKeyName, - expiration: '1ms', - }, - grant_type: 'password', - run_as: 'elastic', - username: 'elastic', - password: 'changeme', - }); - - await browser.refresh(); - - log.debug('API key created, moving on to view'); - - await pageObjects.apiKeys.clickExistingApiKeyToOpenFlyout(apiKeyName); - - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); - expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); - - // Verify name input box are disabled - const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); - expect(await apiKeyNameInput.isEnabled()).to.be(false); - - // Status should be displayed - const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); - expect(await apiKeyStatus).to.be('Expired'); - - const apiKeyMetadataSwitch = await pageObjects.apiKeys.getMetadataSwitch(); - const apiKeyRestrictPrivilegesSwitch = - await pageObjects.apiKeys.getRestrictPrivilegesSwitch(); - - // Verify metadata and restrict privileges switches are now disabled - expect(await apiKeyMetadataSwitch.isEnabled()).to.be(false); - expect(await apiKeyRestrictPrivilegesSwitch.isEnabled()).to.be(false); - - await pageObjects.apiKeys.clickCancelButtonOnApiKeyFlyout(); - }); - - it('should show the `API key details flyout` if the API key does not belong to the user', async () => { - const apiKeyName = 'other-key'; - - await es.security.grantApiKey({ - api_key: { - name: apiKeyName, - }, - grant_type: 'password', - run_as: otherUser.username, - username: 'elastic', - password: 'changeme', - }); - - await browser.refresh(); - - log.debug('API key created, moving on to view'); - await pageObjects.apiKeys.clickExistingApiKeyToOpenFlyout(apiKeyName); - expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); - expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); + const apiKeyName = 'Happy API Key'; + await pageObjects.apiKeys.clickOnPromptCreateApiKey(); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys/create'); + expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Create API key'); - // Verify name input box are disabled - const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); - expect(await apiKeyNameInput.isEnabled()).to.be(false); + await pageObjects.apiKeys.setApiKeyName(apiKeyName); + await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); + const newApiKeyCreation = await pageObjects.apiKeys.getNewApiKeyCreation(); - // Status should be displayed - const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); - expect(await apiKeyStatus).to.be('Active'); + expect(await browser.getCurrentUrl()).to.not.contain( + 'app/management/security/api_keys/create' + ); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); + expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false); + expect(newApiKeyCreation).to.be(`Created API key '${apiKeyName}'`); - const apiKeyMetadataSwitch = await pageObjects.apiKeys.getMetadataSwitch(); - const apiKeyRestrictPrivilegesSwitch = - await pageObjects.apiKeys.getRestrictPrivilegesSwitch(); - - // Verify metadata and restrict privileges switches are now disabled - expect(await apiKeyMetadataSwitch.isEnabled()).to.be(false); - expect(await apiKeyRestrictPrivilegesSwitch.isEnabled()).to.be(false); - - await pageObjects.apiKeys.clickCancelButtonOnApiKeyFlyout(); - }); - }); - - describe('deletes API key(s)', function () { - before(async () => { - await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); - await pageObjects.common.navigateToUrl('management', 'security/api_keys', { - shouldUseHashForSubUrl: false, - }); - }); - - beforeEach(async () => { - await pageObjects.apiKeys.clickOnPromptCreateApiKey(); - await pageObjects.apiKeys.setApiKeyName('api key 1'); - await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); - await ensureApiKeysExist(['api key 1']); - }); - - it('one by one', async () => { - await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); - expect(await pageObjects.apiKeys.getApiKeysFirstPromptTitle()).to.be( - 'Create your first API key' - ); - }); - - it('by bulk', async () => { - await pageObjects.apiKeys.clickOnTableCreateApiKey(); - await pageObjects.apiKeys.setApiKeyName('api key 2'); - await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); - - // Make sure all API keys we want to delete are created and rendered. - await ensureApiKeysExist(['api key 1', 'api key 2']); - - await pageObjects.apiKeys.bulkDeleteApiKeys(); - expect(await pageObjects.apiKeys.getApiKeysFirstPromptTitle()).to.be( - 'Create your first API key' - ); - }); + await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); }); }); }; diff --git a/x-pack/test_serverless/functional/test_suites/common/security/navigation/avatar_menu.ts b/x-pack/test_serverless/functional/test_suites/common/security/navigation/avatar_menu.ts index b8647d0423e02..0a58a580b81dd 100644 --- a/x-pack/test_serverless/functional/test_suites/common/security/navigation/avatar_menu.ts +++ b/x-pack/test_serverless/functional/test_suites/common/security/navigation/avatar_menu.ts @@ -11,7 +11,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { const svlCommonPage = getPageObject('svlCommonPage'); const svlCommonNavigation = getService('svlCommonNavigation'); - describe('Avatar menu', function () { + // FLAKY: https://github.com/elastic/kibana/issues/165694 + describe.skip('Avatar menu', function () { it('is displayed', async () => { await svlCommonNavigation.navigateToKibanaHome(); await svlCommonPage.assertUserAvatarExists(); diff --git a/x-pack/test_serverless/functional/test_suites/observability/config.examples.ts b/x-pack/test_serverless/functional/test_suites/observability/config.examples.ts new file mode 100644 index 0000000000000..358dddbe89aca --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/config.examples.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { REPO_ROOT } from '@kbn/repo-info'; +import { findTestPluginPaths } from '@kbn/test'; +import { resolve } from 'path'; +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'oblt', + testFiles: [require.resolve('../common/index.examples')], + junit: { + reportName: 'Serverless Observability Examples Functional Tests', + }, + kbnServerArgs: findTestPluginPaths([ + resolve(REPO_ROOT, 'examples'), + resolve(REPO_ROOT, 'x-pack/examples'), + ]), +}); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cypress/cypress.config.ts b/x-pack/test_serverless/functional/test_suites/observability/cypress/cypress.config.ts index 52eaff1a67792..b570ec174db30 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cypress/cypress.config.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cypress/cypress.config.ts @@ -6,14 +6,14 @@ */ import { defineCypressConfig } from '@kbn/cypress-config'; -import { kbnTestConfig } from '@kbn/test'; +import { kbnTestConfig, kibanaTestSuperuserServerless } from '@kbn/test'; import Url from 'url'; const kibanaUrlWithoutAuth = Url.format({ - protocol: kbnTestConfig.getUrlParts().protocol, - hostname: kbnTestConfig.getUrlParts().hostname, - port: kbnTestConfig.getUrlParts().port, + protocol: 'https', + hostname: kbnTestConfig.getUrlParts(kibanaTestSuperuserServerless).hostname, + port: kbnTestConfig.getUrlParts(kibanaTestSuperuserServerless).port, }); export default defineCypressConfig({ @@ -35,13 +35,13 @@ export default defineCypressConfig({ runMode: 1, }, e2e: { - baseUrl: 'http://localhost:5620', + baseUrl: 'https://localhost:5620', supportFile: './support/e2e.ts', specPattern: './e2e/**/*.cy.ts', }, env: { - username: kbnTestConfig.getUrlParts().username, - password: kbnTestConfig.getUrlParts().password, + username: kbnTestConfig.getUrlParts(kibanaTestSuperuserServerless).username, + password: kbnTestConfig.getUrlParts(kibanaTestSuperuserServerless).password, kibanaUrlWithoutAuth, }, }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts b/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts index 84abae3258cff..1e32185187f8d 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts @@ -5,13 +5,15 @@ * 2.0. */ -describe('Serverless', () => { +// Flaky in serverless tests +describe.skip('Serverless', () => { beforeEach(() => { cy.loginAsElasticUser(); }); it('contains the side navigation for observabilitity serverless', () => { - cy.contains('Discover'); + cy.loginAsElasticUser(); + cy.contains('Log Explorer'); cy.contains('Dashboards'); cy.contains('Alerts'); cy.contains('AIOps'); @@ -22,8 +24,10 @@ describe('Serverless', () => { }); it('navigates to discover-dashboard-viz links', () => { - cy.contains('Discover').click(); - cy.url().should('include', '/app/discover'); + cy.loginAsElasticUser(); + + cy.contains('Log Explorer').click(); + cy.url().should('include', '/app/observability-log-explorer'); cy.contains('Dashboards').click(); cy.url().should('include', '/app/dashboards'); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/observability_onboarding/home.cy.ts b/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/observability_onboarding/home.cy.ts index 7e74c75b62ebc..b0f95e0f66438 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/observability_onboarding/home.cy.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/observability_onboarding/home.cy.ts @@ -5,7 +5,8 @@ * 2.0. */ -describe('[Serverless Observability onboarding] Landing page', () => { +// Flaky in serverless tests +describe.skip('[Serverless Observability onboarding] Landing page', () => { beforeEach(() => { cy.loginAsElasticUser(); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cypress/oblt_config.base.ts b/x-pack/test_serverless/functional/test_suites/observability/cypress/oblt_config.base.ts index 22bd611067f48..5508b8cd1a618 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cypress/oblt_config.base.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cypress/oblt_config.base.ts @@ -16,10 +16,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...svlSharedConfig.getAll(), esTestCluster: { ...svlSharedConfig.get('esTestCluster'), - serverArgs: [ - ...svlSharedConfig.get('esTestCluster.serverArgs'), - 'xpack.security.enabled=true', - ], + serverArgs: [...svlSharedConfig.get('esTestCluster.serverArgs')], }, kbnTestServer: { ...svlSharedConfig.get('kbnTestServer'), diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/customization.ts b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/customization.ts deleted file mode 100644 index a647293a73145..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/customization.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common']); - const testSubjects = getService('testSubjects'); - - describe('Customizations', () => { - before('initialize tests', async () => { - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); - }); - - after('clean up archives', async () => { - await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); - }); - - describe('when Discover is loaded with the log-explorer profile', () => { - it('DatasetSelector should replace the DataViewPicker', async () => { - // Assert does not render on discover app - await PageObjects.common.navigateToApp('discover'); - await testSubjects.missingOrFail('datasetSelectorPopover'); - - // Assert it renders on log-explorer profile - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); - await testSubjects.existOrFail('datasetSelectorPopover'); - }); - - it('the TopNav bar should hide New, Open and Save options', async () => { - // Assert does not render on discover app - await PageObjects.common.navigateToApp('discover'); - await testSubjects.existOrFail('discoverNewButton'); - await testSubjects.existOrFail('discoverOpenButton'); - await testSubjects.existOrFail('shareTopNavButton'); - await testSubjects.existOrFail('discoverAlertsButton'); - await testSubjects.existOrFail('openInspectorButton'); - await testSubjects.existOrFail('discoverSaveButton'); - - // Assert it renders on log-explorer profile - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); - await testSubjects.missingOrFail('discoverNewButton'); - await testSubjects.missingOrFail('discoverOpenButton'); - await testSubjects.existOrFail('shareTopNavButton'); - await testSubjects.existOrFail('discoverAlertsButton'); - await testSubjects.existOrFail('openInspectorButton'); - await testSubjects.missingOrFail('discoverSaveButton'); - }); - - it('should render a filter controls section as part of the unified search bar', async () => { - // Assert does not render on discover app - await PageObjects.common.navigateToApp('discover'); - await testSubjects.missingOrFail('datasetFiltersCustomization'); - - // Assert it renders on log-explorer profile - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); - await testSubjects.existOrFail('datasetFiltersCustomization', { allowHidden: true }); - }); - }); - }); -} diff --git a/x-pack/test_serverless/functional/test_suites/observability/index.ts b/x-pack/test_serverless/functional/test_suites/observability/index.ts index f376e49f56daf..e24841e6fbff9 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/index.ts @@ -6,13 +6,12 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import loadDiscoverLogExplorerSuite from './discover_log_explorer'; export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless observability UI', function () { loadTestFile(require.resolve('./landing_page')); loadTestFile(require.resolve('./navigation')); - loadDiscoverLogExplorerSuite(loadTestFile); + loadTestFile(require.resolve('./observability_log_explorer')); loadTestFile(require.resolve('./cases/attachment_framework')); loadTestFile(require.resolve('./cases/configure')); loadTestFile(require.resolve('./cases/list_view')); diff --git a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts index eb56af9f88688..7e84512ab4b0a 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts @@ -35,13 +35,15 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); await svlCommonNavigation.sidenav.expectSectionClosed('project_settings_project_nav'); - // navigate to discover - await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'discover:log-explorer' }); - await svlCommonNavigation.sidenav.expectLinkActive({ deepLinkId: 'discover:log-explorer' }); + // navigate to log explorer + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'observability-log-explorer' }); + await svlCommonNavigation.sidenav.expectLinkActive({ + deepLinkId: 'observability-log-explorer', + }); await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ - deepLinkId: 'discover:log-explorer', + deepLinkId: 'observability-log-explorer', }); - await expect(await browser.getCurrentUrl()).contain('/app/discover'); + await expect(await browser.getCurrentUrl()).contain('/app/observability-log-explorer'); // check the aiops subsection await svlCommonNavigation.sidenav.clickLink({ navId: 'aiops' }); // open ai ops subsection @@ -77,15 +79,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await svlCommonNavigation.sidenav.expectSectionOpen('project_settings_project_nav'); }); - it('navigate using search', async () => { - await svlCommonNavigation.search.showSearch(); - await svlCommonNavigation.search.searchFor('discover log explorer'); - await svlCommonNavigation.search.clickOnOption(0); - await svlCommonNavigation.search.hideSearch(); - - await expect(await browser.getCurrentUrl()).contain('/app/discover#/p/log-explorer'); - }); - it('shows cases in sidebar navigation', async () => { await svlCommonNavigation.expectExists(); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/app.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/app.ts new file mode 100644 index 0000000000000..6a9d76a9b594c --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/app.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['observabilityLogExplorer', 'svlCommonNavigation']); + + describe('Application', () => { + it('is shown in the global search', async () => { + await PageObjects.observabilityLogExplorer.navigateTo(); + await PageObjects.svlCommonNavigation.search.showSearch(); + await PageObjects.svlCommonNavigation.search.searchFor('log explorer'); + + const results = await PageObjects.svlCommonNavigation.search.getDisplayedResults(); + expect(results[0].label).to.eql('Log Explorer'); + + await PageObjects.svlCommonNavigation.search.hideSearch(); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/columns_selection.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts similarity index 69% rename from x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/columns_selection.ts rename to x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts index dfba8f72a699d..92ccb09a27f00 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/columns_selection.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts @@ -5,6 +5,8 @@ * 2.0. */ import expect from '@kbn/expect'; +import rison from '@kbn/rison'; +import querystring from 'querystring'; import { FtrProviderContext } from '../../../ftr_provider_context'; const defaultLogColumns = ['@timestamp', 'message']; @@ -12,24 +14,24 @@ const defaultLogColumns = ['@timestamp', 'message']; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'discover']); + const PageObjects = getPageObjects(['discover', 'observabilityLogExplorer']); describe('Columns selection initialization and update', () => { before(async () => { await esArchiver.load( - 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' ); }); after(async () => { await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' ); }); - describe('when the log explorer profile loads', () => { + describe('when the log explorer loads', () => { it("should initialize the table columns to logs' default selection", async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); @@ -39,8 +41,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should restore the table columns from the URL state if exists', async () => { - await PageObjects.common.navigateToApp('discover', { - hash: '/p/log-explorer?_a=(columns:!(message,data_stream.namespace))', + await PageObjects.observabilityLogExplorer.navigateTo({ + search: querystring.stringify({ + _a: rison.encode({ + columns: ['message', 'data_stream.namespace'], + }), + }), }); await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selection_state.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts similarity index 60% rename from x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selection_state.ts rename to x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts index 01f5551a94e29..5652843c3fc0c 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selection_state.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts @@ -5,75 +5,82 @@ * 2.0. */ import expect from '@kbn/expect'; +import rison from '@kbn/rison'; +import querystring from 'querystring'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'discoverLogExplorer']); + const PageObjects = getPageObjects(['common', 'observabilityLogExplorer']); describe('DatasetSelection initialization and update', () => { describe('when the "index" query param does not exist', () => { it('should initialize the "All log datasets" selection', async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); const datasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(datasetSelectionTitle).to.be('All log datasets'); }); }); - describe('when the "index" query param exist', () => { + describe('when the "index" query param exists', () => { it('should decode and restore the selection from a valid encoded index', async () => { const azureActivitylogsIndex = 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; - await PageObjects.common.navigateToApp('discover', { - hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`, + await PageObjects.observabilityLogExplorer.navigateTo({ + search: querystring.stringify({ + _a: rison.encode({ index: azureActivitylogsIndex }), + }), }); const datasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(datasetSelectionTitle).to.be('[Azure Logs] activitylogs'); }); - it('should fallback to "All log datasets" selection and notify the user for an invalid encoded index', async () => { + it('should fallback to the "All log datasets" selection and notify the user of an invalid encoded index', async () => { const invalidEncodedIndex = 'invalid-encoded-index'; - await PageObjects.common.navigateToApp('discover', { - hash: `/p/log-explorer?_a=(index:${encodeURIComponent(invalidEncodedIndex)})`, + await PageObjects.observabilityLogExplorer.navigateTo({ + search: querystring.stringify({ + _a: rison.encode({ index: invalidEncodedIndex }), + }), }); const datasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); - await PageObjects.discoverLogExplorer.assertRestoreFailureToastExist(); + await PageObjects.observabilityLogExplorer.assertRestoreFailureToastExist(); expect(datasetSelectionTitle).to.be('All log datasets'); }); }); describe('when navigating back and forth on the page history', () => { it('should decode and restore the selection for the current index', async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); const allDatasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(allDatasetSelectionTitle).to.be('All log datasets'); const azureActivitylogsIndex = 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; - await PageObjects.common.navigateToApp('discover', { - hash: `/p/log-explorer?_a=(index:${encodeURIComponent( - azureActivitylogsIndex - )})&controlPanels=()`, + await PageObjects.observabilityLogExplorer.navigateTo({ + search: querystring.stringify({ + _a: rison.encode({ index: azureActivitylogsIndex }), + controlPanels: rison.encode({}), + }), }); const azureDatasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(azureDatasetSelectionTitle).to.be('[Azure Logs] activitylogs'); // Go back to previous page selection await retry.try(async () => { await browser.goBack(); const backNavigationDatasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(backNavigationDatasetSelectionTitle).to.be('All log datasets'); }); @@ -81,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { await browser.goForward(); const forwardNavigationDatasetSelectionTitle = - await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(forwardNavigationDatasetSelectionTitle).to.be('[Azure Logs] activitylogs'); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selector.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selector.ts similarity index 60% rename from x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selector.ts rename to x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selector.ts index cbb3ea9d95de5..622c282e716eb 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selector.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selector.ts @@ -20,28 +20,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'discoverLogExplorer']); + const PageObjects = getPageObjects(['common', 'observabilityLogExplorer']); describe('Dataset Selector', () => { before(async () => { - await PageObjects.discoverLogExplorer.removeInstalledPackages(); + await PageObjects.observabilityLogExplorer.removeInstalledPackages(); }); describe('without installed integrations or uncategorized data streams', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); describe('when open on the first navigation level', () => { it('should always display the "All log datasets" entry as the first item', async () => { const allLogDatasetButton = - await PageObjects.discoverLogExplorer.getAllLogDatasetsButton(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getAllLogDatasetsButton(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); const allLogDatasetTitle = await allLogDatasetButton.getVisibleText(); const firstEntryTitle = await menuEntries[0].getVisibleText(); @@ -52,8 +52,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should always display the unmanaged datasets entry as the second item', async () => { const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); const unmanagedDatasetTitle = await unamanagedDatasetButton.getVisibleText(); const secondEntryTitle = await menuEntries[1].getVisibleText(); @@ -66,15 +66,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Skip the test in case network condition utils are not available try { await retry.try(async () => { - await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoIntegrationsPromptExists(); }); await PageObjects.common.sleep(5000); await browser.setNetworkConditions('OFFLINE'); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('a'); await retry.try(async () => { - await PageObjects.discoverLogExplorer.assertNoIntegrationsErrorExists(); + await PageObjects.observabilityLogExplorer.assertNoIntegrationsErrorExists(); }); await browser.restoreNetworkConditions(); @@ -83,11 +83,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } }); - it('should display an empty prompt for no integrations', async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + // Skip: failing assertion + // Issue: https://github.com/elastic/kibana/issues/165138 + it.skip('should display an empty prompt for no integrations', async () => { + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations.length).to.be(0); - await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoIntegrationsPromptExists(); }); }); @@ -97,10 +99,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { try { await browser.setNetworkConditions('SLOW_3G'); // Almost stuck network conditions const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await unamanagedDatasetButton.click(); - await PageObjects.discoverLogExplorer.assertLoadingSkeletonExists(); + await PageObjects.observabilityLogExplorer.assertLoadingSkeletonExists(); await browser.restoreNetworkConditions(); } catch (error) { @@ -112,18 +114,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Skip the test in case network condition utils are not available try { const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await unamanagedDatasetButton.click(); await retry.try(async () => { - await PageObjects.discoverLogExplorer.assertNoDataStreamsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoDataStreamsPromptExists(); }); await browser.setNetworkConditions('OFFLINE'); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('a'); await retry.try(async () => { - await PageObjects.discoverLogExplorer.assertNoDataStreamsErrorExists(); + await PageObjects.observabilityLogExplorer.assertNoDataStreamsErrorExists(); }); await browser.restoreNetworkConditions(); @@ -134,15 +136,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should display an empty prompt for no data streams', async () => { const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await unamanagedDatasetButton.click(); const unamanagedDatasetEntries = - await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(unamanagedDatasetEntries.length).to.be(0); - await PageObjects.discoverLogExplorer.assertNoDataStreamsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoDataStreamsPromptExists(); }); }); }); @@ -152,32 +154,33 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await esArchiver.load( - 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' ); - cleanupIntegrationsSetup = await PageObjects.discoverLogExplorer.setupInitialIntegrations(); + cleanupIntegrationsSetup = + await PageObjects.observabilityLogExplorer.setupInitialIntegrations(); }); after(async () => { await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' ); await cleanupIntegrationsSetup(); }); describe('when open on the first navigation level', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); it('should always display the "All log datasets" entry as the first item', async () => { const allLogDatasetButton = - await PageObjects.discoverLogExplorer.getAllLogDatasetsButton(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getAllLogDatasetsButton(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); const allLogDatasetTitle = await allLogDatasetButton.getVisibleText(); const firstEntryTitle = await menuEntries[0].getVisibleText(); @@ -188,8 +191,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should always display the unmanaged datasets entry as the second item', async () => { const unamanagedDatasetButton = - await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); const unmanagedDatasetTitle = await unamanagedDatasetButton.getVisibleText(); const secondEntryTitle = await menuEntries[1].getVisibleText(); @@ -199,7 +202,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should display a list of installed integrations', async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations.length).to.be(3); expect(integrations).to.eql(initialPackagesTexts); @@ -207,82 +210,82 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should sort the integrations list by the clicked sorting option', async () => { // Test ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts); }); // Test descending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('desc'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts.slice().reverse()); }); // Test back ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts); }); }); it('should filter the integrations list by the typed integration name', async () => { - await PageObjects.discoverLogExplorer.typeSearchFieldWith('system'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('system'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.system]); }); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('a'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.apache, initialPackageMap.aws]); }); }); it('should display an empty prompt when the search does not match any result', async () => { - await PageObjects.discoverLogExplorer.typeSearchFieldWith('no result search text'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('no result search text'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations.length).to.be(0); }); - await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + await PageObjects.observabilityLogExplorer.assertNoIntegrationsPromptExists(); }); it('should load more integrations by scrolling to the end of the list', async () => { // Install more integrations and reload the page const cleanupAdditionalSetup = - await PageObjects.discoverLogExplorer.setupAdditionalIntegrations(); + await PageObjects.observabilityLogExplorer.setupAdditionalIntegrations(); await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); // Initially fetched integrations await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(nodes.length).to.be(15); - await nodes.at(-1)?.scrollIntoViewIfNecessary(); + await nodes.at(-1)?.scrollIntoView(); }); // Load more integrations await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(nodes.length).to.be(20); - await nodes.at(-1)?.scrollIntoViewIfNecessary(); + await nodes.at(-1)?.scrollIntoView(); }); // No other integrations to load after scrolling to last integration await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(nodes.length).to.be(20); }); @@ -292,24 +295,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('when clicking on integration and moving into the second navigation level', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); it('should display a list of available datasets', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); expect(await menuEntries[0].getVisibleText()).to.be('access'); @@ -319,39 +322,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should sort the datasets list by the clicked sorting option', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); }); // Test ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); }); // Test descending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('desc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('error'); expect(await menuEntries[1].getVisibleText()).to.be('access'); }); // Test back ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); @@ -360,28 +363,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should filter the datasets list by the typed dataset name', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); }); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('err'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('err'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(menuEntries.length).to.be(1); expect(await menuEntries[0].getVisibleText()).to.be('error'); @@ -390,26 +393,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should update the current selection with the clicked dataset', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('access'); menuEntries[0].click(); }); await retry.try(async () => { - const selectorButton = await PageObjects.discoverLogExplorer.getDatasetSelectorButton(); + const selectorButton = + await PageObjects.observabilityLogExplorer.getDatasetSelectorButton(); expect(await selectorButton.getVisibleText()).to.be('[Apache HTTP Server] access'); }); @@ -418,22 +422,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('when navigating into Uncategorized data streams', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); it('should display a list of available datasets', async () => { - const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const button = await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await button.click(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); @@ -443,20 +447,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should sort the datasets list by the clicked sorting option', async () => { - const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const button = await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await button.click(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); }); // Test ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); @@ -464,9 +468,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // Test descending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('desc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[2]); expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); @@ -474,9 +478,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // Test back ascending order - await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); @@ -485,28 +489,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should filter the datasets list by the typed dataset name', async () => { - const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const button = await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await button.click(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); }); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('retail'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('retail'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(menuEntries.length).to.be(1); expect(await menuEntries[0].getVisibleText()).to.be('logs-retail-*'); @@ -514,25 +518,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should update the current selection with the clicked dataset', async () => { - const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const button = await PageObjects.observabilityLogExplorer.getUnmanagedDatasetsButton(); await button.click(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await menuEntries[0].getVisibleText()).to.be('logs-gaming-*'); menuEntries[0].click(); }); await retry.try(async () => { - const selectorButton = await PageObjects.discoverLogExplorer.getDatasetSelectorButton(); + const selectorButton = + await PageObjects.observabilityLogExplorer.getDatasetSelectorButton(); expect(await selectorButton.getVisibleText()).to.be('logs-gaming-*'); }); @@ -541,37 +546,37 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('when open/close the selector', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); beforeEach(async () => { await browser.refresh(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); }); it('should restore the latest navigation panel', async () => { await retry.try(async () => { - const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes } = await PageObjects.observabilityLogExplorer.getIntegrations(); await nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); }); - await PageObjects.discoverLogExplorer.closeDatasetSelector(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.closeDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); expect(await menuEntries[0].getVisibleText()).to.be('access'); @@ -580,18 +585,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should restore the latest search results', async () => { - await PageObjects.discoverLogExplorer.typeSearchFieldWith('system'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('system'); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.system]); }); - await PageObjects.discoverLogExplorer.closeDatasetSelector(); - await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.closeDatasetSelector(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); await retry.try(async () => { - const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { integrations } = await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.system]); }); }); @@ -599,35 +604,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('when switching between integration panels', () => { before(async () => { - await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await PageObjects.observabilityLogExplorer.navigateTo(); }); it('should remember the latest search and restore its results for each integration', async () => { - await PageObjects.discoverLogExplorer.openDatasetSelector(); - await PageObjects.discoverLogExplorer.clearSearchField(); + await PageObjects.observabilityLogExplorer.openDatasetSelector(); + await PageObjects.observabilityLogExplorer.clearSearchField(); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('apache'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('apache'); await retry.try(async () => { - const { nodes, integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes, integrations } = + await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.apache]); nodes[0].click(); }); await retry.try(async () => { const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); expect(await menuEntries[0].getVisibleText()).to.be('access'); expect(await menuEntries[1].getVisibleText()).to.be('error'); }); - await PageObjects.discoverLogExplorer.typeSearchFieldWith('err'); + await PageObjects.observabilityLogExplorer.typeSearchFieldWith('err'); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); expect(menuEntries.length).to.be(1); expect(await menuEntries[0].getVisibleText()).to.be('error'); @@ -635,23 +641,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Navigate back to integrations const panelTitleNode = - await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + await PageObjects.observabilityLogExplorer.getDatasetSelectorContextMenuPanelTitle(); panelTitleNode.click(); await retry.try(async () => { - const { nodes, integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + const { nodes, integrations } = + await PageObjects.observabilityLogExplorer.getIntegrations(); expect(integrations).to.eql([initialPackageMap.apache]); - const searchValue = await PageObjects.discoverLogExplorer.getSearchFieldValue(); + const searchValue = await PageObjects.observabilityLogExplorer.getSearchFieldValue(); expect(searchValue).to.eql('apache'); nodes[0].click(); }); await retry.try(async () => { - const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + const menuEntries = await PageObjects.observabilityLogExplorer.getCurrentPanelEntries(); - const searchValue = await PageObjects.discoverLogExplorer.getSearchFieldValue(); + const searchValue = await PageObjects.observabilityLogExplorer.getSearchFieldValue(); expect(searchValue).to.eql('err'); expect(menuEntries.length).to.be(1); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/filter_controls.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/filter_controls.ts new file mode 100644 index 0000000000000..b5e25744f2c5b --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/filter_controls.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['observabilityLogExplorer']); + const testSubjects = getService('testSubjects'); + + describe('Filter controls customization', () => { + before('initialize tests', async () => { + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + }); + + after('clean up archives', async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + + it('renders a filter controls section as part of the unified search bar', async () => { + await PageObjects.observabilityLogExplorer.navigateTo(); + await testSubjects.existOrFail('datasetFiltersCustomization', { allowHidden: true }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/index.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts similarity index 69% rename from x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/index.ts rename to x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts index 8e9843fc02815..b0555b4447d27 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts @@ -7,11 +7,12 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function (loadTestFile: FtrProviderContext['loadTestFile']) { - describe('Discover Log-Explorer profile', function () { +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Observability Log Explorer', function () { + loadTestFile(require.resolve('./app')); loadTestFile(require.resolve('./columns_selection')); - loadTestFile(require.resolve('./customization')); loadTestFile(require.resolve('./dataset_selection_state')); loadTestFile(require.resolve('./dataset_selector')); + loadTestFile(require.resolve('./filter_controls')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/search/config.examples.ts b/x-pack/test_serverless/functional/test_suites/search/config.examples.ts new file mode 100644 index 0000000000000..eb7e66d8a3786 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/config.examples.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { REPO_ROOT } from '@kbn/repo-info'; +import { findTestPluginPaths } from '@kbn/test'; +import { resolve } from 'path'; +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'es', + testFiles: [require.resolve('../common/index.examples')], + junit: { + reportName: 'Serverless Search Examples Functional Tests', + }, + kbnServerArgs: findTestPluginPaths([ + resolve(REPO_ROOT, 'examples'), + resolve(REPO_ROOT, 'x-pack/examples'), + ]), +}); diff --git a/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts b/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts new file mode 100644 index 0000000000000..f8d8db6f06ed4 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'timePicker', + 'dashboard', + 'visualize', + 'common', + 'header', + 'lens', + ]); + + const pieChart = getService('pieChart'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const elasticChart = getService('elasticChart'); + const dashboardPanelActions = getService('dashboardPanelActions'); + + describe('Building a new dashboard', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' + ); + await PageObjects.common.navigateToApp('dashboards'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + await elasticChart.setNewChartUiDebugFlag(true); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' + ); + }); + + it('can add a lens panel by value', async () => { + await PageObjects.lens.createAndAddLensFromDashboard({}); + const newPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.eql(1); + }); + + it('can edit a Lens panel by value and save changes', async () => { + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.lens.switchToVisualization('donut'); + await PageObjects.lens.saveAndReturn(); + await PageObjects.dashboard.waitForRenderComplete(); + + const partitionVisExists = await testSubjects.exists('partitionVisChart'); + expect(partitionVisExists).to.be(true); + }); + + it('can add a filter pill by clicking on the Lens chart', async () => { + await pieChart.filterOnPieSlice('97.220.3.248'); + await PageObjects.dashboard.waitForRenderComplete(); + await pieChart.expectPieSliceCount(1); + }); + + it('can access a new Dashboard from the unsaved changes section of the listing page', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.expectUnsavedChangesListingExists('New Dashboard'); + await PageObjects.dashboard.clickUnsavedChangesContinueEditing('New Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + + // Test that the panel loads and the filter is properly applied + await pieChart.expectPieSliceCount(1); + }); + + it('can save the Dashboard successfully', async () => { + await PageObjects.dashboard.expectUnsavedChangesBadge(); + await PageObjects.dashboard.saveDashboard('Super Serverless'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.expectMissingUnsavedChangesBadge(); + }); + + it('loads the saved Dashboard', async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.expectUnsavedChangesListingDoesNotExist('New Dashboard'); + + await PageObjects.dashboard.loadSavedDashboard('Super Serverless'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.expectMissingUnsavedChangesBadge(); + // Test that the panel loads and the filter is properly applied + await pieChart.expectPieSliceCount(1); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/dashboards/exports/serverless_dashboard_8_11.ndjson b/x-pack/test_serverless/functional/test_suites/search/dashboards/exports/serverless_dashboard_8_11.ndjson new file mode 100644 index 0000000000000..87cb044789f7e --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/dashboards/exports/serverless_dashboard_8_11.ndjson @@ -0,0 +1,5 @@ +{"attributes":{"fields":"[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]","timeFieldName":"@timestamp","title":"logstash-*"},"coreMigrationVersion":"8.8.0","created_at":"2023-08-25T14:45:18.313Z","id":"logstash-*","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"7.11.0","updated_at":"2023-08-25T14:45:18.313Z","version":"WzE0MSwxXQ=="} +{"attributes":{"state":{"datasourceStates":{"formBased":{"layers":{"4ba1a1be-6e67-434b-b3a0-f30db8ea5395":{"columnOrder":["7a5d833b-ca6f-4e48-a924-d2a28d365dc3","3cf18f28-3495-4d45-a55f-d97f88022099","3dc0bd55-2087-4e60-aea2-f9910714f7db"],"columns":{"3cf18f28-3495-4d45-a55f-d97f88022099":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"includeEmptyRows":false,"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"3dc0bd55-2087-4e60-aea2-f9910714f7db":{"dataType":"number","isBucketed":false,"label":"Average of bytes","operationType":"average","scale":"ratio","sourceField":"bytes"},"7a5d833b-ca6f-4e48-a924-d2a28d365dc3":{"dataType":"ip","isBucketed":true,"label":"Top values of ip","operationType":"terms","params":{"orderBy":{"columnId":"3dc0bd55-2087-4e60-aea2-f9910714f7db","type":"column"},"orderDirection":"desc","size":3},"scale":"ordinal","sourceField":"ip"}}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"accessors":["3dc0bd55-2087-4e60-aea2-f9910714f7db"],"layerId":"4ba1a1be-6e67-434b-b3a0-f30db8ea5395","seriesType":"bar_stacked","splitAccessor":"7a5d833b-ca6f-4e48-a924-d2a28d365dc3","xAccessor":"3cf18f28-3495-4d45-a55f-d97f88022099"}],"legend":{"isVisible":true,"legendSize":"auto","position":"right"},"preferredSeriesType":"bar_stacked"}},"title":"lnsXYvis","visualizationType":"lnsXY"},"coreMigrationVersion":"8.8.0","created_at":"2023-08-25T14:45:18.313Z","id":"76fc4200-cf44-11e9-b933-fd84270f3ac2","managed":false,"references":[{"id":"logstash-*","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"logstash-*","name":"indexpattern-datasource-layer-4ba1a1be-6e67-434b-b3a0-f30db8ea5395","type":"index-pattern"}],"type":"lens","typeMigrationVersion":"8.9.0","updated_at":"2023-08-25T14:45:18.313Z","version":"WzE0MywxXQ=="} +{"attributes":{"state":{"datasourceStates":{"formBased":{"layers":{"c61a8afb-a185-4fae-a064-fb3846f6c451":{"columnOrder":["2cd09808-3915-49f4-b3b0-82767eba23f7"],"columns":{"2cd09808-3915-49f4-b3b0-82767eba23f7":{"dataType":"number","isBucketed":false,"label":"Maximum of bytes","operationType":"max","scale":"ratio","sourceField":"bytes"}}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"accessor":"2cd09808-3915-49f4-b3b0-82767eba23f7","isHorizontal":false,"layerId":"c61a8afb-a185-4fae-a064-fb3846f6c451","layerType":"data","layers":[{"accessors":["d3e62a7a-c259-4fff-a2fc-eebf20b7008a","26ef70a9-c837-444c-886e-6bd905ee7335"],"layerId":"c61a8afb-a185-4fae-a064-fb3846f6c451","layerType":"data","seriesType":"area","splitAccessor":"54cd64ed-2a44-4591-af84-b2624504569a","xAccessor":"d6e40cea-6299-43b4-9c9d-b4ee305a2ce8"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"area","size":"xl","textAlign":"center","titlePosition":"bottom"}},"title":"Artistpreviouslyknownaslens","visualizationType":"lnsLegacyMetric"},"coreMigrationVersion":"8.8.0","created_at":"2023-08-25T14:45:18.313Z","id":"76fc4200-cf44-11e9-b933-fd84270f3ac1","managed":false,"references":[{"id":"logstash-*","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"logstash-*","name":"indexpattern-datasource-layer-c61a8afb-a185-4fae-a064-fb3846f6c451","type":"index-pattern"}],"type":"lens","typeMigrationVersion":"8.9.0","updated_at":"2023-08-25T14:45:18.313Z","version":"WzE0MiwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"disabled\":false,\"negate\":true,\"alias\":null,\"key\":\"agent.raw\",\"field\":\"agent.raw\",\"params\":{\"query\":\"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\"},\"type\":\"phrase\",\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match_phrase\":{\"agent.raw\":\"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\"}},\"$state\":{\"store\":\"appState\"}}]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.11.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":12,\"h\":13,\"i\":\"5b087cde-634a-4815-9093-71891a900380\"},\"panelIndex\":\"5b087cde-634a-4815-9093-71891a900380\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsPie\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"logstash-*\",\"name\":\"indexpattern-datasource-layer-36023ac0-43e8-41de-aee2-ebabf1b4fb65\"}],\"state\":{\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"36023ac0-43e8-41de-aee2-ebabf1b4fb65\",\"primaryGroups\":[\"fbf03774-001d-4032-a808-004140e94918\"],\"metrics\":[\"65a386b0-e74e-4076-a419-ee1d3ed7ce87\"],\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false,\"layerType\":\"data\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"36023ac0-43e8-41de-aee2-ebabf1b4fb65\":{\"columns\":{\"fbf03774-001d-4032-a808-004140e94918\":{\"label\":\"Top 3 values of ip\",\"dataType\":\"ip\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"ip\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"65a386b0-e74e-4076-a419-ee1d3ed7ce87\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}},\"65a386b0-e74e-4076-a419-ee1d3ed7ce87\":{\"label\":\"Average of bytes\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true}}},\"columnOrder\":[\"fbf03774-001d-4032-a808-004140e94918\",\"65a386b0-e74e-4076-a419-ee1d3ed7ce87\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{},\"hidePanelTitles\":false},\"title\":\"Custom Title\"},{\"version\":\"8.11.0\",\"type\":\"lens\",\"gridData\":{\"x\":12,\"y\":1,\"w\":36,\"h\":13,\"i\":\"ee9dceec-afc3-4258-9998-44a00c2b36fc\"},\"panelIndex\":\"ee9dceec-afc3-4258-9998-44a00c2b36fc\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"description\":\"Wow what a neat description\",\"enhancements\":{}},\"title\":\"Custom Title on a Library Item\",\"panelRefName\":\"panel_ee9dceec-afc3-4258-9998-44a00c2b36fc\"},{\"version\":\"8.11.0\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":14,\"w\":38,\"h\":16,\"i\":\"7557df66-cfde-4401-a926-aff27d774715\"},\"panelIndex\":\"7557df66-cfde-4401-a926-aff27d774715\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"layerListJSON\":\"[{\\\"locale\\\":\\\"autoselect\\\",\\\"sourceDescriptor\\\":{\\\"type\\\":\\\"EMS_TMS\\\",\\\"isAutoSelect\\\":true,\\\"lightModeDefault\\\":\\\"road_map_desaturated\\\"},\\\"id\\\":\\\"710998eb-fda1-462c-80f2-d498df132ebf\\\",\\\"label\\\":null,\\\"minZoom\\\":0,\\\"maxZoom\\\":24,\\\"alpha\\\":1,\\\"visible\\\":true,\\\"style\\\":{\\\"type\\\":\\\"EMS_VECTOR_TILE\\\",\\\"color\\\":\\\"\\\"},\\\"includeInFitToBounds\\\":true,\\\"type\\\":\\\"EMS_VECTOR_TILE\\\"},{\\\"sourceDescriptor\\\":{\\\"geoField\\\":\\\"geo.coordinates\\\",\\\"scalingType\\\":\\\"MVT\\\",\\\"id\\\":\\\"4ed5225d-d42d-40e3-9515-5a918208db27\\\",\\\"type\\\":\\\"ES_SEARCH\\\",\\\"applyGlobalQuery\\\":true,\\\"applyGlobalTime\\\":true,\\\"applyForceRefresh\\\":true,\\\"filterByMapBounds\\\":true,\\\"tooltipProperties\\\":[],\\\"sortField\\\":\\\"\\\",\\\"sortOrder\\\":\\\"desc\\\",\\\"topHitsGroupByTimeseries\\\":false,\\\"topHitsSplitField\\\":\\\"\\\",\\\"topHitsSize\\\":1,\\\"indexPatternRefName\\\":\\\"layer_1_source_index_pattern\\\"},\\\"id\\\":\\\"07facc2c-117d-4335-bd53-90e0ab36aa53\\\",\\\"label\\\":null,\\\"minZoom\\\":0,\\\"maxZoom\\\":24,\\\"alpha\\\":1,\\\"visible\\\":true,\\\"style\\\":{\\\"type\\\":\\\"VECTOR\\\",\\\"properties\\\":{\\\"icon\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"value\\\":\\\"marker\\\"}},\\\"fillColor\\\":{\\\"type\\\":\\\"DYNAMIC\\\",\\\"options\\\":{\\\"color\\\":\\\"Blues\\\",\\\"colorCategory\\\":\\\"palette_0\\\",\\\"field\\\":{\\\"name\\\":\\\"extension.raw\\\",\\\"origin\\\":\\\"source\\\"},\\\"fieldMetaOptions\\\":{\\\"isEnabled\\\":true,\\\"sigma\\\":3},\\\"type\\\":\\\"CATEGORICAL\\\",\\\"otherCategoryColor\\\":\\\"#000000\\\"}},\\\"lineColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#41937c\\\"}},\\\"lineWidth\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":0}},\\\"iconSize\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":6}},\\\"iconOrientation\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"orientation\\\":0}},\\\"labelText\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"value\\\":\\\"\\\"}},\\\"labelColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#000000\\\"}},\\\"labelSize\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":14}},\\\"labelZoomRange\\\":{\\\"options\\\":{\\\"useLayerZoomRange\\\":true,\\\"minZoom\\\":0,\\\"maxZoom\\\":24}},\\\"labelBorderColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#FFFFFF\\\"}},\\\"symbolizeAs\\\":{\\\"options\\\":{\\\"value\\\":\\\"circle\\\"}},\\\"labelBorderSize\\\":{\\\"options\\\":{\\\"size\\\":\\\"SMALL\\\"}},\\\"labelPosition\\\":{\\\"options\\\":{\\\"position\\\":\\\"CENTER\\\"}}},\\\"isTimeAware\\\":true},\\\"includeInFitToBounds\\\":true,\\\"type\\\":\\\"MVT_VECTOR\\\",\\\"joins\\\":[],\\\"disableTooltips\\\":false}]\",\"mapStateJSON\":\"{\\\"adHocDataViews\\\":[],\\\"zoom\\\":3.53,\\\"center\\\":{\\\"lon\\\":-98.19524,\\\"lat\\\":42.06188},\\\"timeFilters\\\":{\\\"from\\\":\\\"2015-09-19T06:31:44.000Z\\\",\\\"to\\\":\\\"2015-09-23T18:31:44.000Z\\\"},\\\"refreshConfig\\\":{\\\"isPaused\\\":true,\\\"interval\\\":60000},\\\"query\\\":{\\\"query\\\":\\\"\\\",\\\"language\\\":\\\"kuery\\\"},\\\"filters\\\":[],\\\"settings\\\":{\\\"autoFitToDataBounds\\\":false,\\\"backgroundColor\\\":\\\"#ffffff\\\",\\\"customIcons\\\":[],\\\"disableInteractive\\\":false,\\\"disableTooltipControl\\\":false,\\\"hideToolbarOverlay\\\":false,\\\"hideLayerControl\\\":false,\\\"hideViewControl\\\":false,\\\"initialLocation\\\":\\\"LAST_SAVED_LOCATION\\\",\\\"fixedLocation\\\":{\\\"lat\\\":0,\\\"lon\\\":0,\\\"zoom\\\":2},\\\"browserLocation\\\":{\\\"zoom\\\":2},\\\"keydownScrollZoom\\\":false,\\\"maxZoom\\\":24,\\\"minZoom\\\":0,\\\"showScaleControl\\\":false,\\\"showSpatialFilters\\\":true,\\\"showTimesliderToggleButton\\\":true,\\\"spatialFiltersAlpa\\\":0.3,\\\"spatialFiltersFillColor\\\":\\\"#DA8B45\\\",\\\"spatialFiltersLineColor\\\":\\\"#DA8B45\\\"}}\",\"uiStateJSON\":\"{\\\"isLayerTOCOpen\\\":true,\\\"openTOCDetails\\\":[\\\"07facc2c-117d-4335-bd53-90e0ab36aa53\\\"]}\"},\"mapCenter\":{\"lat\":42.37743,\"lon\":-101.55858,\"zoom\":3.53},\"mapBuffer\":{\"minLon\":-135,\"minLat\":21.94305,\"maxLon\":-45,\"maxLat\":55.77657},\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"07facc2c-117d-4335-bd53-90e0ab36aa53\"],\"hiddenLayers\":[],\"enhancements\":{}}},{\"version\":\"8.11.0\",\"type\":\"lens\",\"gridData\":{\"x\":38,\"y\":20,\"w\":10,\"h\":9,\"i\":\"d3089be5-dff0-4bbe-9a36-76dd1dec98ef\"},\"panelIndex\":\"d3089be5-dff0-4bbe-9a36-76dd1dec98ef\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"timeRange\":{\"from\":\"2015-09-21T06:31:44.000Z\",\"to\":\"2015-09-23T18:31:44.000Z\"},\"enhancements\":{}},\"panelRefName\":\"panel_d3089be5-dff0-4bbe-9a36-76dd1dec98ef\"},{\"version\":\"8.11.0\",\"type\":\"lens\",\"gridData\":{\"x\":38,\"y\":12,\"w\":10,\"h\":7,\"i\":\"b7b17dfe-87b7-4c8b-81f5-aab625251c3f\"},\"panelIndex\":\"b7b17dfe-87b7-4c8b-81f5-aab625251c3f\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsMetric\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"logstash-*\",\"name\":\"indexpattern-datasource-layer-c0b8fb6e-3ed0-4565-b5f5-635ed8fe4d9f\"},{\"type\":\"index-pattern\",\"id\":\"logstash-*\",\"name\":\"indexpattern-datasource-layer-5dd18300-eb6a-4259-8e68-2e302c2b9bcb\"}],\"state\":{\"visualization\":{\"layerId\":\"c0b8fb6e-3ed0-4565-b5f5-635ed8fe4d9f\",\"layerType\":\"data\",\"metricAccessor\":\"f8cf87ac-8f5b-4eda-b266-0c37d0205858\",\"showBar\":false,\"trendlineLayerId\":\"5dd18300-eb6a-4259-8e68-2e302c2b9bcb\",\"trendlineLayerType\":\"metricTrendline\",\"trendlineTimeAccessor\":\"c993571d-2a82-4162-a367-3542041c811f\",\"trendlineMetricAccessor\":\"5c57e520-1e7b-41f4-a362-a20f8b2762d4\",\"color\":\"#fccada\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"c0b8fb6e-3ed0-4565-b5f5-635ed8fe4d9f\":{\"columns\":{\"f8cf87ac-8f5b-4eda-b266-0c37d0205858\":{\"label\":\"Median RAM\",\"dataType\":\"number\",\"operationType\":\"median\",\"sourceField\":\"machine.ram\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":1}}},\"customLabel\":true}},\"columnOrder\":[\"f8cf87ac-8f5b-4eda-b266-0c37d0205858\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}},\"5dd18300-eb6a-4259-8e68-2e302c2b9bcb\":{\"linkToLayers\":[\"c0b8fb6e-3ed0-4565-b5f5-635ed8fe4d9f\"],\"columns\":{\"c993571d-2a82-4162-a367-3542041c811f\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"5c57e520-1e7b-41f4-a362-a20f8b2762d4\":{\"label\":\"Median RAM\",\"dataType\":\"number\",\"operationType\":\"median\",\"sourceField\":\"machine.ram\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":1}}},\"customLabel\":true}},\"columnOrder\":[\"c993571d-2a82-4162-a367-3542041c811f\",\"5c57e520-1e7b-41f4-a362-a20f8b2762d4\"],\"sampling\":1,\"ignoreGlobalFilters\":false,\"incompleteColumns\":{}}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}}}]","refreshInterval":{"pause":true,"value":60000},"timeFrom":"2015-09-19T06:31:44.000Z","timeRestore":true,"timeTo":"2015-09-23T18:31:44.000Z","title":"Super Saved Serverless","version":1},"coreMigrationVersion":"8.8.0","created_at":"2023-08-25T16:37:35.012Z","id":"4dc11f80-42b5-11ee-89b3-c776e03685a8","managed":false,"references":[{"id":"logstash-*","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"},{"id":"logstash-*","name":"5b087cde-634a-4815-9093-71891a900380:indexpattern-datasource-layer-36023ac0-43e8-41de-aee2-ebabf1b4fb65","type":"index-pattern"},{"id":"76fc4200-cf44-11e9-b933-fd84270f3ac2","name":"ee9dceec-afc3-4258-9998-44a00c2b36fc:panel_ee9dceec-afc3-4258-9998-44a00c2b36fc","type":"lens"},{"id":"logstash-*","name":"7557df66-cfde-4401-a926-aff27d774715:layer_1_source_index_pattern","type":"index-pattern"},{"id":"76fc4200-cf44-11e9-b933-fd84270f3ac1","name":"d3089be5-dff0-4bbe-9a36-76dd1dec98ef:panel_d3089be5-dff0-4bbe-9a36-76dd1dec98ef","type":"lens"},{"id":"logstash-*","name":"b7b17dfe-87b7-4c8b-81f5-aab625251c3f:indexpattern-datasource-layer-c0b8fb6e-3ed0-4565-b5f5-635ed8fe4d9f","type":"index-pattern"},{"id":"logstash-*","name":"b7b17dfe-87b7-4c8b-81f5-aab625251c3f:indexpattern-datasource-layer-5dd18300-eb6a-4259-8e68-2e302c2b9bcb","type":"index-pattern"}],"type":"dashboard","typeMigrationVersion":"8.9.0","updated_at":"2023-08-25T16:37:35.012Z","version":"WzE2NCwxXQ=="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":4,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test_serverless/functional/test_suites/search/dashboards/import_dashboard.ts b/x-pack/test_serverless/functional/test_suites/search/dashboards/import_dashboard.ts new file mode 100644 index 0000000000000..2f01a0a67167f --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/dashboards/import_dashboard.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* This test is importing saved objects from 7.13.0 to 8.0 and the backported version + * will import from 6.8.x to 7.x.x + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects', 'dashboard']); + + describe('Importing an existing dashboard', () => { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.uiSettings.replace({}); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + it('should be able to import dashboard created in 8.11', async () => { + await PageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await PageObjects.savedObjects.waitTableIsLoaded(); + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', 'serverless_dashboard_8_11.ndjson') + ); + + // this will catch cases where there is an error in the migrations. + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + }); + + it('should render all panels on the dashboard', async () => { + await PageObjects.common.navigateToApp('dashboards'); + await PageObjects.dashboard.loadSavedDashboard('Super Saved Serverless'); + + // dashboard should load properly + await PageObjects.dashboard.expectOnDashboard('Super Saved Serverless'); + await PageObjects.dashboard.waitForRenderComplete(); + + // There should be 0 error embeddables on the dashboard + const errorEmbeddables = await testSubjects.findAll('embeddableStackError'); + expect(errorEmbeddables.length).to.be(0); + }); + + it('does not show the unsaved changes badge in edit mode', async () => { + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.expectMissingUnsavedChangesBadge(); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index e4e3021ef8143..e169f69f3f78b 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -13,5 +13,8 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./empty_page')); loadTestFile(require.resolve('./navigation')); loadTestFile(require.resolve('./cases/attachment_framework')); + + loadTestFile(require.resolve('./dashboards/build_dashboard')); + loadTestFile(require.resolve('./dashboards/import_dashboard')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/security/config.examples.ts b/x-pack/test_serverless/functional/test_suites/security/config.examples.ts new file mode 100644 index 0000000000000..7be037baa9c9a --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/config.examples.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { REPO_ROOT } from '@kbn/repo-info'; +import { findTestPluginPaths } from '@kbn/test'; +import { resolve } from 'path'; +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'security', + testFiles: [require.resolve('../common/index.examples')], + junit: { + reportName: 'Serverless Security Examples Functional Tests', + }, + kbnServerArgs: findTestPluginPaths([ + resolve(REPO_ROOT, 'examples'), + resolve(REPO_ROOT, 'x-pack/examples'), + ]), +}); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts index 6b76503531d3c..c213e48348b67 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts @@ -18,7 +18,9 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const cases = getService('cases'); const find = getService('find'); - describe('persistable attachment', () => { + // Failing + // Issue: https://github.com/elastic/kibana/issues/165135 + describe.skip('persistable attachment', () => { describe('lens visualization', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test_serverless/shared/config.base.ts b/x-pack/test_serverless/shared/config.base.ts index 4a61bf164be2c..68324f7245f8f 100644 --- a/x-pack/test_serverless/shared/config.base.ts +++ b/x-pack/test_serverless/shared/config.base.ts @@ -7,15 +7,27 @@ import { resolve } from 'path'; import { format as formatUrl } from 'url'; +import Fs from 'fs'; import { REPO_ROOT } from '@kbn/repo-info'; -import { esTestConfig, kbnTestConfig, kibanaServerTestUser } from '@kbn/test'; +import { + esTestConfig, + kbnTestConfig, + kibanaTestSuperuserServerless, + getDockerFileMountPath, +} from '@kbn/test'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH, kibanaDevServiceAccount } from '@kbn/dev-utils'; import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; +import { services } from './services'; export default async () => { const servers = { - kibana: kbnTestConfig.getUrlParts(), - elasticsearch: esTestConfig.getUrlParts(), + kibana: { + ...kbnTestConfig.getUrlParts(kibanaTestSuperuserServerless), + protocol: 'https', + certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)], + }, + elasticsearch: { ...esTestConfig.getUrlParts(), protocol: 'https' }, }; // "Fake" SAML provider @@ -32,37 +44,39 @@ export default async () => { return { servers, - + browser: { + acceptInsecureCerts: true, + }, esTestCluster: { - license: 'trial', - from: 'snapshot', + from: 'serverless', + files: [idpPath, jwksPath], serverArgs: [ 'xpack.security.authc.realms.file.file1.order=-100', 'xpack.security.authc.realms.jwt.jwt1.order=-98', `xpack.security.authc.realms.jwt.jwt1.token_type=access_token`, 'xpack.security.authc.realms.jwt.jwt1.client_authentication.type=shared_secret', - `xpack.security.authc.realms.jwt.jwt1.client_authentication.shared_secret=my_super_secret`, `xpack.security.authc.realms.jwt.jwt1.allowed_issuer=https://kibana.elastic.co/jwt/`, `xpack.security.authc.realms.jwt.jwt1.allowed_subjects=elastic-agent`, 'xpack.security.authc.realms.jwt.jwt1.allowed_audiences=elasticsearch', `xpack.security.authc.realms.jwt.jwt1.allowed_signature_algorithms=[RS256]`, `xpack.security.authc.realms.jwt.jwt1.claims.principal=sub`, - `xpack.security.authc.realms.jwt.jwt1.pkc_jwkset_path=${jwksPath}`, + `xpack.security.authc.realms.jwt.jwt1.pkc_jwkset_path=${getDockerFileMountPath(jwksPath)}`, - // TODO: We should set this flag to `false` as soon as we fully migrate tests to SAML and file realms. - `xpack.security.authc.realms.native.native1.enabled=true`, + `xpack.security.authc.realms.native.native1.enabled=false`, `xpack.security.authc.realms.native.native1.order=-97`, - 'xpack.security.authc.token.enabled=true', 'xpack.security.authc.realms.saml.cloud-saml-kibana.order=101', - `xpack.security.authc.realms.saml.cloud-saml-kibana.idp.metadata.path=${idpPath}`, + `xpack.security.authc.realms.saml.cloud-saml-kibana.idp.metadata.path=${getDockerFileMountPath( + idpPath + )}`, 'xpack.security.authc.realms.saml.cloud-saml-kibana.idp.entity_id=http://www.elastic.co/saml1', `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.entity_id=http://localhost:${servers.kibana.port}`, `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.logout=http://localhost:${servers.kibana.port}/logout`, `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.acs=http://localhost:${servers.kibana.port}/api/security/saml/callback`, 'xpack.security.authc.realms.saml.cloud-saml-kibana.attributes.principal=urn:oid:0.0.7', ], + ssl: true, // not needed as for serverless ssl is always on but added it anyway }, kbnTestServer: { @@ -72,9 +86,14 @@ export default async () => { }, sourceArgs: ['--no-base-path', '--env.name=development'], serverArgs: [ + '--server.ssl.enabled=true', + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, + `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, `--server.restrictInternalApis=true`, `--server.port=${servers.kibana.port}`, '--status.allowAnonymous=true', + `--migrations.zdt.runOnRoles=${JSON.stringify(['ui'])}`, // We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should // either include `kibanaServerTestUser` credentials, or credentials provided by the test // user, or none at all in case anonymous access is used. @@ -83,8 +102,8 @@ export default async () => { Object.entries(servers.elasticsearch).filter(([key]) => key.toLowerCase() !== 'auth') ) )}`, - `--elasticsearch.username=${kibanaServerTestUser.username}`, - `--elasticsearch.password=${kibanaServerTestUser.password}`, + `--elasticsearch.serviceAccountToken=${kibanaDevServiceAccount.token}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, '--telemetry.sendUsageTo=staging', `--logging.appenders.deprecation=${JSON.stringify({ type: 'console', @@ -121,6 +140,7 @@ export default async () => { services: { ...commonFunctionalServices, + ...services, }, // overriding default timeouts from packages/kbn-test/src/functional_test_runner/lib/config/schema.ts diff --git a/x-pack/test_serverless/shared/lib/index.ts b/x-pack/test_serverless/shared/lib/index.ts index e8a7526591f40..da096c611c8d0 100644 --- a/x-pack/test_serverless/shared/lib/index.ts +++ b/x-pack/test_serverless/shared/lib/index.ts @@ -6,4 +6,6 @@ */ export * from './security'; +export * from './object_remover'; +export * from './space_path_prefix'; export * from './cases'; diff --git a/x-pack/test_serverless/shared/lib/object_remover.ts b/x-pack/test_serverless/shared/lib/object_remover.ts new file mode 100644 index 0000000000000..ad029ca579cbd --- /dev/null +++ b/x-pack/test_serverless/shared/lib/object_remover.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SuperTest, Test } from 'supertest'; + +import { getUrlPathPrefixForSpace } from './space_path_prefix'; + +interface ObjectToRemove { + spaceId: string; + id: string; + type: string; + plugin: string; + isInternal?: boolean; +} + +export class ObjectRemover { + private readonly supertest: SuperTest; + private objectsToRemove: ObjectToRemove[] = []; + + constructor(supertest: SuperTest) { + this.supertest = supertest; + } + + /** + * Add a saved object to the collection. It will be deleted as + * + * DELETE [/s/{spaceId}]/[api|internal]/{plugin}/{type}/{id} + * + * @param spaceId The space ID + * @param id The saved object ID + * @param type The saved object type + * @param plugin The plugin name + * @param isInternal Whether the saved object is internal or not (default false/external) + */ + add( + spaceId: ObjectToRemove['spaceId'], + id: ObjectToRemove['id'], + type: ObjectToRemove['type'], + plugin: ObjectToRemove['plugin'], + isInternal?: ObjectToRemove['isInternal'] + ) { + this.objectsToRemove.push({ spaceId, id, type, plugin, isInternal }); + } + + async removeAll() { + await Promise.all( + this.objectsToRemove.map(({ spaceId, id, type, plugin, isInternal }) => { + const url = `${getUrlPathPrefixForSpace(spaceId)}/${ + isInternal ? 'internal' : 'api' + }/${plugin}/${type}/${id}`; + return deleteObject({ supertest: this.supertest, url, plugin }); + }) + ); + this.objectsToRemove = []; + } +} + +interface DeleteObjectParams { + supertest: SuperTest; + url: string; + plugin: string; +} + +async function deleteObject({ supertest, url, plugin }: DeleteObjectParams) { + const result = await supertest + .delete(url) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo'); + + if (plugin === 'saved_objects' && result.status === 200) return; + if (plugin !== 'saved_objects' && result.status === 204) return; + + // eslint-disable-next-line no-console + console.log( + `ObjectRemover: unexpected status deleting ${url}: ${result.status}`, + result.body.text + ); +} diff --git a/x-pack/test_serverless/shared/lib/space_path_prefix.ts b/x-pack/test_serverless/shared/lib/space_path_prefix.ts new file mode 100644 index 0000000000000..adf7cbdfb0df4 --- /dev/null +++ b/x-pack/test_serverless/shared/lib/space_path_prefix.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function getUrlPathPrefixForSpace(spaceId: string) { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; +} diff --git a/x-pack/test_serverless/shared/services/index.ts b/x-pack/test_serverless/shared/services/index.ts index d6e9fd713d90f..02a03229b8383 100644 --- a/x-pack/test_serverless/shared/services/index.ts +++ b/x-pack/test_serverless/shared/services/index.ts @@ -5,4 +5,8 @@ * 2.0. */ -export const services = {}; +import { SupertestProvider, SupertestWithoutAuthProvider } from './supertest'; +export const services = { + supertest: SupertestProvider, + supertestWithoutAuth: SupertestWithoutAuthProvider, +}; diff --git a/x-pack/test_serverless/shared/services/supertest.ts b/x-pack/test_serverless/shared/services/supertest.ts new file mode 100644 index 0000000000000..3855bbdf7137c --- /dev/null +++ b/x-pack/test_serverless/shared/services/supertest.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 { format as formatUrl } from 'url'; +import supertest from 'supertest'; +import { FtrProviderContext } from '../../functional/ftr_provider_context'; + +export function SupertestProvider({ getService }: FtrProviderContext) { + const config = getService('config'); + const kbnUrl = formatUrl(config.get('servers.kibana')); + const cAuthorities = config.get('servers.kibana').certificateAuthorities; + + return supertest.agent(kbnUrl, { ca: cAuthorities }); +} + +export function SupertestWithoutAuthProvider({ getService }: FtrProviderContext) { + const config = getService('config'); + const kbnUrl = formatUrl({ + ...config.get('servers.kibana'), + auth: false, + }); + const cAuthorities = config.get('servers.kibana').certificateAuthorities; + + return supertest.agent(kbnUrl, { ca: cAuthorities }); +} diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index cf52b712de521..de711146da7ec 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -45,11 +45,14 @@ "@kbn/cases-plugin", "@kbn/test-subj-selector", "@kbn/core-http-common", + "@kbn/std", "@kbn/data-views-plugin", "@kbn/core-saved-objects-server", "@kbn/security-api-integration-helpers", "@kbn/data-view-field-editor-plugin", "@kbn/data-plugin", + "@kbn/dev-utils", "@kbn/bfetch-plugin", + "@kbn/rison", ] } diff --git a/yarn.lock b/yarn.lock index 7264182e33b4a..006b36f36eb02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1320,6 +1320,11 @@ resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.0.0.tgz#4d4ad91527a8313c3db1e2167a8821dfae9d6211" integrity sha512-XqVuJEnE0jpl/RkuSp04FF2UE73gY52Y4nZaIE6j9GAeSH2cHYU5CCd4TaVMDi2M18ZpZv7XhL/k+nneQzyJpQ== +"@cfworker/json-schema@^1.12.7": + version "1.12.7" + resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-1.12.7.tgz#064d082a11881f684300bc7e6d3021e9d98f9a59" + integrity sha512-KEJUW22arGRQVoS6Ti8SvgXnme6NNMMcGBugdir1hf32ofWUXC8guwrFbepO2+YtqxNBUo5oO0pLYM5d4pyjOg== + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -2188,6 +2193,24 @@ resolved "https://registry.yarnpkg.com/@foliojs-fork/restructure/-/restructure-2.0.2.tgz#73759aba2aff1da87b7c4554e6839c70d43c92b4" integrity sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA== +"@frsource/base64@1.0.17": + version "1.0.17" + resolved "https://registry.yarnpkg.com/@frsource/base64/-/base64-1.0.17.tgz#b5a37e2ffab4f7fc9ef2e9f660dd3f38ccb05ff4" + integrity sha512-QyMv52jCRIMUIlDM6ysSVPc6Cp3KCTu6/YeLUyJpTEhleXssYB3CT5PqmQijGGwu4819qvan8Eu4PWJRAW5Akg== + +"@frsource/cypress-plugin-visual-regression-diff@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@frsource/cypress-plugin-visual-regression-diff/-/cypress-plugin-visual-regression-diff-3.3.10.tgz#f8c42d457409c4ac3868814085894db705026e97" + integrity sha512-ZjfOpdmXUgNRfLpsbrYiDujUzNEgLx+3dMtvMJHO3d+Yri0wniMB3mukf5+58QUMXuw8mr1cLoBkeWr1UxCOpA== + dependencies: + "@frsource/base64" "1.0.17" + glob "8.1.0" + meta-png "1.0.6" + move-file "2.1.0" + pixelmatch "5.3.0" + pngjs "7.0.0" + sharp "0.32.1" + "@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -4224,10 +4247,6 @@ version "0.0.0" uid "" -"@kbn/discover-log-explorer-plugin@link:x-pack/plugins/discover_log_explorer": - version "0.0.0" - uid "" - "@kbn/discover-plugin@link:src/plugins/discover": version "0.0.0" uid "" @@ -4796,6 +4815,10 @@ version "0.0.0" uid "" +"@kbn/log-explorer-plugin@link:x-pack/plugins/log_explorer": + version "0.0.0" + uid "" + "@kbn/logging-mocks@link:packages/kbn-logging-mocks": version "0.0.0" uid "" @@ -5004,6 +5027,10 @@ version "0.0.0" uid "" +"@kbn/observability-log-explorer-plugin@link:x-pack/plugins/observability_log_explorer": + version "0.0.0" + uid "" + "@kbn/observability-onboarding-plugin@link:x-pack/plugins/observability_onboarding": version "0.0.0" uid "" @@ -5084,6 +5111,10 @@ version "0.0.0" uid "" +"@kbn/profiling-data-access-plugin@link:x-pack/plugins/profiling_data_access": + version "0.0.0" + uid "" + "@kbn/profiling-plugin@link:x-pack/plugins/profiling": version "0.0.0" uid "" @@ -5308,6 +5339,10 @@ version "0.0.0" uid "" +"@kbn/security-solution-features@link:x-pack/packages/security-solution/features": + version "0.0.0" + uid "" + "@kbn/security-solution-fixtures-plugin@link:x-pack/test/cases_api_integration/common/plugins/security_solution": version "0.0.0" uid "" @@ -5900,6 +5935,22 @@ version "0.0.0" uid "" +"@kbn/unified-doc-viewer-examples@link:examples/unified_doc_viewer": + version "0.0.0" + uid "" + +"@kbn/unified-doc-viewer-plugin@link:src/plugins/unified_doc_viewer": + version "0.0.0" + uid "" + +"@kbn/unified-doc-viewer@link:packages/kbn-unified-doc-viewer": + version "0.0.0" + uid "" + +"@kbn/unified-data-table@link:packages/kbn-unified-data-table": + version "0.0.0" + uid "" + "@kbn/unified-field-list-examples-plugin@link:examples/unified_field_list_examples": version "0.0.0" uid "" @@ -9775,6 +9826,11 @@ dependencies: "@types/jest" "*" +"@types/textarea-caret@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/textarea-caret/-/textarea-caret-3.0.1.tgz#5afd4b1c1b3bacb001d76a1e6ef192c710709a86" + integrity sha512-JjrXYzk4t6dM/5nz1hHkZXmd3xSdJM6mOIDSBUrpg4xThwKNryiu4CqHx81LwUJHxEEoQWHTu4fMV4em+c5bXg== + "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -13583,7 +13639,7 @@ css-what@^6.0.1, css-what@^6.1.0: css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" - integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== css@2.X, css@^2.2.1, css@^2.2.4: version "2.2.4" @@ -17369,6 +17425,17 @@ glob@7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" +glob@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + glob@^10.2.2: version "10.2.7" resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.7.tgz#9dd2828cd5bc7bd861e7738d91e7113dda41d7d8" @@ -21757,6 +21824,11 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +meta-png@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/meta-png/-/meta-png-1.0.6.tgz#34d78a403cc1c809978d3e9f89485a2700daafce" + integrity sha512-eQtEi5E9axqwqA/sDK1dyhX9kYHCUe2m+45aQ3JHrozjGPs+/ab+hdhPp7A3GUNW+ZAbavrsg5xQ4r5jkGDX+A== + methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -22340,6 +22412,13 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" +move-file@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/move-file/-/move-file-2.1.0.tgz#3bec9d34fbe4832df6865f112cda4492b56e8507" + integrity sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA== + dependencies: + path-exists "^4.0.0" + mri@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" @@ -23909,7 +23988,7 @@ piscina@^3.2.0: optionalDependencies: nice-napi "^1.0.2" -pixelmatch@^5.3.0: +pixelmatch@5.3.0, pixelmatch@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.3.0.tgz#5e5321a7abedfb7962d60dbf345deda87cb9560a" integrity sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q== @@ -24005,6 +24084,11 @@ png-js@^1.0.0: resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g== +pngjs@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" + integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== + pngjs@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" @@ -26974,7 +27058,7 @@ shallowequal@^1.1.0: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -sharp@^0.32.0: +sharp@0.32.1, sharp@^0.32.0: version "0.32.1" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.1.tgz#41aa0d0b2048b2e0ee453d9fcb14ec1f408390fe" integrity sha512-kQTFtj7ldpUqSe8kDxoGLZc1rnMFU0AO2pqbX6pLy3b7Oj8ivJIdoKNwxHVQG2HN6XpHPJqCSM2nsma2gOXvOg== @@ -28499,6 +28583,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +textarea-caret@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.1.0.tgz#5d5a35bb035fd06b2ff0e25d5359e97f2655087f" + integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q== + throttle-debounce@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5"