diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index ee6bb5ca8ffc1..4bca3d4b02b04 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -122,7 +122,6 @@ enabled: - test/functional/apps/home/config.ts - test/functional/apps/kibana_overview/config.ts - test/functional/apps/management/config.ts - - test/functional/apps/navigation/config.ts - test/functional/apps/saved_objects_management/config.ts - test/functional/apps/sharing/config.ts - test/functional/apps/status_page/config.ts diff --git a/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml b/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml new file mode 100644 index 0000000000000..0cbf4bad865d0 --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml @@ -0,0 +1,54 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-elasticsearch-serverless-verify-and-promote + description: Verify & promote ElasticSearch Serverless images that pass Kibana's test suite + links: + - url: 'https://buildkite.com/elastic/kibana-elasticsearch-serverless-verify-and-promote' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: 'group:kibana-operations' + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / elasticsearch serverless verify and promote + description: Verify & promote ElasticSearch Serverless images that pass Kibana's test suite + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-operations-alerts' + ES_SERVERLESS_IMAGE: latest + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + allow_rebuilds: false + branch_configuration: main + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/es_serverless/verify_es_serverless_image.yml + skip_intermediate_builds: false + provider_settings: + build_branches: false + build_pull_requests: false + publish_commit_status: false + trigger_mode: none + build_tags: false + prefix_pull_request_fork_branch_names: false + skip_pull_request_builds_for_existing_commits: true + teams: + everyone: + access_level: BUILD_AND_READ + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + schedules: + Daily build: + cronline: 0 9 * * * America/New_York + message: Daily build + env: + PUBLISH_DOCKER_TAG: 'true' + branch: main diff --git a/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml b/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml new file mode 100644 index 0000000000000..6691a460776ac --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml @@ -0,0 +1,153 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-elasticsearch-snapshot-build + description: Build new Elasticsearch snapshots for use by kbn-es / FTR + links: + - url: 'https://buildkite.com/elastic/kibana-elasticsearch-snapshot-build' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: 'group:kibana-operations' + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / elasticsearch snapshot build + description: Build new Elasticsearch snapshots for use by kbn-es / FTR + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-operations-alerts' + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + allow_rebuilds: false + branch_configuration: main 8.13 7.17 + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/es_snapshots/build.yml + skip_intermediate_builds: false + provider_settings: + build_branches: false + build_pull_requests: false + publish_commit_status: false + trigger_mode: none + build_tags: false + prefix_pull_request_fork_branch_names: false + skip_pull_request_builds_for_existing_commits: true + teams: + everyone: + access_level: BUILD_AND_READ + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + schedules: + Daily build (main): + cronline: 0 9 * * * America/New_York + message: Daily build + branch: main + Daily build (8.13): + cronline: 0 9 * * * America/New_York + message: Daily build + branch: '8.13' + Daily build (7.17): + cronline: 0 9 * * * America/New_York + message: Daily build + branch: '7.17' +--- +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-elasticsearch-snapshot-promote + description: Promote Elasticsearch snapshots for use by kbn-es / FTR + links: + - url: 'https://buildkite.com/elastic/kibana-elasticsearch-snapshot-promote' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: 'group:kibana-operations' + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / elasticsearch snapshot promote + description: Promote Elasticsearch snapshots for use by kbn-es / FTR + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-operations-alerts' + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + allow_rebuilds: false + branch_configuration: main 8.13 7.17 + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/es_snapshots/promote.yml + skip_intermediate_builds: false + provider_settings: + build_branches: false + build_pull_requests: false + publish_commit_status: false + trigger_mode: none + build_tags: false + prefix_pull_request_fork_branch_names: false + skip_pull_request_builds_for_existing_commits: true + teams: + everyone: + access_level: BUILD_AND_READ + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ +--- +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-elasticsearch-snapshot-verify + description: Verify Elasticsearch snapshots for use by kbn-es / FTR + links: + - url: 'https://buildkite.com/elastic/kibana-elasticsearch-snapshot-verify' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: 'group:kibana-operations' + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / elasticsearch snapshot verify + description: Verify Elasticsearch snapshots for use by kbn-es / FTR + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-operations-alerts' + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + allow_rebuilds: false + branch_configuration: main 8.13 7.17 + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/es_snapshots/verify.yml + skip_intermediate_builds: false + provider_settings: + build_branches: false + build_pull_requests: false + publish_commit_status: false + trigger_mode: none + build_tags: false + prefix_pull_request_fork_branch_names: false + skip_pull_request_builds_for_existing_commits: true + teams: + everyone: + access_level: BUILD_AND_READ + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ diff --git a/.buildkite/pipeline-resource-definitions/locations.yml b/.buildkite/pipeline-resource-definitions/locations.yml index f0632ef2c0f26..eae8124dfa28b 100644 --- a/.buildkite/pipeline-resource-definitions/locations.yml +++ b/.buildkite/pipeline-resource-definitions/locations.yml @@ -6,16 +6,18 @@ metadata: spec: type: url targets: - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-esql-grammar-sync.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-migration-staging.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-purge-cloud-deployments.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-ess/security-solution-ess.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-defend-workflows.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-detection-engine.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-entity-analytics.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-explore.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-gen-ai.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-investigations.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-rule-management.yml + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-esql-grammar-sync.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-migration-staging.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-purge-cloud-deployments.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-ess/security-solution-ess.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-defend-workflows.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-detection-engine.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-entity-analytics.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-explore.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-gen-ai.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-investigations.yml' + - 'https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-rule-management.yml' diff --git a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml index 5be24f7d151cb..99f5c98a06c35 100644 --- a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml +++ b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml @@ -9,7 +9,10 @@ # BUILDKITE_COMMIT: the commit hash of the kibana branch to test agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 steps: - label: "Annotate runtime parameters" @@ -25,12 +28,18 @@ steps: key: pre-build timeout_in_minutes: 10 agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 - label: "Build Kibana Distribution and Plugins" command: .buildkite/scripts/steps/build_kibana.sh agents: - queue: n2-16-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-16 key: build depends_on: pre-build timeout_in_minutes: 60 @@ -41,8 +50,6 @@ steps: - label: "Pick Test Group Run Order (FTR + Integration)" command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh - agents: - queue: kibana-default depends_on: build timeout_in_minutes: 10 env: @@ -60,7 +67,11 @@ steps: label: 'Serverless Entity Analytics - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 2 @@ -73,7 +84,11 @@ steps: label: 'Serverless Explore - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 4 @@ -86,7 +101,11 @@ steps: label: 'Serverless Investigations - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 8 @@ -99,7 +118,11 @@ steps: label: 'Serverless Rule Management - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 5 @@ -112,7 +135,11 @@ steps: label: 'Serverless Rule Management - Prebuilt Rules - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 2 @@ -125,7 +152,11 @@ steps: label: 'Serverless Detection Engine - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 5 @@ -138,7 +169,11 @@ steps: label: 'Serverless Detection Engine - Exceptions - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 2 @@ -151,7 +186,11 @@ steps: label: 'Serverless AI Assistant - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 1 @@ -164,7 +203,13 @@ steps: label: 'Defend Workflows Cypress Tests on Serverless' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-virt + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 depends_on: build timeout_in_minutes: 60 parallelism: 12 @@ -177,7 +222,11 @@ steps: label: 'Serverless Osquery Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: - queue: n2-4-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-4 + preemptible: true depends_on: build timeout_in_minutes: 60 parallelism: 7 @@ -196,5 +245,3 @@ steps: - label: 'Post-Build' command: .buildkite/scripts/lifecycle/post_build.sh timeout_in_minutes: 10 - agents: - queue: kibana-default diff --git a/.buildkite/pipelines/es_snapshots/build.yml b/.buildkite/pipelines/es_snapshots/build.yml index ba4a15f41ba7f..5fcdae8dfc986 100644 --- a/.buildkite/pipelines/es_snapshots/build.yml +++ b/.buildkite/pipelines/es_snapshots/build.yml @@ -3,4 +3,9 @@ steps: label: Build ES Snapshot timeout_in_minutes: 30 agents: - queue: c2-8 + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + localSsds: 1 + localSsdInterface: nvme + machineType: c2-standard-8 diff --git a/.buildkite/pipelines/es_snapshots/promote.yml b/.buildkite/pipelines/es_snapshots/promote.yml index f2f7b423c94c2..70df536a1fd2b 100644 --- a/.buildkite/pipelines/es_snapshots/promote.yml +++ b/.buildkite/pipelines/es_snapshots/promote.yml @@ -11,4 +11,7 @@ steps: - label: Promote Snapshot command: .buildkite/scripts/steps/es_snapshots/promote.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index d48f3052c133b..5321e12b9442d 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -15,14 +15,21 @@ steps: label: Pre-Build timeout_in_minutes: 10 agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 - wait - command: .buildkite/scripts/steps/build_kibana.sh label: Build Kibana Distribution and Plugins agents: - queue: n2-16-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-16 + preemptible: true key: build if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" timeout_in_minutes: 60 @@ -34,7 +41,10 @@ steps: - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh label: 'Pick Test Group Run Order' agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 timeout_in_minutes: 10 env: JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' @@ -50,7 +60,10 @@ steps: label: Trigger promotion timeout_in_minutes: 10 agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 depends_on: - ftr-configs - jest-integration @@ -62,4 +75,7 @@ steps: label: Post-Build timeout_in_minutes: 10 agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml index 2b74ee24165dc..837234fc51441 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml @@ -21,6 +21,19 @@ steps: EC_REGION: aws-us-east-1 RETRY_TESTS_ON_FAIL: "true" message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-staging.yaml)" + + - label: ":rocket: Fleet synthetic monitor to check the long standing project" + trigger: "serverless-quality-gates" + build: + message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-staging.yaml)" + env: + TARGET_ENV: staging + CHECK_SYNTHETICS: true + CHECK_SYNTHETICS_TAG: "fleet" + CHECK_SYNTHETICS_MINIMUM_RUNS: 3 + MAX_FAILURES: 2 + CHECK_SYNTHETIC_MAX_POLL: 50 + soft_fail: true - wait: ~ diff --git a/.buildkite/pipelines/upload_pipeline.yml b/.buildkite/pipelines/upload_pipeline.yml index a6fa136efb2ed..83daee197f9dd 100644 --- a/.buildkite/pipelines/upload_pipeline.yml +++ b/.buildkite/pipelines/upload_pipeline.yml @@ -2,4 +2,4 @@ steps: - label: Upload tested pipeline - command: buildkite-agent pipeline upload .buildkite/pipelines/pipeline_to_test.yml + command: buildkite-agent pipeline upload ${TESTED_PIPELINE_PATH:-.buildkite/pipelines/pipeline_to_test.yml} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9e6bca0b54433..70640a4502c6f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -478,6 +478,7 @@ src/plugins/image_embeddable @elastic/appex-sharedux packages/kbn-import-locator @elastic/kibana-operations packages/kbn-import-resolver @elastic/kibana-operations x-pack/plugins/index_lifecycle_management @elastic/kibana-management +x-pack/packages/index-management @elastic/kibana-management x-pack/plugins/index_management @elastic/kibana-management test/plugin_functional/plugins/index_patterns @elastic/kibana-data-discovery x-pack/packages/kbn-infra-forge @elastic/obs-ux-management-team @@ -588,9 +589,9 @@ test/common/plugins/newsfeed @elastic/kibana-core src/plugins/no_data_page @elastic/appex-sharedux x-pack/plugins/notifications @elastic/appex-sharedux packages/kbn-object-versioning @elastic/appex-sharedux -x-pack/plugins/observability_solution/observability_ai_assistant_app @elastic/obs-knowledge-team -x-pack/plugins/observability_solution/observability_ai_assistant_management @elastic/obs-knowledge-team -x-pack/plugins/observability_solution/observability_ai_assistant @elastic/obs-knowledge-team +x-pack/plugins/observability_solution/observability_ai_assistant_app @elastic/obs-ai-assistant +x-pack/plugins/observability_solution/observability_ai_assistant_management @elastic/obs-ai-assistant +x-pack/plugins/observability_solution/observability_ai_assistant @elastic/obs-ai-assistant x-pack/packages/observability/alert_details @elastic/obs-ux-management-team x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops diff --git a/package.json b/package.json index dfb2282bf15c1..9546120c02433 100644 --- a/package.json +++ b/package.json @@ -509,6 +509,7 @@ "@kbn/iframe-embedded-plugin": "link:x-pack/test/functional_embedded/plugins/iframe_embedded", "@kbn/image-embeddable-plugin": "link:src/plugins/image_embeddable", "@kbn/index-lifecycle-management-plugin": "link:x-pack/plugins/index_lifecycle_management", + "@kbn/index-management": "link:x-pack/packages/index-management", "@kbn/index-management-plugin": "link:x-pack/plugins/index_management", "@kbn/index-patterns-test-plugin": "link:test/plugin_functional/plugins/index_patterns", "@kbn/infra-forge": "link:x-pack/packages/kbn-infra-forge", diff --git a/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.test.ts b/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.test.ts new file mode 100644 index 0000000000000..dbf4b806be42c --- /dev/null +++ b/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may 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 mockFs from 'mock-fs'; +import { kibanaResponseFactory } from '@kbn/core-http-router-server-internal'; +import { createDynamicAssetHandler } from './dynamic_asset_response'; + +function getHandler(args?: Partial[0]>) { + return createDynamicAssetHandler({ + bundlesPath: '/test', + publicPath: '/public', + fileHashCache: { + get: jest.fn(), + set: jest.fn(), + del: jest.fn(), + }, + isDist: true, + ...args, + }); +} + +afterEach(() => { + mockFs.restore(); +}); +it('returns 403 if the path requested does not match bundle path', async () => { + const handler = getHandler(); + const result = await handler( + {} as any, + { params: { path: '/non-existent/abc.js' }, headers: { 'accept-encoding': '*' } } as any, + kibanaResponseFactory + ); + expect(result.status).toBe(403); +}); + +it('returns 404 if the file does not exist', async () => { + const handler = getHandler(); + mockFs({}); // no files + const filePath = '/test/abc.js'; + const result = await handler( + {} as any, + { params: { path: filePath }, headers: { 'accept-encoding': '*' } } as any, + kibanaResponseFactory + ); + expect(result.status).toBe(404); +}); + +describe('headers', () => { + it('returns the expected headers', async () => { + const handler = getHandler(); + const filePath = '/test/abc.js'; + mockFs({ + [filePath]: Buffer.from('test'), + }); + const result = await handler( + {} as any, + { params: { path: filePath }, headers: { 'accept-encoding': 'br' } } as any, + kibanaResponseFactory + ); + expect(result.options.headers).toEqual({ + 'cache-control': 'public, max-age=31536000, immutable', + 'content-type': 'application/javascript; charset=utf-8', + }); + }); + + it('returns the expected headers when not in dist mode', async () => { + const handler = getHandler({ isDist: false }); + const filePath = '/test/abc.js'; + mockFs({ + [filePath]: Buffer.from('test'), + }); + const result = await handler( + {} as any, + { params: { path: filePath }, headers: { 'accept-encoding': '*' } } as any, + kibanaResponseFactory + ); + expect(result.options.headers).toEqual({ + 'cache-control': 'must-revalidate', + 'content-type': 'application/javascript; charset=utf-8', + etag: expect.stringMatching(/^[a-f0-9]{40}-\/public/i), + }); + }); +}); diff --git a/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.ts b/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.ts index 219beced6ca65..b79e4d27e1b86 100644 --- a/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.ts +++ b/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.ts @@ -75,7 +75,9 @@ export const createDynamicAssetHandler = ({ let headers: Record; if (isDist) { - headers = { 'cache-control': `max-age=${365 * DAY}` }; + headers = { + 'cache-control': `public, max-age=${365 * DAY}, immutable`, + }; } else { const stat = await fstat(fd); const hash = await getFileHash(fileHashCache, path, stat, fd); diff --git a/packages/core/apps/core-apps-server-internal/tsconfig.json b/packages/core/apps/core-apps-server-internal/tsconfig.json index fc8aa9f25349c..35698e91f6ddb 100644 --- a/packages/core/apps/core-apps-server-internal/tsconfig.json +++ b/packages/core/apps/core-apps-server-internal/tsconfig.json @@ -33,6 +33,7 @@ "@kbn/core-ui-settings-server", "@kbn/monaco", "@kbn/core-http-server-internal", + "@kbn/core-http-router-server-internal", ], "exclude": [ "target/**/*", diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx index 664230d749d0b..1d48fdbb24dc0 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx @@ -245,7 +245,6 @@ export class ChromeService { http, chromeBreadcrumbs$: breadcrumbs$, logger: this.logger, - setChromeStyle, }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); const docTitle = this.docTitle.start(); diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts index 52e22f7aef03a..508e72c5b86b4 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts @@ -60,11 +60,9 @@ const logger = loggerMock.create(); const setup = ({ locationPathName = '/', navLinkIds, - setChromeStyle = jest.fn(), }: { locationPathName?: string; navLinkIds?: Readonly; - setChromeStyle?: () => void; } = {}) => { const history = createMemoryHistory({ initialEntries: [locationPathName], @@ -87,7 +85,6 @@ const setup = ({ http: httpServiceMock.createStartContract(), chromeBreadcrumbs$, logger, - setChromeStyle, }); return { projectNavigation, history, chromeBreadcrumbs$, navLinksService, application }; @@ -1018,22 +1015,6 @@ describe('solution navigations', () => { ); }); - it('should set the Chrome style when the active solution navigation changes', async () => { - const setChromeStyle = jest.fn(); - const { projectNavigation } = setup({ setChromeStyle }); - - expect(setChromeStyle).not.toHaveBeenCalled(); - - projectNavigation.updateSolutionNavigations({ 1: solution1, 2: solution2 }); - expect(setChromeStyle).not.toHaveBeenCalled(); - - projectNavigation.changeActiveSolutionNavigation('2'); - expect(setChromeStyle).toHaveBeenCalledWith('project'); // We have an active solution nav, we should switch to project style - - projectNavigation.changeActiveSolutionNavigation(null); - expect(setChromeStyle).toHaveBeenCalledWith('classic'); // No active solution, we should switch back to classic Kibana - }); - it('should change the active solution if no node match the current Location', async () => { const { projectNavigation, navLinksService } = setup({ locationPathName: '/app/app3', // we are on app3 which only exists in solution3 diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts index 63675ea742aaf..59becdebeb406 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts @@ -16,7 +16,6 @@ import type { ChromeProjectNavigationNode, NavigationTreeDefinition, SolutionNavigationDefinitions, - ChromeStyle, CloudLinks, } from '@kbn/core-chrome-browser'; import type { InternalHttpStart } from '@kbn/core-http-browser-internal'; @@ -57,7 +56,6 @@ interface StartDeps { http: InternalHttpStart; chromeBreadcrumbs$: Observable; logger: Logger; - setChromeStyle: (style: ChromeStyle) => void; } export class ProjectNavigationService { @@ -91,23 +89,14 @@ export class ProjectNavigationService { private http?: InternalHttpStart; private navigationChangeSubscription?: Subscription; private unlistenHistory?: () => void; - private setChromeStyle: StartDeps['setChromeStyle'] = () => {}; - - public start({ - application, - navLinksService, - http, - chromeBreadcrumbs$, - logger, - setChromeStyle, - }: StartDeps) { + + public start({ application, navLinksService, http, chromeBreadcrumbs$, logger }: StartDeps) { this.application = application; this.navLinksService = navLinksService; this.http = http; this.logger = logger; this.onHistoryLocationChange(application.history.location); this.unlistenHistory = application.history.listen(this.onHistoryLocationChange.bind(this)); - this.setChromeStyle = setChromeStyle; this.handleActiveNodesChange(); this.handleEmptyActiveNodes(); @@ -418,7 +407,6 @@ export class ProjectNavigationService { // When we **do** have definitions, then passing `null` does mean we should change to "classic". if (Object.keys(definitions).length > 0) { if (id === null) { - this.setChromeStyle('classic'); this.navigationTree$.next(undefined); this.activeSolutionNavDefinitionId$.next(null); } else { @@ -427,8 +415,6 @@ export class ProjectNavigationService { throw new Error(`Solution navigation definition with id "${id}" does not exist.`); } - this.setChromeStyle('project'); - const { sideNavComponent } = definition; if (sideNavComponent) { this.setSideNavComponent(sideNavComponent); diff --git a/packages/core/root/core-root-server-internal/src/server.test.mocks.ts b/packages/core/root/core-root-server-internal/src/server.test.mocks.ts index d1fd4d8ba1266..6e4f18637ab15 100644 --- a/packages/core/root/core-root-server-internal/src/server.test.mocks.ts +++ b/packages/core/root/core-root-server-internal/src/server.test.mocks.ts @@ -68,6 +68,7 @@ jest.doMock('@kbn/core-ui-settings-server-internal', () => ({ })); import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks'; +import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks'; export const mockCustomBrandingService = customBrandingServiceMock.create(); jest.doMock('@kbn/core-custom-branding-server-internal', () => ({ @@ -138,3 +139,8 @@ export const mockSecurityService = securityServiceMock.create(); jest.doMock('@kbn/core-security-server-internal', () => ({ SecurityService: jest.fn(() => mockSecurityService), })); + +export const mockUsageDataService = coreUsageDataServiceMock.create(); +jest.doMock('@kbn/core-usage-data-server-internal', () => ({ + CoreUsageDataService: jest.fn(() => mockUsageDataService), +})); diff --git a/packages/core/root/core-root-server-internal/tsconfig.json b/packages/core/root/core-root-server-internal/tsconfig.json index 8c8340b2aca0c..6eed4c2e59413 100644 --- a/packages/core/root/core-root-server-internal/tsconfig.json +++ b/packages/core/root/core-root-server-internal/tsconfig.json @@ -73,6 +73,7 @@ "@kbn/core-user-settings-server-mocks", "@kbn/core-security-server-mocks", "@kbn/core-security-server-internal", + "@kbn/core-usage-data-server-mocks", ], "exclude": [ "target/**/*", diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts index f03e26d70cb11..11602704cf0f0 100644 --- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts +++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts @@ -86,6 +86,10 @@ describe('CoreUsageDataService', () => { service = new CoreUsageDataService(coreContext); }); + afterEach(() => { + service.stop(); + }); + describe('setup', () => { it('creates internal repository', async () => { const http = httpServiceMock.createInternalSetupContract(); diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts index 0d2ae2ad0f2f7..4b4ab735d5eea 100644 --- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts +++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts @@ -496,7 +496,12 @@ export class CoreUsageDataService const getClient = () => { const debugLogger = (message: string) => this.logger.debug(message); - return new CoreUsageStatsClient(debugLogger, http.basePath, internalRepositoryPromise); + return new CoreUsageStatsClient( + debugLogger, + http.basePath, + internalRepositoryPromise, + this.stop$ + ); }; this.coreUsageStatsClient = getClient(); diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts index 86d1e1bbd8712..6c30d6ce2c8ff 100644 --- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts +++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Subject } from 'rxjs'; import { httpServerMock, httpServiceMock } from '@kbn/core-http-server-mocks'; import { savedObjectsRepositoryMock } from '@kbn/core-saved-objects-api-server-mocks'; import { @@ -38,6 +39,7 @@ import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import { CoreUsageStatsClient } from '.'; describe('CoreUsageStatsClient', () => { + const stop$ = new Subject(); const setup = (namespace?: string) => { const debugLoggerMock = jest.fn(); const basePathMock = httpServiceMock.createBasePath(); @@ -47,7 +49,8 @@ describe('CoreUsageStatsClient', () => { const usageStatsClient = new CoreUsageStatsClient( debugLoggerMock, basePathMock, - Promise.resolve(repositoryMock) + Promise.resolve(repositoryMock), + stop$ ); return { usageStatsClient, debugLoggerMock, basePathMock, repositoryMock }; }; @@ -58,6 +61,115 @@ describe('CoreUsageStatsClient', () => { }; // as long as these header fields are truthy, this will be treated like a first-party request const incrementOptions = { refresh: false }; + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + stop$.next(); + }); + + describe('Request-batching', () => { + it.each([ + { triggerName: 'timer-based', triggerFn: async () => await jest.runOnlyPendingTimersAsync() }, + { + triggerName: 'forced-flush', + triggerFn: (usageStatsClient: CoreUsageStatsClient) => { + // eslint-disable-next-line dot-notation + usageStatsClient['flush$'].next(); + }, + }, + ])('batches multiple increments into one ($triggerName)', async ({ triggerFn }) => { + const { usageStatsClient, repositoryMock } = setup(); + + // First request + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsBulkCreate({ + request, + } as BaseIncrementOptions); + + // Second request + const kibanaRequest = httpServerMock.createKibanaRequest({ + headers: firstPartyRequestHeaders, + }); + await usageStatsClient.incrementSavedObjectsBulkCreate({ + request: kibanaRequest, + } as BaseIncrementOptions); + + // Run trigger + await triggerFn(usageStatsClient); + + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 2 }, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 2 }, + { + fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, + { + fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, + ], + incrementOptions + ); + }); + + it('triggers when the queue is too large', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + // Trigger enough requests to overflow the queue + const request = httpServerMock.createKibanaRequest(); + await Promise.all( + [...new Array(10_001).keys()].map(() => + usageStatsClient.incrementSavedObjectsBulkCreate({ + request, + } as BaseIncrementOptions) + ) + ); + + // It sends all elements in the max batch + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith( + 1, + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 10_000 }, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 10_000 }, + { + fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 10_000, + }, + ], + incrementOptions + ); + + // After timer, it sends the remainder event + await jest.runOnlyPendingTimersAsync(); + + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(2); + expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith( + 2, + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, + ], + incrementOptions + ); + }); + }); + describe('#getUsageStats', () => { it('returns empty object when encountering a repository error', async () => { const { usageStatsClient, repositoryMock } = setup(); @@ -93,6 +205,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -103,14 +216,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkCreate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_CREATE_STATS_PREFIX}.total`, - `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, - `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -123,14 +240,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkCreate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_CREATE_STATS_PREFIX}.total`, - `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, - `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -143,14 +264,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkCreate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_CREATE_STATS_PREFIX}.total`, - `${BULK_CREATE_STATS_PREFIX}.namespace.custom.total`, - `${BULK_CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { + fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -168,6 +293,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -178,14 +304,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkGet({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_GET_STATS_PREFIX}.total`, - `${BULK_GET_STATS_PREFIX}.namespace.default.total`, - `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${BULK_GET_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_GET_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -198,14 +328,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkGet({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_GET_STATS_PREFIX}.total`, - `${BULK_GET_STATS_PREFIX}.namespace.default.total`, - `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${BULK_GET_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_GET_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -218,14 +352,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkGet({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_GET_STATS_PREFIX}.total`, - `${BULK_GET_STATS_PREFIX}.namespace.custom.total`, - `${BULK_GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${BULK_GET_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_GET_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { + fieldName: `${BULK_GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -243,6 +381,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -253,14 +392,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkResolve({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_RESOLVE_STATS_PREFIX}.total`, - `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.total`, - `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -273,14 +416,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkResolve({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_RESOLVE_STATS_PREFIX}.total`, - `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.total`, - `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -293,14 +440,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkResolve({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_RESOLVE_STATS_PREFIX}.total`, - `${BULK_RESOLVE_STATS_PREFIX}.namespace.custom.total`, - `${BULK_RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { + fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -318,6 +469,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -328,14 +480,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkUpdate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_UPDATE_STATS_PREFIX}.total`, - `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`, - `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${BULK_UPDATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -348,14 +504,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkUpdate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_UPDATE_STATS_PREFIX}.total`, - `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`, - `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${BULK_UPDATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -368,14 +528,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkUpdate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_UPDATE_STATS_PREFIX}.total`, - `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.total`, - `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${BULK_UPDATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { + fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -393,6 +557,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -403,14 +568,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsCreate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${CREATE_STATS_PREFIX}.total`, - `${CREATE_STATS_PREFIX}.namespace.default.total`, - `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${CREATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -423,14 +592,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsCreate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${CREATE_STATS_PREFIX}.total`, - `${CREATE_STATS_PREFIX}.namespace.default.total`, - `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${CREATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -443,14 +616,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsCreate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${CREATE_STATS_PREFIX}.total`, - `${CREATE_STATS_PREFIX}.namespace.custom.total`, - `${CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${CREATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${CREATE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { fieldName: `${CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 }, ], incrementOptions ); @@ -468,6 +642,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -478,14 +653,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkDelete({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_DELETE_STATS_PREFIX}.total`, - `${BULK_DELETE_STATS_PREFIX}.namespace.default.total`, - `${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${BULK_DELETE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -498,14 +677,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkDelete({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_DELETE_STATS_PREFIX}.total`, - `${BULK_DELETE_STATS_PREFIX}.namespace.default.total`, - `${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${BULK_DELETE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -518,14 +701,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsBulkDelete({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${BULK_DELETE_STATS_PREFIX}.total`, - `${BULK_DELETE_STATS_PREFIX}.namespace.custom.total`, - `${BULK_DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${BULK_DELETE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { + fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -543,6 +730,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -553,14 +741,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsDelete({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${DELETE_STATS_PREFIX}.total`, - `${DELETE_STATS_PREFIX}.namespace.default.total`, - `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${DELETE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${DELETE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -573,14 +765,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsDelete({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${DELETE_STATS_PREFIX}.total`, - `${DELETE_STATS_PREFIX}.namespace.default.total`, - `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${DELETE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${DELETE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -593,14 +789,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsDelete({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${DELETE_STATS_PREFIX}.total`, - `${DELETE_STATS_PREFIX}.namespace.custom.total`, - `${DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${DELETE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${DELETE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { fieldName: `${DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 }, ], incrementOptions ); @@ -618,6 +815,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -628,14 +826,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsFind({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${FIND_STATS_PREFIX}.total`, - `${FIND_STATS_PREFIX}.namespace.default.total`, - `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${FIND_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${FIND_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { fieldName: `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.no`, incrementBy: 1 }, ], incrementOptions ); @@ -648,14 +847,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsFind({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${FIND_STATS_PREFIX}.total`, - `${FIND_STATS_PREFIX}.namespace.default.total`, - `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${FIND_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${FIND_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { fieldName: `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, incrementBy: 1 }, ], incrementOptions ); @@ -668,14 +868,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsFind({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${FIND_STATS_PREFIX}.total`, - `${FIND_STATS_PREFIX}.namespace.custom.total`, - `${FIND_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${FIND_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${FIND_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { fieldName: `${FIND_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 }, ], incrementOptions ); @@ -693,6 +894,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -703,14 +905,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsGet({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${GET_STATS_PREFIX}.total`, - `${GET_STATS_PREFIX}.namespace.default.total`, - `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${GET_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${GET_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { fieldName: `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`, incrementBy: 1 }, ], incrementOptions ); @@ -723,14 +926,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsGet({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${GET_STATS_PREFIX}.total`, - `${GET_STATS_PREFIX}.namespace.default.total`, - `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${GET_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${GET_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { fieldName: `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, incrementBy: 1 }, ], incrementOptions ); @@ -743,14 +947,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsGet({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${GET_STATS_PREFIX}.total`, - `${GET_STATS_PREFIX}.namespace.custom.total`, - `${GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${GET_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${GET_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { fieldName: `${GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 }, ], incrementOptions ); @@ -768,6 +973,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -778,14 +984,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsResolve({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${RESOLVE_STATS_PREFIX}.total`, - `${RESOLVE_STATS_PREFIX}.namespace.default.total`, - `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${RESOLVE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -798,14 +1008,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsResolve({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${RESOLVE_STATS_PREFIX}.total`, - `${RESOLVE_STATS_PREFIX}.namespace.default.total`, - `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${RESOLVE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -818,14 +1032,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsResolve({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${RESOLVE_STATS_PREFIX}.total`, - `${RESOLVE_STATS_PREFIX}.namespace.custom.total`, - `${RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${RESOLVE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -843,6 +1061,7 @@ describe('CoreUsageStatsClient', () => { request, } as BaseIncrementOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -853,14 +1072,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsUpdate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${UPDATE_STATS_PREFIX}.total`, - `${UPDATE_STATS_PREFIX}.namespace.default.total`, - `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + { fieldName: `${UPDATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${UPDATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -873,14 +1096,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsUpdate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${UPDATE_STATS_PREFIX}.total`, - `${UPDATE_STATS_PREFIX}.namespace.default.total`, - `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${UPDATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${UPDATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -893,14 +1120,15 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsUpdate({ request, } as BaseIncrementOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${UPDATE_STATS_PREFIX}.total`, - `${UPDATE_STATS_PREFIX}.namespace.custom.total`, - `${UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${UPDATE_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${UPDATE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { fieldName: `${UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 }, ], incrementOptions ); @@ -918,6 +1146,7 @@ describe('CoreUsageStatsClient', () => { request, } as IncrementSavedObjectsImportOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); }); @@ -928,17 +1157,21 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsImport({ request, } as IncrementSavedObjectsImportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.namespace.default.total`, - `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, - `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, - `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, - `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, + { fieldName: `${IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, + { fieldName: `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, incrementBy: 1 }, ], incrementOptions ); @@ -954,22 +1187,27 @@ describe('CoreUsageStatsClient', () => { overwrite: true, compatibilityMode: true, } as IncrementSavedObjectsImportOptions); + await jest.runOnlyPendingTimersAsync(); await usageStatsClient.incrementSavedObjectsImport({ request, createNewCopies: false, overwrite: true, compatibilityMode: true, } as IncrementSavedObjectsImportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(2); expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith( 1, CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.namespace.default.total`, - `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, - `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, + { fieldName: `${IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, + { fieldName: `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, incrementBy: 1 }, // excludes 'overwriteEnabled.yes', 'overwriteEnabled.no', 'compatibilityModeEnabled.yes`, and // `compatibilityModeEnabled.no` when createNewCopies is true ], @@ -980,12 +1218,15 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.namespace.default.total`, - `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, - `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, - `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, - `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.yes`, + { fieldName: `${IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, + { fieldName: `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.yes`, incrementBy: 1 }, ], incrementOptions ); @@ -998,17 +1239,18 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsImport({ request, } as IncrementSavedObjectsImportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.namespace.custom.total`, - `${IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, - `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, - `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, - `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, + { fieldName: `${IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, incrementBy: 1 }, + { fieldName: `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, incrementBy: 1 }, ], incrementOptions ); @@ -1026,6 +1268,7 @@ describe('CoreUsageStatsClient', () => { request, } as IncrementSavedObjectsResolveImportErrorsOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -1036,16 +1279,23 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsResolveImportErrors({ request, } as IncrementSavedObjectsResolveImportErrorsOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, - `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, - `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -1060,21 +1310,29 @@ describe('CoreUsageStatsClient', () => { createNewCopies: true, compatibilityMode: true, } as IncrementSavedObjectsResolveImportErrorsOptions); + await jest.runOnlyPendingTimersAsync(); await usageStatsClient.incrementSavedObjectsResolveImportErrors({ request, createNewCopies: false, compatibilityMode: true, } as IncrementSavedObjectsResolveImportErrorsOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(2); expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith( 1, CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, - `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, + { + fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, + incrementBy: 1, + }, // excludes 'compatibilityModeEnabled.yes` and `compatibilityModeEnabled.no` when createNewCopies is true ], incrementOptions @@ -1084,11 +1342,17 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, - `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, - `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.yes`, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -1101,16 +1365,23 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsResolveImportErrors({ request, } as IncrementSavedObjectsResolveImportErrorsOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, - `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, - `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, + { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 }, + { + fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -1128,6 +1399,7 @@ describe('CoreUsageStatsClient', () => { request, } as IncrementSavedObjectsExportOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -1140,15 +1412,19 @@ describe('CoreUsageStatsClient', () => { types: undefined, supportedTypes: ['foo', 'bar'], } as IncrementSavedObjectsExportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.namespace.default.total`, - `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, - `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, + { fieldName: `${EXPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${EXPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + incrementBy: 1, + }, + { fieldName: `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, incrementBy: 1 }, ], incrementOptions ); @@ -1163,15 +1439,19 @@ describe('CoreUsageStatsClient', () => { types: ['foo', 'bar'], supportedTypes: ['foo', 'bar'], } as IncrementSavedObjectsExportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.namespace.default.total`, - `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, - `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, + { fieldName: `${EXPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${EXPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 }, + { + fieldName: `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, + { fieldName: `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, incrementBy: 1 }, ], incrementOptions ); @@ -1184,15 +1464,16 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementSavedObjectsExport({ request, } as IncrementSavedObjectsExportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.namespace.custom.total`, - `${EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, - `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, + { fieldName: `${EXPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { fieldName: `${EXPORT_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 }, + { fieldName: `${EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 }, + { fieldName: `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, incrementBy: 1 }, ], incrementOptions ); @@ -1210,6 +1491,7 @@ describe('CoreUsageStatsClient', () => { request, } as IncrementSavedObjectsExportOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -1220,14 +1502,21 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementLegacyDashboardsImport({ request, } as IncrementSavedObjectsExportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`, - `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.total`, - `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { + fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.total`, + incrementBy: 1, + }, + { + fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -1240,14 +1529,21 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementLegacyDashboardsImport({ request, } as IncrementSavedObjectsExportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`, - `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.total`, - `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { + fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.total`, + incrementBy: 1, + }, + { + fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); @@ -1265,6 +1561,7 @@ describe('CoreUsageStatsClient', () => { request, } as IncrementSavedObjectsExportOptions) ).resolves.toBeUndefined(); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -1275,14 +1572,21 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementLegacyDashboardsExport({ request, } as IncrementSavedObjectsExportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`, - `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.total`, - `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + { fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { + fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.total`, + incrementBy: 1, + }, + { + fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + incrementBy: 1, + }, ], incrementOptions ); @@ -1295,14 +1599,21 @@ describe('CoreUsageStatsClient', () => { await usageStatsClient.incrementLegacyDashboardsExport({ request, } as IncrementSavedObjectsExportOptions); + await jest.runOnlyPendingTimersAsync(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ - `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`, - `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.total`, - `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + { fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`, incrementBy: 1 }, + { + fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.total`, + incrementBy: 1, + }, + { + fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + incrementBy: 1, + }, ], incrementOptions ); diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts index f66e51ed2bf77..19c1bc1facafb 100644 --- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts +++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts @@ -7,7 +7,10 @@ */ import type { KibanaRequest, IBasePath } from '@kbn/core-http-server'; -import type { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; +import type { + ISavedObjectsRepository, + SavedObjectsIncrementCounterField, +} from '@kbn/core-saved-objects-api-server'; import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import type { CoreUsageStats } from '@kbn/core-usage-data-server'; import { @@ -20,6 +23,17 @@ import { CORE_USAGE_STATS_ID, REPOSITORY_RESOLVE_OUTCOME_STATS, } from '@kbn/core-usage-data-base-server-internal'; +import { + bufferWhen, + exhaustMap, + filter, + interval, + map, + merge, + skip, + Subject, + takeUntil, +} from 'rxjs'; export const BULK_CREATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkCreate'; export const BULK_GET_STATS_PREFIX = 'apiCalls.savedObjectsBulkGet'; @@ -74,19 +88,74 @@ const ALL_COUNTER_FIELDS = [ ]; const SPACE_CONTEXT_REGEX = /^\/s\/([a-z0-9_\-]+)/; +// Buffering up to 10k events because: +// - ALL_COUNTER_FIELDS has 125 fields, so that's the max request we can expect after grouping the keys. +// - A typical counter reports 3 fields, so taking 10k events, means around 30k fields (to be later grouped into max 125 fields). +// - Taking into account the longest possible string, this queue can use 15MB max. +const MAX_BUFFER_SIZE = 10_000; +const DEFAULT_BUFFER_TIME_MS = 10_000; + /** @internal */ export class CoreUsageStatsClient implements ICoreUsageStatsClient { + private readonly fieldsToIncrement$ = new Subject(); + private readonly flush$ = new Subject(); + constructor( private readonly debugLogger: (message: string) => void, private readonly basePath: IBasePath, - private readonly repositoryPromise: Promise - ) {} + private readonly repositoryPromise: Promise, + stop$: Subject, + bufferTimeMs: number = DEFAULT_BUFFER_TIME_MS + ) { + this.fieldsToIncrement$ + .pipe( + takeUntil(stop$), + // Buffer until either the timer, a forced flush occur, or there are too many queued fields + bufferWhen(() => + merge( + interval(bufferTimeMs), + this.flush$, + this.fieldsToIncrement$.pipe(skip(MAX_BUFFER_SIZE)) + ) + ), + map((listOfFields) => { + const fieldsMap = listOfFields.flat().reduce((acc, fieldName) => { + const incrementCounterField: Required = acc.get( + fieldName + ) ?? { + fieldName, + incrementBy: 0, + }; + incrementCounterField.incrementBy++; + return acc.set(fieldName, incrementCounterField); + }, new Map>()); + return [...fieldsMap.values()]; + }), + filter((fields) => fields.length > 0), + exhaustMap(async (fields) => { + const options = { refresh: false }; + try { + const repository = await this.repositoryPromise; + await repository.incrementCounter( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + fields, + options + ); + } catch (err) { + // do nothing + } + }) + ) + .subscribe(); + } public async getUsageStats() { this.debugLogger('getUsageStats() called'); let coreUsageStats: CoreUsageStats = {}; try { const repository = await this.repositoryPromise; + this.flush$.next(); const result = await repository.incrementCounter( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, @@ -185,19 +254,8 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient { prefix: string, { request }: BaseIncrementOptions ) { - const options = { refresh: false }; - try { - const repository = await this.repositoryPromise; - const fields = this.getFieldsToIncrement(counterFieldNames, prefix, request); - await repository.incrementCounter( - CORE_USAGE_STATS_TYPE, - CORE_USAGE_STATS_ID, - fields, - options - ); - } catch (err) { - // do nothing - } + const fields = this.getFieldsToIncrement(counterFieldNames, prefix, request); + this.fieldsToIncrement$.next(fields); } private getIsDefaultNamespace(request: KibanaRequest) { diff --git a/packages/kbn-discover-utils/index.ts b/packages/kbn-discover-utils/index.ts index 5eb2650482611..a409962230ce6 100644 --- a/packages/kbn-discover-utils/index.ts +++ b/packages/kbn-discover-utils/index.ts @@ -37,5 +37,6 @@ export { getIgnoredReason, getShouldShowFieldHandler, isNestedFieldParent, + isLegacyTableEnabled, usePager, } from './src'; diff --git a/packages/kbn-discover-utils/src/utils/index.ts b/packages/kbn-discover-utils/src/utils/index.ts index 4828fcf82a447..8415fc7df0710 100644 --- a/packages/kbn-discover-utils/src/utils/index.ts +++ b/packages/kbn-discover-utils/src/utils/index.ts @@ -13,3 +13,4 @@ export * from './get_doc_id'; export * from './get_ignored_reason'; export * from './get_should_show_field_handler'; export * from './nested_fields'; +export { isLegacyTableEnabled } from './is_legacy_table_enabled'; diff --git a/packages/kbn-discover-utils/src/utils/is_legacy_table_enabled.ts b/packages/kbn-discover-utils/src/utils/is_legacy_table_enabled.ts new file mode 100644 index 0000000000000..7e575cf80dbfb --- /dev/null +++ b/packages/kbn-discover-utils/src/utils/is_legacy_table_enabled.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { DOC_TABLE_LEGACY } from '../constants'; + +export function isLegacyTableEnabled({ + uiSettings, + isTextBasedQueryMode, +}: { + uiSettings: IUiSettingsClient; + isTextBasedQueryMode: boolean; +}): boolean { + if (isTextBasedQueryMode) { + return false; // only show the new data grid + } + + return uiSettings.get(DOC_TABLE_LEGACY); +} diff --git a/packages/kbn-discover-utils/tsconfig.json b/packages/kbn-discover-utils/tsconfig.json index 0051ad5b2a00a..64453f8245afe 100644 --- a/packages/kbn-discover-utils/tsconfig.json +++ b/packages/kbn-discover-utils/tsconfig.json @@ -21,6 +21,7 @@ "@kbn/es-query", "@kbn/field-formats-plugin", "@kbn/field-types", - "@kbn/i18n" + "@kbn/i18n", + "@kbn/core-ui-settings-browser" ] } diff --git a/packages/kbn-esql-ast/src/ast_helpers.ts b/packages/kbn-esql-ast/src/ast_helpers.ts index 0f85e30b96c45..0b3c3eff25ada 100644 --- a/packages/kbn-esql-ast/src/ast_helpers.ts +++ b/packages/kbn-esql-ast/src/ast_helpers.ts @@ -113,14 +113,25 @@ export function createLiteral( return; } const text = node.getText(); - return { + + const partialLiteral: Omit = { type: 'literal', - literalType: type, text, name: text, - value: type === 'number' ? Number(text) : text, location: getPosition(node.symbol), - incomplete: isMissingText(node.getText()), + incomplete: isMissingText(text), + }; + if (type === 'number') { + return { + ...partialLiteral, + literalType: type, + value: Number(text), + }; + } + return { + ...partialLiteral, + literalType: type, + value: text, }; } diff --git a/packages/kbn-esql-ast/src/types.ts b/packages/kbn-esql-ast/src/types.ts index 4bce46d776671..f167276ae84a5 100644 --- a/packages/kbn-esql-ast/src/types.ts +++ b/packages/kbn-esql-ast/src/types.ts @@ -72,10 +72,38 @@ export interface ESQLList extends ESQLAstBaseItem { values: ESQLLiteral[]; } -export interface ESQLLiteral extends ESQLAstBaseItem { +export type ESQLLiteral = + | ESQLNumberLiteral + | ESQLBooleanLiteral + | ESQLNullLiteral + | ESQLStringLiteral; + +// @internal +export interface ESQLNumberLiteral extends ESQLAstBaseItem { type: 'literal'; - literalType: 'string' | 'number' | 'boolean' | 'null'; - value: string | number; + literalType: 'number'; + value: number; +} + +// @internal +export interface ESQLBooleanLiteral extends ESQLAstBaseItem { + type: 'literal'; + literalType: 'boolean'; + value: string; +} + +// @internal +export interface ESQLNullLiteral extends ESQLAstBaseItem { + type: 'literal'; + literalType: 'null'; + value: string; +} + +// @internal +export interface ESQLStringLiteral extends ESQLAstBaseItem { + type: 'literal'; + literalType: 'string'; + value: string; } export interface ESQLMessage { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 6afe146560057..c51173cae68ce 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -245,7 +245,7 @@ describe('autocomplete', () => { // simulate the editor behaviour for sorting suggestions .sort((a, b) => (a.sortText || '').localeCompare(b.sortText || '')); for (const [index, receivedSuggestion] of suggestionInertTextSorted.entries()) { - if (typeof expected[index] === 'string') { + if (typeof expected[index] !== 'object') { expect(receivedSuggestion.text).toEqual(expected[index]); } else { // check all properties that are defined in the expected suggestion @@ -1054,38 +1054,47 @@ describe('autocomplete', () => { if (fn.name !== 'auto_bucket') { for (const signature of fn.signatures) { signature.params.forEach((param, i) => { - if (i < signature.params.length - 1) { + if (i < signature.params.length) { const canHaveMoreArgs = + i + 1 < (signature.minParams ?? 0) || signature.params.filter(({ optional }, j) => !optional && j > i).length > i; testSuggestions( `from a | eval ${fn.name}(${Array(i).fill('field').join(', ')}${i ? ',' : ''} )`, - [ - ...getFieldNamesByType(param.type).map((f) => (canHaveMoreArgs ? `${f},` : f)), - ...getFunctionSignaturesByReturnType( - 'eval', - param.type, - { evalMath: true }, - undefined, - [fn.name] - ).map((l) => (canHaveMoreArgs ? `${l},` : l)), - ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)), - ] + param.literalOptions?.length + ? param.literalOptions.map((option) => `"${option}"${canHaveMoreArgs ? ',' : ''}`) + : [ + ...getFieldNamesByType(param.type).map((f) => + canHaveMoreArgs ? `${f},` : f + ), + ...getFunctionSignaturesByReturnType( + 'eval', + param.type, + { evalMath: true }, + undefined, + [fn.name] + ).map((l) => (canHaveMoreArgs ? `${l},` : l)), + ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)), + ] ); testSuggestions( `from a | eval var0 = ${fn.name}(${Array(i).fill('field').join(', ')}${ i ? ',' : '' } )`, - [ - ...getFieldNamesByType(param.type).map((f) => (canHaveMoreArgs ? `${f},` : f)), - ...getFunctionSignaturesByReturnType( - 'eval', - param.type, - { evalMath: true }, - undefined, - [fn.name] - ).map((l) => (canHaveMoreArgs ? `${l},` : l)), - ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)), - ] + param.literalOptions?.length + ? param.literalOptions.map((option) => `"${option}"${canHaveMoreArgs ? ',' : ''}`) + : [ + ...getFieldNamesByType(param.type).map((f) => + canHaveMoreArgs ? `${f},` : f + ), + ...getFunctionSignaturesByReturnType( + 'eval', + param.type, + { evalMath: true }, + undefined, + [fn.name] + ).map((l) => (canHaveMoreArgs ? `${l},` : l)), + ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)), + ] ); } }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 84c5f59f49c31..ce9b2d2e0acf2 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -68,6 +68,7 @@ import { buildVariablesDefinitions, buildOptionDefinition, buildSettingDefinitions, + buildValueDefinitions, } from './factories'; import { EDITOR_MARKER, SINGLE_BACKTICK } from '../shared/constants'; import { getAstContext, removeMarkerArgFromArgsList } from '../shared/context'; @@ -1082,6 +1083,15 @@ async function getFunctionArgsSuggestions( return []; }); + const literalOptions = fnDefinition.signatures.reduce((acc, signature) => { + const literalOptionsForThisParameter = signature.params[argIndex]?.literalOptions; + return literalOptionsForThisParameter ? acc.concat(literalOptionsForThisParameter) : acc; + }, [] as string[]); + + if (literalOptions.length) { + return buildValueDefinitions(literalOptions); + } + const arg = node.args[argIndex]; // the first signature is used as reference diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index 3e243618ed7fa..2818634c58188 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -165,6 +165,16 @@ export const buildConstantsDefinitions = ( sortText: 'A', })); +export const buildValueDefinitions = (values: string[]): SuggestionRawDefinition[] => + values.map((value) => ({ + label: `"${value}"`, + text: `"${value}"`, + detail: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.valueDefinition', { + defaultMessage: 'Literal value', + }), + kind: 'Value', + })); + export const buildNewVarDefinition = (label: string): SuggestionRawDefinition => { return { label, diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/aggs.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/aggs.ts index 94ab3106036fd..169ae23052ebc 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/aggs.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/aggs.ts @@ -175,4 +175,22 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [ }, ], }, + { + name: 'values', + type: 'agg', + description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.values', { + defaultMessage: 'Returns all values in a group as an array.', + }), + supportedCommands: ['stats'], + signatures: [ + { + params: [{ name: 'expression', type: 'any', noNestingFunctions: true }], + returnType: 'any', + examples: [ + 'from index | stats all_agents=values(agents.keyword)', + 'from index | stats all_sorted_agents=mv_sort(values(agents.keyword))', + ], + }, + ], + }, ]); diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts index 99f373879dbb6..f5badd2693234 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts @@ -97,7 +97,6 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [ ], validate: validateLogFunctions, }, - { name: 'log', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.logDoc', { @@ -481,14 +480,9 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [ signatures: [ { params: [{ name: 'field', type: 'string' }], - returnType: 'version', + returnType: 'string', examples: [`from index | EVAL version = to_version(stringField)`], }, - { - params: [{ name: 'field', type: 'version' }], - returnType: 'version', - examples: [`from index | EVAL version = to_version(versionField)`], - }, ], }, { @@ -924,6 +918,30 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [ }, ], }, + { + name: 'mv_sort', + description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.mvSortDoc', { + defaultMessage: 'Sorts a multivalue expression in lexicographical order.', + }), + signatures: [ + { + params: [ + { name: 'field', type: 'any' }, + { + name: 'order', + type: 'string', + optional: true, + literalOptions: ['asc', 'desc'], + }, + ], + returnType: 'any', + examples: [ + 'row a = [4, 2, -3, 2] | eval sorted = mv_sort(a)', + 'row a = ["b", "c", "a"] | sorted = mv_sort(a, "DESC")', + ], + }, + ], + }, { name: 'mv_avg', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.mvAvgDoc', { diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index 111846f1f515f..6d3488aacc003 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -23,7 +23,22 @@ export interface FunctionDefinition { optional?: boolean; noNestingFunctions?: boolean; supportsWildcard?: boolean; + /** + * if set this indicates that the value must be a literal + * but can be any literal of the correct type + */ literalOnly?: boolean; + /** + * if provided this means that the value must be one + * of the options in the array iff the value is a literal. + * + * String values are case insensitive. + * + * If the value is not a literal, this field is ignored because + * we can't check the return value of a function to see if it + * matches one of the options prior to runtime. + */ + literalOptions?: string[]; }>; minParams?: number; returnType: string; @@ -87,3 +102,5 @@ export type SignatureType = | FunctionDefinition['signatures'][number] | CommandOptionsDefinition['signature']; export type SignatureArgType = SignatureType['params'][number]; + +export type FunctionArgSignature = FunctionDefinition['signatures'][number]['params'][number]; diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 8eab378214a47..1d439aa3a0b9e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -34,6 +34,7 @@ import { import type { CommandDefinition, CommandOptionsDefinition, + FunctionArgSignature, FunctionDefinition, SignatureArgType, } from '../definitions/types'; @@ -179,6 +180,8 @@ export function getFunctionDefinition(name: string) { return buildFunctionLookup().get(name.toLowerCase()); } +const unwrapStringLiteralQuotes = (value: string) => value.slice(1, -1); + function buildCommandLookup() { if (!commandLookups) { commandLookups = commandDefinitions.reduce((memo, def) => { @@ -337,8 +340,28 @@ export function inKnownTimeInterval(item: ESQLTimeInterval): boolean { return timeLiterals.some(({ name }) => name === item.unit.toLowerCase()); } +/** + * Checks if this argument is one of the possible options + * if they are defined on the arg definition. + * + * TODO - Consider merging with isEqualType to create a unified arg validation function + */ +export function isValidLiteralOption(arg: ESQLLiteral, argDef: FunctionArgSignature) { + return ( + arg.literalType === 'string' && + argDef.literalOptions && + !argDef.literalOptions + .map((option) => option.toLowerCase()) + .includes(unwrapStringLiteralQuotes(arg.value).toLowerCase()) + ); +} + +/** + * Checks if an AST argument is of the correct type + * given the definition. + */ export function isEqualType( - item: ESQLSingleAstItem, + arg: ESQLSingleAstItem, argDef: SignatureArgType, references: ReferenceMaps, parentCommand?: string, @@ -348,24 +371,24 @@ export function isEqualType( if (argType === 'any') { return true; } - if (item.type === 'literal') { - return compareLiteralType(argType, item); + if (arg.type === 'literal') { + return compareLiteralType(argType, arg); } - if (item.type === 'function') { - if (isSupportedFunction(item.name, parentCommand).supported) { - const fnDef = buildFunctionLookup().get(item.name)!; + if (arg.type === 'function') { + if (isSupportedFunction(arg.name, parentCommand).supported) { + const fnDef = buildFunctionLookup().get(arg.name)!; return fnDef.signatures.some((signature) => argType === signature.returnType); } } - if (item.type === 'timeInterval') { - return argType === 'time_literal' && inKnownTimeInterval(item); + if (arg.type === 'timeInterval') { + return argType === 'time_literal' && inKnownTimeInterval(arg); } - if (item.type === 'column') { + if (arg.type === 'column') { if (argType === 'column') { // anything goes, so avoid any effort here return true; } - const hit = getColumnHit(nameHit ?? item.name, references); + const hit = getColumnHit(nameHit ?? arg.name, references); const validHit = hit; if (!validHit) { return false; diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts b/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts index b7e02f10683f3..5c8608e37ea7c 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts @@ -300,6 +300,22 @@ function getMessageAndTypeFromId({ ), type: 'error', }; + case 'unsupportedLiteralOption': + return { + message: i18n.translate( + 'kbn-esql-validation-autocomplete.esql.validation.unsupportedLiteralOption', + { + defaultMessage: + 'Invalid option [{value}] for {name}. Supported options: [{supportedOptions}].', + values: { + name: out.name, + value: out.value, + supportedOptions: out.supportedOptions, + }, + } + ), + type: 'warning', + }; case 'expectedConstant': return { message: i18n.translate( diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index c64d7235b907a..4adae6dc511eb 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -1370,6 +1370,16 @@ "error": [], "warning": [] }, + { + "query": "row var = mv_sort(\"a\", \"asc\")", + "error": [], + "warning": [] + }, + { + "query": "row mv_sort(\"a\", \"asc\")", + "error": [], + "warning": [] + }, { "query": "row var = mv_sum(5)", "error": [], @@ -2005,21 +2015,6 @@ "error": [], "warning": [] }, - { - "query": "row var = to_version(\"a\")", - "error": [], - "warning": [] - }, - { - "query": "row to_version(\"a\")", - "error": [], - "warning": [] - }, - { - "query": "row var = to_ver(\"a\")", - "error": [], - "warning": [] - }, { "query": "row var = trim(\"a\")", "error": [], @@ -2333,6 +2328,23 @@ ], "warning": [] }, + { + "query": "row var = mv_sort([\"a\", \"b\"], \"bogus\")", + "error": [], + "warning": [ + "Invalid option [\"bogus\"] for mv_sort. Supported options: [\"asc\", \"desc\"]." + ] + }, + { + "query": "row var = mv_sort([\"a\", \"b\"], \"ASC\")", + "error": [], + "warning": [] + }, + { + "query": "row var = mv_sort([\"a\", \"b\"], \"DESC\")", + "error": [], + "warning": [] + }, { "query": "row 1 anno", "error": [ @@ -5751,6 +5763,18 @@ ], "warning": [] }, + { + "query": "from a_index | where length(to_version(stringField)) > 0", + "error": [], + "warning": [] + }, + { + "query": "from a_index | where length(to_version(numberField)) > 0", + "error": [ + "Argument of [to_version] must be [string], found value [numberField] type [number]" + ], + "warning": [] + }, { "query": "from a_index | where length(trim(stringField)) > 0", "error": [], @@ -6883,6 +6907,34 @@ ], "warning": [] }, + { + "query": "from a_index | eval var = values(stringField)", + "error": [ + "EVAL does not support function values" + ], + "warning": [] + }, + { + "query": "from a_index | eval var = values(stringField) > 0", + "error": [ + "EVAL does not support function values" + ], + "warning": [] + }, + { + "query": "from a_index | eval values(stringField)", + "error": [ + "EVAL does not support function values" + ], + "warning": [] + }, + { + "query": "from a_index | eval values(stringField) > 0", + "error": [ + "EVAL does not support function values" + ], + "warning": [] + }, { "query": "from a_index | eval var = abs(numberField)", "error": [], @@ -7871,6 +7923,16 @@ ], "warning": [] }, + { + "query": "from a_index | eval var = mv_sort(stringField, \"asc\")", + "error": [], + "warning": [] + }, + { + "query": "from a_index | eval mv_sort(stringField, \"asc\")", + "error": [], + "warning": [] + }, { "query": "from a_index | eval var = mv_sum(numberField)", "error": [], @@ -8821,28 +8883,6 @@ ], "warning": [] }, - { - "query": "from a_index | eval var = to_version(stringField)", - "error": [], - "warning": [] - }, - { - "query": "from a_index | eval to_version(stringField)", - "error": [], - "warning": [] - }, - { - "query": "from a_index | eval var = to_ver(stringField)", - "error": [], - "warning": [] - }, - { - "query": "from a_index | eval var = to_version(*)", - "error": [ - "Using wildcards (*) in to_version is not allowed" - ], - "warning": [] - }, { "query": "from a_index | eval var = trim(stringField)", "error": [], @@ -9413,6 +9453,23 @@ ], "warning": [] }, + { + "query": "from a_index | eval mv_sort([\"a\", \"b\"], \"bogus\")", + "error": [], + "warning": [ + "Invalid option [\"bogus\"] for mv_sort. Supported options: [\"asc\", \"desc\"]." + ] + }, + { + "query": "from a_index | eval mv_sort([\"a\", \"b\"], \"ASC\")", + "error": [], + "warning": [] + }, + { + "query": "from a_index | eval mv_sort([\"a\", \"b\"], \"DESC\")", + "error": [], + "warning": [] + }, { "query": "from a_index | eval 1 anno", "error": [ @@ -12175,6 +12232,16 @@ ], "warning": [] }, + { + "query": "from a_index | stats var = values(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | stats values(stringField)", + "error": [], + "warning": [] + }, { "query": "FROM index\n | EVAL numberField * 3.281\n | STATS avg_numberField = AVG(`numberField * 3.281`)", "error": [], diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/types.ts b/packages/kbn-esql-validation-autocomplete/src/validation/types.ts index 0aec2b79eaa89..aaf98773eca2c 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/types.ts @@ -96,6 +96,10 @@ export interface ValidationErrors { message: string; type: { name: string; command: string; option: string }; }; + unsupportedLiteralOption: { + message: string; + type: { name: string; value: string; supportedOptions: string }; + }; shadowFieldType: { message: string; type: { field: string; fieldType: string; newType: string }; diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index 7d19f4c23cad1..0e48e60b9cc3d 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -164,9 +164,17 @@ function getFieldMapping( string: `"a"`, number: '5', }; - return params.map(({ name: _name, type, literalOnly, ...rest }) => { + return params.map(({ name: _name, type, literalOnly, literalOptions, ...rest }) => { const typeString: string = type; if (fieldTypes.includes(typeString)) { + if (useLiterals && literalOptions) { + return { + name: `"${literalOptions[0]}"`, + type, + ...rest, + }; + } + const fieldName = literalOnly && typeString in literalValues ? literalValues[typeString as keyof typeof literalValues]! @@ -187,7 +195,7 @@ function getFieldMapping( ...rest, }; } - if (/[]$/.test(typeString)) { + if (/\[\]$/.test(typeString)) { return { name: getMultiValue(typeString), type, @@ -198,7 +206,7 @@ function getFieldMapping( }); } -function generateWrongMappingForArgs( +function generateIncorrectlyTypedParameters( name: string, signatures: FunctionDefinition['signatures'], currentParams: FunctionDefinition['signatures'][number]['params'], @@ -208,28 +216,30 @@ function generateWrongMappingForArgs( string: `"a"`, number: '5', }; - const wrongFieldMapping = currentParams.map(({ name: _name, literalOnly, type, ...rest }, i) => { - // this thing is complex enough, let's not make it harder for constants - if (literalOnly) { - return { name: literalValues[type as keyof typeof literalValues], type, ...rest }; + const wrongFieldMapping = currentParams.map( + ({ name: _name, literalOnly, literalOptions, type, ...rest }, i) => { + // this thing is complex enough, let's not make it harder for constants + if (literalOnly) { + return { name: literalValues[type as keyof typeof literalValues], type, ...rest }; + } + const canBeFieldButNotString = Boolean( + fieldTypes.filter((t) => t !== 'string').includes(type) && + signatures.every(({ params: fnParams }) => fnParams[i].type !== 'string') + ); + const canBeFieldButNotNumber = + fieldTypes.filter((t) => t !== 'number').includes(type) && + signatures.every(({ params: fnParams }) => fnParams[i].type !== 'number'); + const isLiteralType = /literal$/.test(type); + // pick a field name purposely wrong + const nameValue = + canBeFieldButNotString || isLiteralType + ? values.stringField + : canBeFieldButNotNumber + ? values.numberField + : values.booleanField; + return { name: nameValue, type, ...rest }; } - const canBeFieldButNotString = Boolean( - fieldTypes.filter((t) => t !== 'string').includes(type) && - signatures.every(({ params: fnParams }) => fnParams[i].type !== 'string') - ); - const canBeFieldButNotNumber = - fieldTypes.filter((t) => t !== 'number').includes(type) && - signatures.every(({ params: fnParams }) => fnParams[i].type !== 'number'); - const isLiteralType = /literal$/.test(type); - // pick a field name purposely wrong - const nameValue = - canBeFieldButNotString || isLiteralType - ? values.stringField - : canBeFieldButNotNumber - ? values.numberField - : values.booleanField; - return { name: nameValue, type, ...rest }; - }); + ); const generatedFieldTypes = { [values.stringField]: 'string', @@ -250,6 +260,7 @@ function generateWrongMappingForArgs( return `Argument of [${name}] must be [${type}], found value [${fieldName}] type [${generatedFieldTypes[fieldName]}]`; }) .filter(nonNullable); + return { wrongFieldMapping, expectedErrors }; } @@ -565,7 +576,7 @@ describe('validation logic', () => { // the right error message if ( params.every(({ type }) => type !== 'any') && - !['auto_bucket', 'to_version'].includes(name) + !['auto_bucket', 'to_version', 'mv_sort'].includes(name) ) { // now test nested functions const fieldMappingWithNestedFunctions = getFieldMapping(params, { @@ -585,11 +596,15 @@ describe('validation logic', () => { testErrorsAndWarnings(`row var = ${signatureString}`, []); - const { wrongFieldMapping, expectedErrors } = generateWrongMappingForArgs( + const { wrongFieldMapping, expectedErrors } = generateIncorrectlyTypedParameters( name, signatures, params, - { stringField: '"a"', numberField: '5', booleanField: 'true' } + { + stringField: '"a"', + numberField: '5', + booleanField: 'true', + } ); const wrongSignatureString = tweakSignatureForRowCommand( getFunctionSignatures( @@ -634,6 +649,15 @@ describe('validation logic', () => { ]); } + testErrorsAndWarnings( + `row var = mv_sort(["a", "b"], "bogus")`, + [], + ['Invalid option ["bogus"] for mv_sort. Supported options: ["asc", "desc"].'] + ); + + testErrorsAndWarnings(`row var = mv_sort(["a", "b"], "ASC")`, []); + testErrorsAndWarnings(`row var = mv_sort(["a", "b"], "DESC")`, []); + describe('date math', () => { testErrorsAndWarnings('row 1 anno', [ 'ROW does not support [date_period] in expression [1 anno]', @@ -1187,7 +1211,7 @@ describe('validation logic', () => { [] ); - const { wrongFieldMapping, expectedErrors } = generateWrongMappingForArgs( + const { wrongFieldMapping, expectedErrors } = generateIncorrectlyTypedParameters( name, signatures, params, @@ -1435,7 +1459,7 @@ describe('validation logic', () => { // the right error message if ( params.every(({ type }) => type !== 'any') && - !['auto_bucket', 'to_version'].includes(name) + !['auto_bucket', 'to_version', 'mv_sort'].includes(name) ) { // now test nested functions const fieldMappingWithNestedFunctions = getFieldMapping(params, { @@ -1455,7 +1479,7 @@ describe('validation logic', () => { }` ); - const { wrongFieldMapping, expectedErrors } = generateWrongMappingForArgs( + const { wrongFieldMapping, expectedErrors } = generateIncorrectlyTypedParameters( name, signatures, params, @@ -1676,6 +1700,15 @@ describe('validation logic', () => { "SyntaxError: mismatched input '' expecting {',', ')'}", ]); + testErrorsAndWarnings( + 'from a_index | eval mv_sort(["a", "b"], "bogus")', + [], + ['Invalid option ["bogus"] for mv_sort. Supported options: ["asc", "desc"].'] + ); + + testErrorsAndWarnings(`from a_index | eval mv_sort(["a", "b"], "ASC")`, []); + testErrorsAndWarnings(`from a_index | eval mv_sort(["a", "b"], "DESC")`, []); + describe('date math', () => { testErrorsAndWarnings('from a_index | eval 1 anno', [ 'EVAL does not support [date_period] in expression [1 anno]', @@ -2064,7 +2097,7 @@ describe('validation logic', () => { // the right error message if ( params.every(({ type }) => type !== 'any') && - !['auto_bucket', 'to_version'].includes(name) + !['auto_bucket', 'to_version', 'mv_sort'].includes(name) ) { // now test nested functions const fieldMappingWithNestedAggsFunctions = getFieldMapping(params, { @@ -2103,7 +2136,7 @@ describe('validation logic', () => { }`, nestedAggsExpectedErrors ); - const { wrongFieldMapping, expectedErrors } = generateWrongMappingForArgs( + const { wrongFieldMapping, expectedErrors } = generateIncorrectlyTypedParameters( name, signatures, params, diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts index c8e60bf4c7e96..3d508dc93389f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -22,6 +22,7 @@ import type { import { CommandModeDefinition, CommandOptionsDefinition, + FunctionArgSignature, FunctionDefinition, SignatureArgType, } from '../definitions/types'; @@ -51,6 +52,7 @@ import { isSettingItem, isAssignment, isVariable, + isValidLiteralOption, } from '../shared/helpers'; import { collectVariables } from '../shared/variables'; import { getMessageFromId, getUnknownTypeLabel } from './errors'; @@ -75,12 +77,30 @@ import { function validateFunctionLiteralArg( astFunction: ESQLFunction, actualArg: ESQLAstItem, - argDef: SignatureArgType, + argDef: FunctionArgSignature, references: ReferenceMaps, parentCommand: string ) { const messages: ESQLMessage[] = []; if (isLiteralItem(actualArg)) { + if ( + actualArg.literalType === 'string' && + argDef.literalOptions && + isValidLiteralOption(actualArg, argDef) + ) { + messages.push( + getMessageFromId({ + messageId: 'unsupportedLiteralOption', + values: { + name: astFunction.name, + value: actualArg.value, + supportedOptions: argDef.literalOptions?.map((option) => `"${option}"`).join(', '), + }, + locations: actualArg.location, + }) + ); + } + if (!isEqualType(actualArg, argDef, references, parentCommand)) { messages.push( getMessageFromId({ diff --git a/packages/kbn-ftr-common-functional-ui-services/services/test_subjects.ts b/packages/kbn-ftr-common-functional-ui-services/services/test_subjects.ts index 730b7a692aabe..39cd21f284132 100644 --- a/packages/kbn-ftr-common-functional-ui-services/services/test_subjects.ts +++ b/packages/kbn-ftr-common-functional-ui-services/services/test_subjects.ts @@ -21,6 +21,10 @@ interface SetValueOptions { typeCharByChar?: boolean; } +export function nonNullable(v: T): v is NonNullable { + return v != null; +} + export class TestSubjects extends FtrService { public readonly log = this.ctx.getService('log'); public readonly retry = this.ctx.getService('retry'); @@ -226,9 +230,11 @@ export class TestSubjects extends FtrService { public async getAttributeAll(selector: string, attribute: string): Promise { this.log.debug(`TestSubjects.getAttributeAll(${selector}, ${attribute})`); - return await this._mapAll(selector, async (element: WebElementWrapper) => { - return await element.getAttribute(attribute); - }); + return ( + await this._mapAll(selector, async (element: WebElementWrapper) => { + return await element.getAttribute(attribute); + }) + ).filter(nonNullable); } public async getAttribute( @@ -240,7 +246,7 @@ export class TestSubjects extends FtrService { findTimeout?: number; tryTimeout?: number; } - ): Promise { + ): Promise { const findTimeout = (typeof options === 'number' ? options : options?.findTimeout) ?? this.config.get('timeouts.find'); diff --git a/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts b/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts index 568d8dc5cd879..e7083d7f17587 100644 --- a/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts +++ b/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts @@ -231,7 +231,7 @@ export class WebElementWrapper { * @return {Promise} */ public async elementHasClass(className: string): Promise { - const classes: string = await this._webElement.getAttribute('class'); + const classes = (await this._webElement.getAttribute('class')) ?? ''; return classes.includes(className); } @@ -262,7 +262,7 @@ export class WebElementWrapper { */ async clearValueWithKeyboard(options: TypeOptions = { charByChar: false }) { const value = await this.getAttribute('value'); - if (!value.length) { + if (!value?.length) { return; } @@ -344,10 +344,11 @@ export class WebElementWrapper { * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getAttribute * * @param {string} name + * @return {Promise} */ - public async getAttribute(name: string) { - return await this.retryCall(async function getAttribute(wrapper) { - return await wrapper._webElement.getAttribute(name); + public async getAttribute(name: string): Promise { + return await this.retryCall(async function getAttribute(wrapper): Promise { + return await wrapper._webElement.getAttribute(name); // this returns null if not found }); } diff --git a/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx b/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx index fc469d9cb234f..19faf02262aae 100644 --- a/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx +++ b/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx @@ -9,7 +9,7 @@ import moment from 'moment'; import userEvent from '@testing-library/user-event'; import { screen, within, fireEvent } from '@testing-library/react'; -export const getButtonGroupInputValue = (testId: string) => () => { +export const getSelectedButtonInGroup = (testId: string) => () => { const buttonGroup = screen.getByTestId(testId); return within(buttonGroup).getByRole('button', { pressed: true }); }; diff --git a/packages/kbn-test-subj-selector/test_subj_selector.test.ts b/packages/kbn-test-subj-selector/test_subj_selector.test.ts index d8ad1e6ddfd56..e54cb516ba89a 100644 --- a/packages/kbn-test-subj-selector/test_subj_selector.test.ts +++ b/packages/kbn-test-subj-selector/test_subj_selector.test.ts @@ -13,6 +13,10 @@ describe('testSubjSelector()', function () { expect(subj('foo bar')).toEqual('[data-test-subj="foo bar"]'); expect(subj('foo > bar')).toEqual('[data-test-subj="foo"] [data-test-subj="bar"]'); expect(subj('foo > bar baz')).toEqual('[data-test-subj="foo"] [data-test-subj="bar baz"]'); + expect(subj('*foo')).toEqual('[data-test-subj*="foo"]'); + expect(subj('foo*')).toEqual('[data-test-subj="foo*"]'); + expect(subj('foo*bar')).toEqual('[data-test-subj="foo*bar"]'); + expect(subj('*foo >* bar')).toEqual('[data-test-subj*="foo"] [data-test-subj*="bar"]'); expect(subj('foo> ~bar')).toEqual('[data-test-subj="foo"] [data-test-subj~="bar"]'); expect(subj('~ foo')).toEqual('[data-test-subj~="foo"]'); expect(subj('~foo & ~ bar')).toEqual('[data-test-subj~="foo"][data-test-subj~="bar"]'); diff --git a/packages/kbn-test-subj-selector/test_subj_selector.ts b/packages/kbn-test-subj-selector/test_subj_selector.ts index e6db49755a89b..ca6bf569db68e 100644 --- a/packages/kbn-test-subj-selector/test_subj_selector.ts +++ b/packages/kbn-test-subj-selector/test_subj_selector.ts @@ -8,6 +8,7 @@ function selectorToTerms(selector: string) { return selector + .replace(/\s*\*\s*/g, '*') // css locator with '*' operator cannot contain spaces .replace(/\s*~\s*/g, '~') // css locator with '~' operator cannot contain spaces .replace(/\s*>\s*/g, '>') // remove all whitespace around joins > .replace(/\s*&\s*/g, '&') // remove all whitespace around joins & @@ -16,9 +17,13 @@ function selectorToTerms(selector: string) { function termToCssSelector(term: string) { if (term) { - return term.startsWith('~') - ? '[data-test-subj~="' + term.substring(1).replace(/\s/g, '') + '"]' - : '[data-test-subj="' + term + '"]'; + if (term.startsWith('~')) { + return '[data-test-subj~="' + term.substring(1).replace(/\s/g, '') + '"]'; + } else if (term.startsWith('*')) { + return '[data-test-subj*="' + term.substring(1).replace(/\s/g, '') + '"]'; + } else { + return '[data-test-subj="' + term + '"]'; + } } else { return ''; } @@ -31,17 +36,28 @@ function termToCssSelector(term: string) { * * - `data-test-subj` values can include spaces * - * - prefixing a value with `~` will allow matching a single word in a `data-test-subj` that uses several space delimited list words + * - prefixing a value with `*` will allow matching a `data-test-subj` attribute containing at least one occurrence of value within the string. + * - example: `*foo` + * - css equivalent: `[data-test-subj*="foo"]` + * - DOM match example:
data-test-subj="bar-foo"
+ * + * - prefixing a value with `~` will allow matching a `data-test-subj` attribute represented as a whitespace-separated list of words, one of which is exactly value * - example: `~foo` * - css equivalent: `[data-test-subj~="foo"]` + * - DOM match example:
data-test-subj="foo bar"
* * - the `>` character is used between two values to indicate that the value on the right must match an element inside an element matched by the value on the left * - example: `foo > bar` * - css equivalent: `[data-test-subj=foo] [data-test-subj=bar]` + * - DOM match example: + *
data-test-subj="foo" + *
data-test-subj="bar"
+ *
* * - the `&` character is used between two values to indicate that the value on both sides must both match the element * - example: `foo & bar` * - css equivalent: `[data-test-subj=foo][data-test-subj=bar]` + * - DOM match example:
data-test-subj="foo bar"
*/ export function subj(selector: string) { return selectorToTerms(selector) 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 5c83b1bffb540..4c4440fe70029 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 @@ -22,7 +22,7 @@ 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 { CoreStart } from '@kbn/core/public'; -import type { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public'; +import type { IndexManagementPluginSetup } from '@kbn/index-management'; import { TooltipWrapper } from '@kbn/visualization-utils'; import { type LanguageDocumentationSections, diff --git a/packages/kbn-text-based-editor/tsconfig.json b/packages/kbn-text-based-editor/tsconfig.json index ffd9ff79b6aad..78cee94bd68c8 100644 --- a/packages/kbn-text-based-editor/tsconfig.json +++ b/packages/kbn-text-based-editor/tsconfig.json @@ -23,7 +23,7 @@ "@kbn/data-plugin", "@kbn/expressions-plugin", "@kbn/data-views-plugin", - "@kbn/index-management-plugin", + "@kbn/index-management", "@kbn/visualization-utils", "@kbn/code-editor", "@kbn/shared-ux-markdown", diff --git a/packages/presentation/presentation_containers/index.ts b/packages/presentation/presentation_containers/index.ts index 7501766c5cdde..f6049b284eae2 100644 --- a/packages/presentation/presentation_containers/index.ts +++ b/packages/presentation/presentation_containers/index.ts @@ -25,6 +25,10 @@ export { type PanelPackage, type PresentationContainer, } from './interfaces/presentation_container'; +export { + canTrackContentfulRender, + type TrackContentfulRender, +} from './interfaces/track_contentful_render'; export { apiHasSerializableState, type HasSerializableState, diff --git a/test/functional/apps/navigation/index.ts b/packages/presentation/presentation_containers/interfaces/track_contentful_render.ts similarity index 51% rename from test/functional/apps/navigation/index.ts rename to packages/presentation/presentation_containers/interfaces/track_contentful_render.ts index 6a05d098e794e..c5f42c0f99cd4 100644 --- a/test/functional/apps/navigation/index.ts +++ b/packages/presentation/presentation_containers/interfaces/track_contentful_render.ts @@ -6,10 +6,13 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('navigation app', function () { - loadTestFile(require.resolve('./_solution_nav_switcher')); - }); +export interface TrackContentfulRender { + /** + * A way to report that the contentful render has been completed + */ + trackContentfulRender: () => void; } + +export const canTrackContentfulRender = (root: unknown): root is TrackContentfulRender => { + return root !== null && typeof root === 'object' && 'trackContentfulRender' in root; +}; diff --git a/packages/presentation/presentation_publishing/index.ts b/packages/presentation/presentation_publishing/index.ts index d3764da74c6d2..e4476cbd2cff9 100644 --- a/packages/presentation/presentation_publishing/index.ts +++ b/packages/presentation/presentation_publishing/index.ts @@ -111,6 +111,7 @@ export { } from './interfaces/titles/publishes_panel_title'; export { initializeTitles, type SerializedTitles } from './interfaces/titles/titles_api'; export { + useBatchedOptionalPublishingSubjects, useBatchedPublishingSubjects, usePublishingSubject, useStateFromPublishingSubject, diff --git a/packages/presentation/presentation_publishing/publishing_subject/index.ts b/packages/presentation/presentation_publishing/publishing_subject/index.ts index 5dbd2eb95579a..022c4170f6cde 100644 --- a/packages/presentation/presentation_publishing/publishing_subject/index.ts +++ b/packages/presentation/presentation_publishing/publishing_subject/index.ts @@ -6,7 +6,10 @@ * Side Public License, v 1. */ -export { useBatchedPublishingSubjects } from './publishing_batcher'; +export { + useBatchedOptionalPublishingSubjects, + useBatchedPublishingSubjects, +} from './publishing_batcher'; export { useStateFromPublishingSubject, usePublishingSubject } from './publishing_subject'; export type { PublishingSubject, diff --git a/packages/presentation/presentation_publishing/publishing_subject/publishing_batcher.ts b/packages/presentation/presentation_publishing/publishing_subject/publishing_batcher.ts index 4624e43c2a0d1..f04661573d918 100644 --- a/packages/presentation/presentation_publishing/publishing_subject/publishing_batcher.ts +++ b/packages/presentation/presentation_publishing/publishing_subject/publishing_batcher.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { combineLatest, debounceTime, skip } from 'rxjs'; import { AnyPublishingSubject, PublishingSubject, UnwrapPublishingSubjectTuple } from './types'; @@ -25,25 +25,28 @@ const hasSubjectsArrayChanged = ( /** * Batches the latest values of multiple publishing subjects into a single object. Use this to avoid unnecessary re-renders. - * You should avoid using this hook with subjects that your component pushes values to on user interaction, as it can cause a slight delay. + * Use when `subjects` may not be defined on initial component render. + * * @param subjects Publishing subjects array. * When 'subjects' is expected to change, 'subjects' must be part of component react state. */ -export const useBatchedPublishingSubjects = ( +export const useBatchedOptionalPublishingSubjects = < + SubjectsType extends [...AnyPublishingSubject[]] +>( ...subjects: [...SubjectsType] ): UnwrapPublishingSubjectTuple => { const isFirstRender = useRef(true); - /** - * memoize and deep diff subjects to avoid rebuilding the subscription when the subjects are the same. - */ + const previousSubjects = useRef(subjects); - const subjectsToUse = useMemo(() => { + // Can not use 'useMemo' because 'subjects' gets a new reference on each call because of spread + const subjectsToUse = (() => { + // avoid rebuilding the subscription when the subjects are the same if (!hasSubjectsArrayChanged(previousSubjects.current ?? [], subjects)) { return previousSubjects.current; } previousSubjects.current = subjects; return subjects; - }, [subjects]); + })(); /** * Set up latest published values state, initialized with the current values of the subjects. @@ -94,6 +97,46 @@ export const useBatchedPublishingSubjects = >] +>( + ...subjects: [...SubjectsType] +): UnwrapPublishingSubjectTuple => { + /** + * Set up latest published values state, initialized with the current values of the subjects. + */ + const [latestPublishedValues, setLatestPublishedValues] = useState< + UnwrapPublishingSubjectTuple + >(() => unwrapPublishingSubjectArray(subjects)); + + /** + * Subscribe to all subjects and update the latest values when any of them change. + */ + useEffect(() => { + const subscription = combineLatest(subjects) + .pipe( + // When a new observer subscribes to a BehaviorSubject, it immediately receives the current value. Skip this emit. + skip(1), + debounceTime(0) + ) + .subscribe((values) => { + setLatestPublishedValues(values as UnwrapPublishingSubjectTuple); + }); + return () => subscription.unsubscribe(); + // 'subjects' gets a new reference on each call because of spread + // Use 'useBatchedOptionalPublishingSubjects' when 'subjects' are expected to change. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return latestPublishedValues; +}; + const unwrapPublishingSubjectArray = ( subjects: T ): UnwrapPublishingSubjectTuple => { diff --git a/packages/presentation/presentation_publishing/publishing_subject/publishing_subject.test.tsx b/packages/presentation/presentation_publishing/publishing_subject/publishing_subject.test.tsx index e58ca06d54f9b..ec0d80c0dd3c9 100644 --- a/packages/presentation/presentation_publishing/publishing_subject/publishing_subject.test.tsx +++ b/packages/presentation/presentation_publishing/publishing_subject/publishing_subject.test.tsx @@ -11,11 +11,14 @@ import { BehaviorSubject } from 'rxjs'; import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; -import { useBatchedPublishingSubjects } from './publishing_batcher'; +import { + useBatchedPublishingSubjects, + useBatchedOptionalPublishingSubjects, +} from './publishing_batcher'; import { useStateFromPublishingSubject } from './publishing_subject'; import { PublishingSubject } from './types'; -describe('useBatchedPublishingSubjects', () => { +describe('publishing subject', () => { describe('render', () => { let subject1: BehaviorSubject; let subject2: BehaviorSubject; @@ -56,7 +59,6 @@ describe('useBatchedPublishingSubjects', () => { <>