diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e6777e5e69f29a3..d4bf323f938a708 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -183,6 +183,7 @@ x-pack/examples/files_example @elastic/kibana-app-services /src/plugins/controls/ @elastic/kibana-presentation /test/functional/apps/dashboard/ @elastic/kibana-presentation /test/functional/apps/dashboard_elements/ @elastic/kibana-presentation +/test/functional/services/dashboard/ @elastic/kibana-presentation /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 7a1eaae67690664..e98561ce501082a 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -89,7 +89,7 @@ Set this property to `false` to prevent the filter editor and KQL autocomplete from suggesting values for fields. [[autocomplete-valuesuggestionmethod]]`autocomplete:valueSuggestionMethod`:: -When set to `terms_enum`, autocomplete uses the terms enum API for value suggestions. Kibana returns results faster, but suggestions are approximate, sorted alphabetically, and can be outside the selected time range. +When set to `terms_enum`, autocomplete uses the terms enum API for value suggestions. Kibana returns results faster, but suggestions are approximate, sorted alphabetically, and can be outside the selected time range. (Note that this API is incompatible with {ref}/document-level-security.html[Document-Level-Security].) When set to `terms_agg`, Kibana uses a terms aggregation for value suggestions, which is slower, but suggestions include all values that optionally match your time range and are sorted by popularity. diff --git a/docs/maps/asset-tracking-tutorial.asciidoc b/docs/maps/asset-tracking-tutorial.asciidoc index 85629e0e611f6c2..4e6efff35b3a35f 100644 --- a/docs/maps/asset-tracking-tutorial.asciidoc +++ b/docs/maps/asset-tracking-tutorial.asciidoc @@ -318,9 +318,9 @@ cloud.auth: . In {kib}, open the main menu, and click *Stack Management > Data Views*. . Click *Create data view*. . Give the data view a name: *tri_met_tracks**. -. Click *Next step*. -. Set the *Time field* to *trimet.time*. -. Click *Create data view*. +. Set the index pattern as: *tri_met_tracks**. +. Set the *Timestamp field* to *trimet.time*. +. Click *Save data view to Kibana*. {kib} shows the fields in your data view. diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index b084fa8617929e3..812d73e82826f37 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -14,110 +14,110 @@ filegroup( "//packages/analytics/shippers/elastic_v3/common:build", "//packages/analytics/shippers/elastic_v3/server:build", "//packages/analytics/shippers/fullstory:build", + "//packages/core/analytics/core-analytics-browser:build", "//packages/core/analytics/core-analytics-browser-internal:build", "//packages/core/analytics/core-analytics-browser-mocks:build", - "//packages/core/analytics/core-analytics-browser:build", + "//packages/core/analytics/core-analytics-server:build", "//packages/core/analytics/core-analytics-server-internal:build", "//packages/core/analytics/core-analytics-server-mocks:build", - "//packages/core/analytics/core-analytics-server:build", "//packages/core/base/core-base-browser-internal:build", "//packages/core/base/core-base-browser-mocks:build", - "//packages/core/base/core-base-common-internal:build", "//packages/core/base/core-base-common:build", + "//packages/core/base/core-base-common-internal:build", "//packages/core/base/core-base-server-internal:build", "//packages/core/base/core-base-server-mocks:build", "//packages/core/capabilities/core-capabilities-common:build", + "//packages/core/capabilities/core-capabilities-server:build", "//packages/core/capabilities/core-capabilities-server-internal:build", "//packages/core/capabilities/core-capabilities-server-mocks:build", - "//packages/core/capabilities/core-capabilities-server:build", "//packages/core/config/core-config-server-internal:build", + "//packages/core/deprecations/core-deprecations-browser:build", "//packages/core/deprecations/core-deprecations-browser-internal:build", "//packages/core/deprecations/core-deprecations-browser-mocks:build", - "//packages/core/deprecations/core-deprecations-browser:build", "//packages/core/deprecations/core-deprecations-common:build", + "//packages/core/doc-links/core-doc-links-browser:build", "//packages/core/doc-links/core-doc-links-browser-internal:build", "//packages/core/doc-links/core-doc-links-browser-mocks:build", - "//packages/core/doc-links/core-doc-links-browser:build", + "//packages/core/doc-links/core-doc-links-server:build", "//packages/core/doc-links/core-doc-links-server-internal:build", "//packages/core/doc-links/core-doc-links-server-mocks:build", - "//packages/core/doc-links/core-doc-links-server:build", "//packages/core/elasticsearch/core-elasticsearch-client-server-internal:build", "//packages/core/elasticsearch/core-elasticsearch-client-server-mocks:build", + "//packages/core/elasticsearch/core-elasticsearch-server:build", "//packages/core/elasticsearch/core-elasticsearch-server-internal:build", "//packages/core/elasticsearch/core-elasticsearch-server-mocks:build", - "//packages/core/elasticsearch/core-elasticsearch-server:build", "//packages/core/environment/core-environment-server-internal:build", "//packages/core/environment/core-environment-server-mocks:build", + "//packages/core/execution-context/core-execution-context-browser:build", "//packages/core/execution-context/core-execution-context-browser-internal:build", "//packages/core/execution-context/core-execution-context-browser-mocks:build", - "//packages/core/execution-context/core-execution-context-browser:build", "//packages/core/execution-context/core-execution-context-common:build", + "//packages/core/execution-context/core-execution-context-server:build", "//packages/core/execution-context/core-execution-context-server-internal:build", "//packages/core/execution-context/core-execution-context-server-mocks:build", - "//packages/core/execution-context/core-execution-context-server:build", + "//packages/core/fatal-errors/core-fatal-errors-browser:build", "//packages/core/fatal-errors/core-fatal-errors-browser-internal:build", "//packages/core/fatal-errors/core-fatal-errors-browser-mocks:build", - "//packages/core/fatal-errors/core-fatal-errors-browser:build", + "//packages/core/http/core-http-browser:build", "//packages/core/http/core-http-browser-internal:build", "//packages/core/http/core-http-browser-mocks:build", - "//packages/core/http/core-http-browser:build", "//packages/core/http/core-http-common:build", "//packages/core/http/core-http-context-server-internal:build", "//packages/core/http/core-http-context-server-mocks:build", "//packages/core/http/core-http-router-server-internal:build", "//packages/core/http/core-http-router-server-mocks:build", + "//packages/core/http/core-http-server:build", "//packages/core/http/core-http-server-internal:build", "//packages/core/http/core-http-server-mocks:build", - "//packages/core/http/core-http-server:build", + "//packages/core/i18n/core-i18n-browser:build", "//packages/core/i18n/core-i18n-browser-internal:build", "//packages/core/i18n/core-i18n-browser-mocks:build", - "//packages/core/i18n/core-i18n-browser:build", + "//packages/core/injected-metadata/core-injected-metadata-browser:build", "//packages/core/injected-metadata/core-injected-metadata-browser-internal:build", "//packages/core/injected-metadata/core-injected-metadata-browser-mocks:build", - "//packages/core/injected-metadata/core-injected-metadata-browser:build", "//packages/core/injected-metadata/core-injected-metadata-common-internal:build", "//packages/core/integrations/core-integrations-browser-internal:build", "//packages/core/integrations/core-integrations-browser-mocks:build", + "//packages/core/logging/core-logging-server:build", "//packages/core/logging/core-logging-server-internal:build", "//packages/core/logging/core-logging-server-mocks:build", - "//packages/core/logging/core-logging-server:build", "//packages/core/metrics/core-metrics-collectors-server-internal:build", "//packages/core/metrics/core-metrics-collectors-server-mocks:build", + "//packages/core/metrics/core-metrics-server:build", "//packages/core/metrics/core-metrics-server-internal:build", "//packages/core/metrics/core-metrics-server-mocks:build", - "//packages/core/metrics/core-metrics-server:build", - "//packages/core/mount-utils/core-mount-utils-browser-internal:build", "//packages/core/mount-utils/core-mount-utils-browser:build", + "//packages/core/mount-utils/core-mount-utils-browser-internal:build", + "//packages/core/node/core-node-server:build", "//packages/core/node/core-node-server-internal:build", "//packages/core/node/core-node-server-mocks:build", - "//packages/core/node/core-node-server:build", + "//packages/core/notifications/core-notifications-browser:build", "//packages/core/notifications/core-notifications-browser-internal:build", "//packages/core/notifications/core-notifications-browser-mocks:build", - "//packages/core/notifications/core-notifications-browser:build", + "//packages/core/overlays/core-overlays-browser:build", "//packages/core/overlays/core-overlays-browser-internal:build", "//packages/core/overlays/core-overlays-browser-mocks:build", - "//packages/core/overlays/core-overlays-browser:build", + "//packages/core/preboot/core-preboot-server:build", "//packages/core/preboot/core-preboot-server-internal:build", "//packages/core/preboot/core-preboot-server-mocks:build", - "//packages/core/preboot/core-preboot-server:build", "//packages/core/saved-objects/core-saved-objects-api-browser:build", "//packages/core/saved-objects/core-saved-objects-api-server:build", "//packages/core/saved-objects/core-saved-objects-base-server-internal:build", "//packages/core/saved-objects/core-saved-objects-base-server-mocks:build", + "//packages/core/saved-objects/core-saved-objects-browser:build", "//packages/core/saved-objects/core-saved-objects-browser-internal:build", "//packages/core/saved-objects/core-saved-objects-browser-mocks:build", - "//packages/core/saved-objects/core-saved-objects-browser:build", "//packages/core/saved-objects/core-saved-objects-common:build", "//packages/core/saved-objects/core-saved-objects-server:build", "//packages/core/saved-objects/core-saved-objects-utils-server:build", "//packages/core/test-helpers/core-test-helpers-deprecations-getters:build", "//packages/core/test-helpers/core-test-helpers-http-setup-browser:build", + "//packages/core/theme/core-theme-browser:build", "//packages/core/theme/core-theme-browser-internal:build", "//packages/core/theme/core-theme-browser-mocks:build", - "//packages/core/theme/core-theme-browser:build", + "//packages/core/ui-settings/core-ui-settings-browser:build", "//packages/core/ui-settings/core-ui-settings-browser-internal:build", "//packages/core/ui-settings/core-ui-settings-browser-mocks:build", - "//packages/core/ui-settings/core-ui-settings-browser:build", "//packages/core/ui-settings/core-ui-settings-common:build", "//packages/home/sample_data_card:build", "//packages/home/sample_data_tab:build", @@ -141,11 +141,11 @@ filegroup( "//packages/kbn-ci-stats-reporter:build", "//packages/kbn-cli-dev-mode:build", "//packages/kbn-coloring:build", + "//packages/kbn-config:build", "//packages/kbn-config-mocks:build", "//packages/kbn-config-schema:build", - "//packages/kbn-config:build", - "//packages/kbn-crypto-browser:build", "//packages/kbn-crypto:build", + "//packages/kbn-crypto-browser:build", "//packages/kbn-datemath:build", "//packages/kbn-dev-cli-errors:build", "//packages/kbn-dev-cli-runner:build", @@ -154,10 +154,10 @@ filegroup( "//packages/kbn-doc-links:build", "//packages/kbn-docs-utils:build", "//packages/kbn-ebt-tools:build", + "//packages/kbn-es:build", "//packages/kbn-es-archiver:build", "//packages/kbn-es-errors:build", "//packages/kbn-es-query:build", - "//packages/kbn-es:build", "//packages/kbn-eslint-config:build", "//packages/kbn-eslint-plugin-disable:build", "//packages/kbn-eslint-plugin-eslint:build", @@ -170,8 +170,8 @@ filegroup( "//packages/kbn-get-repo-files:build", "//packages/kbn-handlebars:build", "//packages/kbn-hapi-mocks:build", - "//packages/kbn-i18n-react:build", "//packages/kbn-i18n:build", + "//packages/kbn-i18n-react:build", "//packages/kbn-import-resolver:build", "//packages/kbn-interpreter:build", "//packages/kbn-io-ts-utils:build", @@ -179,21 +179,21 @@ filegroup( "//packages/kbn-jsonc:build", "//packages/kbn-kibana-manifest-parser:build", "//packages/kbn-kibana-manifest-schema:build", - "//packages/kbn-logging-mocks:build", "//packages/kbn-logging:build", - "//packages/kbn-managed-vscode-config-cli:build", + "//packages/kbn-logging-mocks:build", "//packages/kbn-managed-vscode-config:build", + "//packages/kbn-managed-vscode-config-cli:build", "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", - "//packages/kbn-optimizer-webpack-helpers:build", "//packages/kbn-optimizer:build", + "//packages/kbn-optimizer-webpack-helpers:build", "//packages/kbn-performance-testing-dataset-extractor:build", "//packages/kbn-plugin-discovery:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-plugin-helpers:build", "//packages/kbn-react-field:build", - "//packages/kbn-repo-source-classifier-cli:build", "//packages/kbn-repo-source-classifier:build", + "//packages/kbn-repo-source-classifier-cli:build", "//packages/kbn-rule-data-utils:build", "//packages/kbn-safer-lodash-set:build", "//packages/kbn-securitysolution-autocomplete:build", @@ -222,23 +222,23 @@ filegroup( "//packages/kbn-storybook:build", "//packages/kbn-synthetic-package-map:build", "//packages/kbn-telemetry-tools:build", + "//packages/kbn-test:build", "//packages/kbn-test-jest-helpers:build", "//packages/kbn-test-subj-selector:build", - "//packages/kbn-test:build", "//packages/kbn-timelion-grammar:build", "//packages/kbn-tinymath:build", "//packages/kbn-tooling-log:build", + "//packages/kbn-type-summarizer:build", "//packages/kbn-type-summarizer-cli:build", "//packages/kbn-type-summarizer-core:build", - "//packages/kbn-type-summarizer:build", "//packages/kbn-typed-react-router-config:build", "//packages/kbn-ui-framework:build", "//packages/kbn-ui-shared-deps-npm:build", "//packages/kbn-ui-shared-deps-src:build", "//packages/kbn-ui-theme:build", "//packages/kbn-user-profile-components:build", - "//packages/kbn-utility-types-jest:build", "//packages/kbn-utility-types:build", + "//packages/kbn-utility-types-jest:build", "//packages/kbn-utils:build", "//packages/kbn-yarn-lock-validator:build", "//packages/shared-ux/avatar/solution:build", @@ -290,110 +290,110 @@ filegroup( "//packages/analytics/shippers/elastic_v3/common:build_types", "//packages/analytics/shippers/elastic_v3/server:build_types", "//packages/analytics/shippers/fullstory:build_types", + "//packages/core/analytics/core-analytics-browser:build_types", "//packages/core/analytics/core-analytics-browser-internal:build_types", "//packages/core/analytics/core-analytics-browser-mocks:build_types", - "//packages/core/analytics/core-analytics-browser:build_types", + "//packages/core/analytics/core-analytics-server:build_types", "//packages/core/analytics/core-analytics-server-internal:build_types", "//packages/core/analytics/core-analytics-server-mocks:build_types", - "//packages/core/analytics/core-analytics-server:build_types", "//packages/core/base/core-base-browser-internal:build_types", "//packages/core/base/core-base-browser-mocks:build_types", - "//packages/core/base/core-base-common-internal:build_types", "//packages/core/base/core-base-common:build_types", + "//packages/core/base/core-base-common-internal:build_types", "//packages/core/base/core-base-server-internal:build_types", "//packages/core/base/core-base-server-mocks:build_types", "//packages/core/capabilities/core-capabilities-common:build_types", + "//packages/core/capabilities/core-capabilities-server:build_types", "//packages/core/capabilities/core-capabilities-server-internal:build_types", "//packages/core/capabilities/core-capabilities-server-mocks:build_types", - "//packages/core/capabilities/core-capabilities-server:build_types", "//packages/core/config/core-config-server-internal:build_types", + "//packages/core/deprecations/core-deprecations-browser:build_types", "//packages/core/deprecations/core-deprecations-browser-internal:build_types", "//packages/core/deprecations/core-deprecations-browser-mocks:build_types", - "//packages/core/deprecations/core-deprecations-browser:build_types", "//packages/core/deprecations/core-deprecations-common:build_types", + "//packages/core/doc-links/core-doc-links-browser:build_types", "//packages/core/doc-links/core-doc-links-browser-internal:build_types", "//packages/core/doc-links/core-doc-links-browser-mocks:build_types", - "//packages/core/doc-links/core-doc-links-browser:build_types", + "//packages/core/doc-links/core-doc-links-server:build_types", "//packages/core/doc-links/core-doc-links-server-internal:build_types", "//packages/core/doc-links/core-doc-links-server-mocks:build_types", - "//packages/core/doc-links/core-doc-links-server:build_types", "//packages/core/elasticsearch/core-elasticsearch-client-server-internal:build_types", "//packages/core/elasticsearch/core-elasticsearch-client-server-mocks:build_types", + "//packages/core/elasticsearch/core-elasticsearch-server:build_types", "//packages/core/elasticsearch/core-elasticsearch-server-internal:build_types", "//packages/core/elasticsearch/core-elasticsearch-server-mocks:build_types", - "//packages/core/elasticsearch/core-elasticsearch-server:build_types", "//packages/core/environment/core-environment-server-internal:build_types", "//packages/core/environment/core-environment-server-mocks:build_types", + "//packages/core/execution-context/core-execution-context-browser:build_types", "//packages/core/execution-context/core-execution-context-browser-internal:build_types", "//packages/core/execution-context/core-execution-context-browser-mocks:build_types", - "//packages/core/execution-context/core-execution-context-browser:build_types", "//packages/core/execution-context/core-execution-context-common:build_types", + "//packages/core/execution-context/core-execution-context-server:build_types", "//packages/core/execution-context/core-execution-context-server-internal:build_types", "//packages/core/execution-context/core-execution-context-server-mocks:build_types", - "//packages/core/execution-context/core-execution-context-server:build_types", + "//packages/core/fatal-errors/core-fatal-errors-browser:build_types", "//packages/core/fatal-errors/core-fatal-errors-browser-internal:build_types", "//packages/core/fatal-errors/core-fatal-errors-browser-mocks:build_types", - "//packages/core/fatal-errors/core-fatal-errors-browser:build_types", + "//packages/core/http/core-http-browser:build_types", "//packages/core/http/core-http-browser-internal:build_types", "//packages/core/http/core-http-browser-mocks:build_types", - "//packages/core/http/core-http-browser:build_types", "//packages/core/http/core-http-common:build_types", "//packages/core/http/core-http-context-server-internal:build_types", "//packages/core/http/core-http-context-server-mocks:build_types", "//packages/core/http/core-http-router-server-internal:build_types", "//packages/core/http/core-http-router-server-mocks:build_types", + "//packages/core/http/core-http-server:build_types", "//packages/core/http/core-http-server-internal:build_types", "//packages/core/http/core-http-server-mocks:build_types", - "//packages/core/http/core-http-server:build_types", + "//packages/core/i18n/core-i18n-browser:build_types", "//packages/core/i18n/core-i18n-browser-internal:build_types", "//packages/core/i18n/core-i18n-browser-mocks:build_types", - "//packages/core/i18n/core-i18n-browser:build_types", + "//packages/core/injected-metadata/core-injected-metadata-browser:build_types", "//packages/core/injected-metadata/core-injected-metadata-browser-internal:build_types", "//packages/core/injected-metadata/core-injected-metadata-browser-mocks:build_types", - "//packages/core/injected-metadata/core-injected-metadata-browser:build_types", "//packages/core/injected-metadata/core-injected-metadata-common-internal:build_types", "//packages/core/integrations/core-integrations-browser-internal:build_types", "//packages/core/integrations/core-integrations-browser-mocks:build_types", + "//packages/core/logging/core-logging-server:build_types", "//packages/core/logging/core-logging-server-internal:build_types", "//packages/core/logging/core-logging-server-mocks:build_types", - "//packages/core/logging/core-logging-server:build_types", "//packages/core/metrics/core-metrics-collectors-server-internal:build_types", "//packages/core/metrics/core-metrics-collectors-server-mocks:build_types", + "//packages/core/metrics/core-metrics-server:build_types", "//packages/core/metrics/core-metrics-server-internal:build_types", "//packages/core/metrics/core-metrics-server-mocks:build_types", - "//packages/core/metrics/core-metrics-server:build_types", - "//packages/core/mount-utils/core-mount-utils-browser-internal:build_types", "//packages/core/mount-utils/core-mount-utils-browser:build_types", + "//packages/core/mount-utils/core-mount-utils-browser-internal:build_types", + "//packages/core/node/core-node-server:build_types", "//packages/core/node/core-node-server-internal:build_types", "//packages/core/node/core-node-server-mocks:build_types", - "//packages/core/node/core-node-server:build_types", + "//packages/core/notifications/core-notifications-browser:build_types", "//packages/core/notifications/core-notifications-browser-internal:build_types", "//packages/core/notifications/core-notifications-browser-mocks:build_types", - "//packages/core/notifications/core-notifications-browser:build_types", + "//packages/core/overlays/core-overlays-browser:build_types", "//packages/core/overlays/core-overlays-browser-internal:build_types", "//packages/core/overlays/core-overlays-browser-mocks:build_types", - "//packages/core/overlays/core-overlays-browser:build_types", + "//packages/core/preboot/core-preboot-server:build_types", "//packages/core/preboot/core-preboot-server-internal:build_types", "//packages/core/preboot/core-preboot-server-mocks:build_types", - "//packages/core/preboot/core-preboot-server:build_types", "//packages/core/saved-objects/core-saved-objects-api-browser:build_types", "//packages/core/saved-objects/core-saved-objects-api-server:build_types", "//packages/core/saved-objects/core-saved-objects-base-server-internal:build_types", "//packages/core/saved-objects/core-saved-objects-base-server-mocks:build_types", + "//packages/core/saved-objects/core-saved-objects-browser:build_types", "//packages/core/saved-objects/core-saved-objects-browser-internal:build_types", "//packages/core/saved-objects/core-saved-objects-browser-mocks:build_types", - "//packages/core/saved-objects/core-saved-objects-browser:build_types", "//packages/core/saved-objects/core-saved-objects-common:build_types", "//packages/core/saved-objects/core-saved-objects-server:build_types", "//packages/core/saved-objects/core-saved-objects-utils-server:build_types", "//packages/core/test-helpers/core-test-helpers-deprecations-getters:build_types", "//packages/core/test-helpers/core-test-helpers-http-setup-browser:build_types", + "//packages/core/theme/core-theme-browser:build_types", "//packages/core/theme/core-theme-browser-internal:build_types", "//packages/core/theme/core-theme-browser-mocks:build_types", - "//packages/core/theme/core-theme-browser:build_types", + "//packages/core/ui-settings/core-ui-settings-browser:build_types", "//packages/core/ui-settings/core-ui-settings-browser-internal:build_types", "//packages/core/ui-settings/core-ui-settings-browser-mocks:build_types", - "//packages/core/ui-settings/core-ui-settings-browser:build_types", "//packages/core/ui-settings/core-ui-settings-common:build_types", "//packages/home/sample_data_card:build_types", "//packages/home/sample_data_tab:build_types", @@ -412,11 +412,11 @@ filegroup( "//packages/kbn-ci-stats-reporter:build_types", "//packages/kbn-cli-dev-mode:build_types", "//packages/kbn-coloring:build_types", + "//packages/kbn-config:build_types", "//packages/kbn-config-mocks:build_types", "//packages/kbn-config-schema:build_types", - "//packages/kbn-config:build_types", - "//packages/kbn-crypto-browser:build_types", "//packages/kbn-crypto:build_types", + "//packages/kbn-crypto-browser:build_types", "//packages/kbn-datemath:build_types", "//packages/kbn-dev-cli-errors:build_types", "//packages/kbn-dev-cli-runner:build_types", @@ -436,8 +436,8 @@ filegroup( "//packages/kbn-get-repo-files:build_types", "//packages/kbn-handlebars:build_types", "//packages/kbn-hapi-mocks:build_types", - "//packages/kbn-i18n-react:build_types", "//packages/kbn-i18n:build_types", + "//packages/kbn-i18n-react:build_types", "//packages/kbn-import-resolver:build_types", "//packages/kbn-interpreter:build_types", "//packages/kbn-io-ts-utils:build_types", @@ -445,21 +445,21 @@ filegroup( "//packages/kbn-jsonc:build_types", "//packages/kbn-kibana-manifest-parser:build_types", "//packages/kbn-kibana-manifest-schema:build_types", - "//packages/kbn-logging-mocks:build_types", "//packages/kbn-logging:build_types", - "//packages/kbn-managed-vscode-config-cli:build_types", + "//packages/kbn-logging-mocks:build_types", "//packages/kbn-managed-vscode-config:build_types", + "//packages/kbn-managed-vscode-config-cli:build_types", "//packages/kbn-mapbox-gl:build_types", "//packages/kbn-monaco:build_types", - "//packages/kbn-optimizer-webpack-helpers:build_types", "//packages/kbn-optimizer:build_types", + "//packages/kbn-optimizer-webpack-helpers:build_types", "//packages/kbn-performance-testing-dataset-extractor:build_types", "//packages/kbn-plugin-discovery:build_types", "//packages/kbn-plugin-generator:build_types", "//packages/kbn-plugin-helpers:build_types", "//packages/kbn-react-field:build_types", - "//packages/kbn-repo-source-classifier-cli:build_types", "//packages/kbn-repo-source-classifier:build_types", + "//packages/kbn-repo-source-classifier-cli:build_types", "//packages/kbn-rule-data-utils:build_types", "//packages/kbn-safer-lodash-set:build_types", "//packages/kbn-securitysolution-autocomplete:build_types", @@ -486,19 +486,19 @@ filegroup( "//packages/kbn-stdio-dev-helpers:build_types", "//packages/kbn-storybook:build_types", "//packages/kbn-telemetry-tools:build_types", - "//packages/kbn-test-jest-helpers:build_types", "//packages/kbn-test:build_types", + "//packages/kbn-test-jest-helpers:build_types", "//packages/kbn-tooling-log:build_types", + "//packages/kbn-type-summarizer:build_types", "//packages/kbn-type-summarizer-cli:build_types", "//packages/kbn-type-summarizer-core:build_types", - "//packages/kbn-type-summarizer:build_types", "//packages/kbn-typed-react-router-config:build_types", "//packages/kbn-ui-shared-deps-npm:build_types", "//packages/kbn-ui-shared-deps-src:build_types", "//packages/kbn-ui-theme:build_types", "//packages/kbn-user-profile-components:build_types", - "//packages/kbn-utility-types-jest:build_types", "//packages/kbn-utility-types:build_types", + "//packages/kbn-utility-types-jest:build_types", "//packages/kbn-utils:build_types", "//packages/kbn-yarn-lock-validator:build_types", "//packages/shared-ux/avatar/solution:build_types", diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/client/apm_synthtrace_kibana_client.ts b/packages/kbn-apm-synthtrace/src/lib/apm/client/apm_synthtrace_kibana_client.ts index bf068d7f3318570..7bd2443031c8009 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/client/apm_synthtrace_kibana_client.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/client/apm_synthtrace_kibana_client.ts @@ -54,9 +54,8 @@ export class ApmSynthtraceKibanaClient { }); } async fetchLatestApmPackageVersion(currentKibanaVersion: string) { - const url = - 'https://epr-snapshot.elastic.co/search?package=apm&prerelease=true&all=true&kibana.version='; - const response = await fetch(url + currentKibanaVersion, { method: 'GET' }); + const url = `https://epr-snapshot.elastic.co/search?package=apm&prerelease=true&all=true&kibana.version=${currentKibanaVersion}`; + const response = await fetch(url, { method: 'GET' }); const json = (await response.json()) as Array<{ version: string }>; const packageVersions = (json ?? []).map((item) => item.version).sort(Semver.rcompare); const validPackageVersions = packageVersions.filter((v) => Semver.valid(v)); @@ -71,7 +70,7 @@ export class ApmSynthtraceKibanaClient { async installApmPackage(kibanaUrl: string, version: string, username: string, password: string) { const packageVersion = await this.fetchLatestApmPackageVersion(version); - const response = await fetch(kibanaUrl + '/api/fleet/epm/packages/apm/' + packageVersion, { + const response = await fetch(`${kibanaUrl}/api/fleet/epm/packages/apm/${packageVersion}`, { method: 'POST', headers: { Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/index.ts b/packages/kbn-apm-synthtrace/src/lib/apm/index.ts index fcb8e078bf02abf..a136daabee8f2f6 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/index.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/index.ts @@ -13,6 +13,7 @@ import { getChromeUserAgentDefaults } from './defaults/get_chrome_user_agent_def import { getBreakdownMetrics } from './processors/get_breakdown_metrics'; import { getApmWriteTargets } from './utils/get_apm_write_targets'; import { ApmSynthtraceEsClient } from './client/apm_synthtrace_es_client'; +import { ApmSynthtraceKibanaClient } from './client/apm_synthtrace_kibana_client'; import type { ApmException } from './apm_fields'; @@ -25,6 +26,7 @@ export const apm = { getBreakdownMetrics, getApmWriteTargets, ApmSynthtraceEsClient, + ApmSynthtraceKibanaClient, }; export type { ApmSynthtraceEsClient, ApmException }; diff --git a/packages/kbn-bazel-packages/BUILD.bazel b/packages/kbn-bazel-packages/BUILD.bazel index e3a3545be167d61..9c7a793459623ed 100644 --- a/packages/kbn-bazel-packages/BUILD.bazel +++ b/packages/kbn-bazel-packages/BUILD.bazel @@ -7,6 +7,7 @@ PKG_REQUIRE_NAME = "@kbn/bazel-packages" SOURCE_FILES = glob( [ + "src/**/*.js", "src/**/*.ts", ], exclude = [ @@ -37,11 +38,6 @@ NPM_MODULE_EXTRA_FILES = [ # "@npm//name-of-package" # eg. "@npm//lodash" RUNTIME_DEPS = [ - "//packages/kbn-utils", - "//packages/kbn-std", - "//packages/kbn-synthetic-package-map", - "@npm//globby", - "@npm//normalize-path", ] # In this array place dependencies necessary to build the types, which will include the @@ -55,13 +51,8 @@ RUNTIME_DEPS = [ # References to NPM packages work the same as RUNTIME_DEPS: # eg. "@npm//@types/babel__core" TYPES_DEPS = [ - "//packages/kbn-utils:npm_module_types", - "//packages/kbn-std:npm_module_types", - "//packages/kbn-synthetic-package-map:npm_module_types", "@npm//@types/node", - "@npm//@types/normalize-path", - "@npm//globby", - "@npm//normalize-path", + "@npm//@types/jest", ] jsts_transpiler( @@ -87,6 +78,7 @@ ts_project( deps = TYPES_DEPS, declaration = True, declaration_map = True, + allow_js = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", diff --git a/packages/kbn-bazel-packages/src/async.js b/packages/kbn-bazel-packages/src/async.js new file mode 100644 index 000000000000000..38b535447953f38 --- /dev/null +++ b/packages/kbn-bazel-packages/src/async.js @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the 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. + */ + +/** + * + * @template T + * @template T2 + * @param {(v: T) => Promise} fn + * @param {T} item + * @returns {Promise>} + */ +const settle = async (fn, item) => { + const [result] = await Promise.allSettled([(async () => fn(item))()]); + return result; +}; + +/** + * @template T + * @template T2 + * @param {Array} source + * @param {number} limit + * @param {(v: T) => Promise} mapFn + * @returns {Promise} + */ +function asyncMapWithLimit(source, limit, mapFn) { + return new Promise((resolve, reject) => { + if (limit < 1) { + reject(new Error('invalid limit, must be greater than 0')); + return; + } + + let failed = false; + let inProgress = 0; + const queue = [...source.entries()]; + + /** @type {T2[]} */ + const results = new Array(source.length); + + /** + * this is run for each item, manages the inProgress state, + * calls the mapFn with that item, writes the map result to + * the result array, and calls runMore() after each item + * completes to either start another item or resolve the + * returned promise. + * + * @param {number} index + * @param {T} item + */ + function run(index, item) { + inProgress += 1; + settle(mapFn, item).then((result) => { + inProgress -= 1; + + if (failed) { + return; + } + + if (result.status === 'fulfilled') { + results[index] = result.value; + runMore(); + return; + } + + // when an error occurs we update the state to prevent + // holding onto old results and ignore future results + // from in-progress promises + failed = true; + results.length = 0; + reject(result.reason); + }); + } + + /** + * If there is work in the queue, schedule it, if there isn't + * any work to be scheduled and there isn't anything in progress + * then we're done. This function is called every time a mapFn + * promise resolves and once after initialization + */ + function runMore() { + if (!queue.length) { + if (inProgress === 0) { + resolve(results); + } + + return; + } + + while (inProgress < limit) { + const entry = queue.shift(); + if (!entry) { + break; + } + + run(...entry); + } + } + + runMore(); + }); +} + +module.exports = { asyncMapWithLimit }; diff --git a/packages/kbn-bazel-packages/src/async.test.ts b/packages/kbn-bazel-packages/src/async.test.ts new file mode 100644 index 000000000000000..b238783f74fb856 --- /dev/null +++ b/packages/kbn-bazel-packages/src/async.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { setTimeout } from 'timers/promises'; +import { asyncMapWithLimit } from './async'; + +const NUMS = [1, 2, 3, 4]; +const ident = jest.fn(async function ident(x: T) { + return await x; +}); +const double = jest.fn(async function double(x: number) { + return x * 2; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('resolves with an empty array', async () => { + const result = await asyncMapWithLimit([], 10, ident); + expect(ident).not.toHaveBeenCalled(); + expect(result).toEqual([]); +}); + +it('resolves with a mapped array', async () => { + const result = await asyncMapWithLimit(NUMS, 10, double); + expect(double).toHaveBeenCalledTimes(NUMS.length); + expect(result.join(',')).toMatchInlineSnapshot(`"2,4,6,8"`); +}); + +it('rejects when limit it not >= 1', async () => { + await expect(() => asyncMapWithLimit([], -1, ident)).rejects.toMatchInlineSnapshot( + `[Error: invalid limit, must be greater than 0]` + ); + await expect(() => asyncMapWithLimit([], 0, ident)).rejects.toMatchInlineSnapshot( + `[Error: invalid limit, must be greater than 0]` + ); + await expect(() => asyncMapWithLimit([], -Infinity, ident)).rejects.toMatchInlineSnapshot( + `[Error: invalid limit, must be greater than 0]` + ); +}); + +it('rejects with the first error produced and stops calling mapFn', async () => { + const map = jest.fn(async (num) => { + if (num % 2 === 0) { + throw new Error('even numbers are not supported'); + } + return num * 2; + }); + + await expect(() => asyncMapWithLimit(NUMS, 1, map)).rejects.toMatchInlineSnapshot( + `[Error: even numbers are not supported]` + ); + await setTimeout(10); + expect(map).toHaveBeenCalledTimes(2); +}); diff --git a/packages/kbn-bazel-packages/src/bazel_package.ts b/packages/kbn-bazel-packages/src/bazel_package.js similarity index 67% rename from packages/kbn-bazel-packages/src/bazel_package.ts rename to packages/kbn-bazel-packages/src/bazel_package.js index 91e411770175bdf..c6a31ccc0dbc1f9 100644 --- a/packages/kbn-bazel-packages/src/bazel_package.ts +++ b/packages/kbn-bazel-packages/src/bazel_package.js @@ -6,27 +6,31 @@ * Side Public License, v 1. */ -import { inspect } from 'util'; -import Path from 'path'; -import Fsp from 'fs/promises'; +const { inspect } = require('util'); +const Path = require('path'); +const Fsp = require('fs/promises'); -import normalizePath from 'normalize-path'; -import { REPO_ROOT } from '@kbn/utils'; - -import { readPackageJson, ParsedPackageJson } from './parse_package_json'; +/** @typedef {import('./types').ParsedPackageJson} ParsedPackageJson */ +const { readPackageJson } = require('./parse_package_json'); const BUILD_RULE_NAME = /(^|\s)name\s*=\s*"build"/; const BUILD_TYPES_RULE_NAME = /(^|\s)name\s*=\s*"build_types"/; /** * Representation of a Bazel Package in the Kibana repository + * @class + * @property {string} normalizedRepoRelativeDir + * @property {import('./types').ParsedPackageJson} pkg + * @property {string | undefined} buildBazelContent */ -export class BazelPackage { +class BazelPackage { /** * Create a BazelPackage object from a package directory. Reads some files from the package and returns * a Promise for a BazelPackage instance. + * @param {string} repoRoot + * @param {string} dir */ - static async fromDir(dir: string) { + static async fromDir(repoRoot, dir) { const pkg = readPackageJson(Path.resolve(dir, 'package.json')); let buildBazelContent; @@ -36,23 +40,30 @@ export class BazelPackage { throw new Error(`unable to read BUILD.bazel file in [${dir}]: ${error.message}`); } - return new BazelPackage(normalizePath(Path.relative(REPO_ROOT, dir)), pkg, buildBazelContent); + return new BazelPackage(Path.relative(repoRoot, dir), pkg, buildBazelContent); } constructor( /** * Relative path from the root of the repository to the package directory + * @type {string} */ - public readonly normalizedRepoRelativeDir: string, + normalizedRepoRelativeDir, /** * Parsed package.json file from the package + * @type {import('./types').ParsedPackageJson} */ - public readonly pkg: ParsedPackageJson, + pkg, /** * Content of the BUILD.bazel file + * @type {string | undefined} */ - private readonly buildBazelContent?: string - ) {} + buildBazelContent = undefined + ) { + this.normalizedRepoRelativeDir = normalizedRepoRelativeDir; + this.pkg = pkg; + this.buildBazelContent = buildBazelContent; + } /** * Returns true if the package includes a `:build` bazel rule @@ -83,3 +94,7 @@ export class BazelPackage { return `BazelPackage<${this.normalizedRepoRelativeDir}>`; } } + +module.exports = { + BazelPackage, +}; diff --git a/packages/kbn-bazel-packages/src/bazel_package_dirs.ts b/packages/kbn-bazel-packages/src/bazel_package_dirs.js similarity index 68% rename from packages/kbn-bazel-packages/src/bazel_package_dirs.ts rename to packages/kbn-bazel-packages/src/bazel_package_dirs.js index 755ef614c045619..7e3f728a21ca72e 100644 --- a/packages/kbn-bazel-packages/src/bazel_package_dirs.ts +++ b/packages/kbn-bazel-packages/src/bazel_package_dirs.js @@ -6,10 +6,7 @@ * Side Public License, v 1. */ -import globby from 'globby'; -import Path from 'path'; - -import { REPO_ROOT } from '@kbn/utils'; +const { expandWildcards } = require('./find_files'); /** * This is a list of repo-relative paths to directories containing packages. Do not @@ -19,7 +16,7 @@ import { REPO_ROOT } from '@kbn/utils'; * eg. src/vis_editors => would find a package at src/vis_editors/foo/package.json * src/vis_editors/* => would find a package at src/vis_editors/foo/bar/package.json */ -export const BAZEL_PACKAGE_DIRS = [ +const BAZEL_PACKAGE_DIRS = [ 'packages', 'packages/shared-ux', 'packages/shared-ux/*', @@ -34,18 +31,13 @@ export const BAZEL_PACKAGE_DIRS = [ /** * Resolve all the BAZEL_PACKAGE_DIRS to absolute paths + * @param {string} repoRoot */ -export function getAllBazelPackageDirs() { - return globby.sync(BAZEL_PACKAGE_DIRS, { - cwd: REPO_ROOT, - onlyDirectories: true, - expandDirectories: false, - }); +function getAllBazelPackageDirs(repoRoot) { + return expandWildcards(repoRoot, BAZEL_PACKAGE_DIRS); } -/** - * Resolve all the BAZEL_PACKAGE_DIRS to repo-relative paths - */ -export function getAllRepoRelativeBazelPackageDirs() { - return getAllBazelPackageDirs().map((path) => Path.relative(REPO_ROOT, path)); -} +module.exports = { + BAZEL_PACKAGE_DIRS, + getAllBazelPackageDirs, +}; diff --git a/packages/kbn-bazel-packages/src/discover_packages.js b/packages/kbn-bazel-packages/src/discover_packages.js new file mode 100644 index 000000000000000..17678115c74526b --- /dev/null +++ b/packages/kbn-bazel-packages/src/discover_packages.js @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const Path = require('path'); + +const { BazelPackage } = require('./bazel_package'); +const { getAllBazelPackageDirs } = require('./bazel_package_dirs'); +const { findPackages } = require('./find_files'); +const { asyncMapWithLimit } = require('./async'); + +/** + * @param {string} repoRoot + */ +function discoverBazelPackageLocations(repoRoot) { + const packagesWithBuildBazel = getAllBazelPackageDirs(repoRoot) + .flatMap((packageDir) => findPackages(packageDir, 'BUILD.bazel')) + .map((path) => Path.dirname(path)); + + // NOTE: only return as discovered packages with a package.json + BUILD.bazel file. + // In the future we should change this to only discover the ones with kibana.jsonc. + return getAllBazelPackageDirs(repoRoot) + .flatMap((packageDir) => findPackages(packageDir, 'package.json')) + .map((path) => Path.dirname(path)) + .filter((pkg) => packagesWithBuildBazel.includes(pkg)) + .sort((a, b) => a.localeCompare(b)); +} + +/** + * @param {string} repoRoot + */ +async function discoverBazelPackages(repoRoot) { + return await asyncMapWithLimit( + discoverBazelPackageLocations(repoRoot), + 100, + async (dir) => await BazelPackage.fromDir(repoRoot, dir) + ); +} + +module.exports = { discoverBazelPackageLocations, discoverBazelPackages }; diff --git a/packages/kbn-bazel-packages/src/discover_packages.ts b/packages/kbn-bazel-packages/src/discover_packages.ts deleted file mode 100644 index 8b78e4e29311886..000000000000000 --- a/packages/kbn-bazel-packages/src/discover_packages.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Path from 'path'; - -import globby from 'globby'; -import normalizePath from 'normalize-path'; -import { REPO_ROOT } from '@kbn/utils'; -import { asyncMapWithLimit } from '@kbn/std'; - -import { BazelPackage } from './bazel_package'; -import { BAZEL_PACKAGE_DIRS } from './bazel_package_dirs'; - -export function discoverBazelPackageLocations(repoRoot: string) { - const packagesWithPackageJson = globby - .sync( - BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/package.json`), - { - cwd: repoRoot, - absolute: true, - } - ) - // NOTE: removing x-pack by default for now to prevent a situation where a BUILD.bazel file - // needs to be added at the root of the folder which will make x-pack to be wrongly recognized - // as a Bazel package in that case - .filter((path) => !normalizePath(path).includes('x-pack/package.json')) - .sort((a, b) => a.localeCompare(b)) - .map((path) => Path.dirname(path)); - - const packagesWithBuildBazel = globby - .sync( - BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/BUILD.bazel`), - { - cwd: repoRoot, - absolute: true, - } - ) - .map((path) => Path.dirname(path)); - - // NOTE: only return as discovered packages the ones with a package.json + BUILD.bazel file. - // In the future we should change this to only discover the ones declaring kibana.json. - return packagesWithPackageJson.filter((pkg) => packagesWithBuildBazel.includes(pkg)); -} - -export async function discoverBazelPackages(repoRoot: string = REPO_ROOT) { - return await asyncMapWithLimit( - discoverBazelPackageLocations(repoRoot), - 100, - async (dir) => await BazelPackage.fromDir(dir) - ); -} diff --git a/packages/kbn-bazel-packages/src/find_files.js b/packages/kbn-bazel-packages/src/find_files.js new file mode 100644 index 000000000000000..9be6c2e25607905 --- /dev/null +++ b/packages/kbn-bazel-packages/src/find_files.js @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const Path = require('path'); +const Fs = require('fs'); + +/** + * @param {string} path + */ +function safeIsFile(path) { + try { + return Fs.statSync(path).isFile(); + } catch (error) { + if (error.code === 'ENOENT') { + return false; + } + + throw error; + } +} + +/** + * @param {string} path + */ +function safeReadDir(path) { + try { + return Fs.readdirSync(path, { + withFileTypes: true, + }); + } catch (error) { + if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { + return []; + } + + throw error; + } +} + +/** + * Search for `name` files in directories within `packageDir` + * + * @param {string} packageDir + * @param {string} name + * @returns {string[]} + */ +function findPackages(packageDir, name) { + return ( + // get the directories within the "package dir" + safeReadDir(packageDir) + // if this directory has a file with the name, it's a match + .flatMap((e) => { + if (!e.isDirectory()) { + return []; + } + + const path = Path.resolve(packageDir, e.name, name); + return safeIsFile(path) ? path : []; + }) + ); +} + +/** + * Expand simple `*` wildcards in patterns, which are otherwise expected to be + * paths relative to `cwd`. + * + * @param {string} cwd + * @param {string[]} patterns + */ +function expandWildcards(cwd, patterns) { + /** @type {Set} */ + const results = new Set(); + + /** @type {Set} */ + const queue = new Set(patterns.map((p) => Path.resolve(cwd, p))); + + for (const pattern of queue) { + let length = 3; + let index = pattern.indexOf('/*/'); + if (index === -1 && pattern.endsWith('/*')) { + length = 2; + index = pattern.length - length; + } + + if (index === -1) { + results.add(pattern); + continue; + } + + const left = pattern.slice(0, index + 1); + const right = pattern.slice(index + length); + for (const ent of safeReadDir(left)) { + if (!ent.isDirectory()) { + continue; + } + + const path = Path.resolve(left, ent.name); + + if (right) { + queue.add(Path.resolve(path, right)); + } else { + results.add(path); + } + } + } + + return [...results]; +} + +module.exports = { findPackages, expandWildcards }; diff --git a/packages/kbn-bazel-packages/src/index.js b/packages/kbn-bazel-packages/src/index.js new file mode 100644 index 000000000000000..254b8bb9da44c6c --- /dev/null +++ b/packages/kbn-bazel-packages/src/index.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** @typedef {import('./bazel_package').BazelPackage} BazelPackage */ + +const { BAZEL_PACKAGE_DIRS, getAllBazelPackageDirs } = require('./bazel_package_dirs'); +const { discoverBazelPackageLocations, discoverBazelPackages } = require('./discover_packages'); + +module.exports = { + BAZEL_PACKAGE_DIRS, + getAllBazelPackageDirs, + discoverBazelPackageLocations, + discoverBazelPackages, +}; diff --git a/packages/kbn-bazel-packages/src/index.ts b/packages/kbn-bazel-packages/src/index.ts deleted file mode 100644 index 55d5db9bb3adb21..000000000000000 --- a/packages/kbn-bazel-packages/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './bazel_package_dirs'; -export * from './discover_packages'; -export type { BazelPackage } from './bazel_package'; diff --git a/packages/kbn-bazel-packages/src/parse_package_json.ts b/packages/kbn-bazel-packages/src/parse_package_json.js similarity index 57% rename from packages/kbn-bazel-packages/src/parse_package_json.ts rename to packages/kbn-bazel-packages/src/parse_package_json.js index b060656c0c51256..27a78f91a446613 100644 --- a/packages/kbn-bazel-packages/src/parse_package_json.ts +++ b/packages/kbn-bazel-packages/src/parse_package_json.js @@ -6,40 +6,22 @@ * Side Public License, v 1. */ -import Fs from 'fs'; +const Fs = require('fs'); /** - * Simple parsed representation of a package.json file, validated - * by `assertParsedPackageJson()` and extensible as needed in the future + * @param {unknown} v + * @returns {v is Record} */ -export interface ParsedPackageJson { - /** The name of the package, usually `@kbn/`+something */ - name: string; - /** "dependenices" property from package.json */ - dependencies?: Record; - /** "devDependenices" property from package.json */ - devDependencies?: Record; - /** Some kibana specific properties about this package */ - kibana?: { - /** Is this package only intended for dev? */ - devOnly?: boolean; - }; - /** Scripts defined in the package.json file */ - scripts?: { - [key: string]: string | undefined; - }; - /** All other fields in the package.json are typed as unknown as we don't care what they are */ - [key: string]: unknown; -} - -function isObj(v: unknown): v is Record { +function isObj(v) { return !!(typeof v === 'object' && v); } /** * Asserts that given value looks like a parsed package.json file + * @param {unknown} v + * @returns {asserts v is import('./types').ParsedPackageJson} */ -export function assertParsedPackageJson(v: unknown): asserts v is ParsedPackageJson { +function validateParsedPackageJson(v) { if (!isObj(v) || typeof v.name !== 'string') { throw new Error('Expected at least a "name" property'); } @@ -66,14 +48,18 @@ export function assertParsedPackageJson(v: unknown): asserts v is ParsedPackageJ /** * Reads a given package.json file from disk and parses it + * @param {string} path + * @returns {import('./types').ParsedPackageJson} */ -export function readPackageJson(path: string): ParsedPackageJson { +function readPackageJson(path) { let pkg; try { pkg = JSON.parse(Fs.readFileSync(path, 'utf8')); - assertParsedPackageJson(pkg); + validateParsedPackageJson(pkg); } catch (error) { throw new Error(`unable to parse package.json at [${path}]: ${error.message}`); } return pkg; } + +module.exports = { readPackageJson, validateParsedPackageJson }; diff --git a/packages/kbn-bazel-packages/src/types.ts b/packages/kbn-bazel-packages/src/types.ts new file mode 100644 index 000000000000000..dc77d35bc206af9 --- /dev/null +++ b/packages/kbn-bazel-packages/src/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Simple parsed representation of a package.json file, validated + * by `assertParsedPackageJson()` and extensible as needed in the future + */ +export interface ParsedPackageJson { + /** The name of the package, usually `@kbn/`+something */ + name: string; + /** "dependenices" property from package.json */ + dependencies?: Record; + /** "devDependenices" property from package.json */ + devDependencies?: Record; + /** Some kibana specific properties about this package */ + kibana?: { + /** Is this package only intended for dev? */ + devOnly?: boolean; + }; + /** Scripts defined in the package.json file */ + scripts?: { + [key: string]: string | undefined; + }; + /** All other fields in the package.json are typed as unknown as we don't care what they are */ + [key: string]: unknown; +} diff --git a/packages/kbn-bazel-packages/tsconfig.json b/packages/kbn-bazel-packages/tsconfig.json index 789c6b3111115dd..9f78bc243ac66cb 100644 --- a/packages/kbn-bazel-packages/tsconfig.json +++ b/packages/kbn-bazel-packages/tsconfig.json @@ -4,6 +4,8 @@ "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, + "allowJs": true, + "checkJs": true, "outDir": "target_types", "rootDir": "src", "stripInternal": false, diff --git a/packages/kbn-generate/src/commands/package_command.ts b/packages/kbn-generate/src/commands/package_command.ts index 4f5d58184aeac73..32d5ff008c79f48 100644 --- a/packages/kbn-generate/src/commands/package_command.ts +++ b/packages/kbn-generate/src/commands/package_command.ts @@ -162,7 +162,7 @@ ${BAZEL_PACKAGE_DIRS.map((dir) => ` ./${dir}/*\n`).join Path.resolve(TEMPLATE_DIR, 'packages_BUILD.bazel.ejs'), Path.resolve(REPO_ROOT, 'packages/BUILD.bazel'), { - packages: await discoverBazelPackages(), + packages: await discoverBazelPackages(REPO_ROOT), } ); log.info('Updated packages/BUILD.bazel'); diff --git a/packages/kbn-generate/src/commands/packages_build_manifest_command.ts b/packages/kbn-generate/src/commands/packages_build_manifest_command.ts index 9103d56d42a1dbf..9b05c1aed332de4 100644 --- a/packages/kbn-generate/src/commands/packages_build_manifest_command.ts +++ b/packages/kbn-generate/src/commands/packages_build_manifest_command.ts @@ -31,7 +31,7 @@ export const PackagesBuildManifestCommand: GenerateCommand = { async run({ log, render, flags }) { const validate = !!flags.validate; - const packages = await discoverBazelPackages(); + const packages = await discoverBazelPackages(REPO_ROOT); const dest = Path.resolve(REPO_ROOT, 'packages/BUILD.bazel'); const relDest = Path.relative(process.cwd(), dest); diff --git a/src/dev/build/tasks/build_packages_task.ts b/src/dev/build/tasks/build_packages_task.ts index 607e0ac0f08bafd..521b1299f423d0d 100644 --- a/src/dev/build/tasks/build_packages_task.ts +++ b/src/dev/build/tasks/build_packages_task.ts @@ -8,6 +8,7 @@ import Path from 'path'; +import { REPO_ROOT } from '@kbn/utils'; import { discoverBazelPackages } from '@kbn/bazel-packages'; import { runBazel } from '@kbn/bazel-runner'; @@ -16,7 +17,7 @@ import { Task, scanCopy, write } from '../lib'; export const BuildBazelPackages: Task = { description: 'Building distributable versions of Bazel packages', async run(config, log, build) { - const packages = (await discoverBazelPackages()).filter((pkg) => !pkg.isDevOnly()); + const packages = (await discoverBazelPackages(REPO_ROOT)).filter((pkg) => !pkg.isDevOnly()); log.info(`Preparing Bazel projects production build non-devOnly packages`); await runBazel(['build', '//packages:build']); diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index 15b20e712334c40..409f6a77b50bfca 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -8,6 +8,7 @@ import minimatch from 'minimatch'; import { discoverBazelPackages } from '@kbn/bazel-packages'; +import { REPO_ROOT } from '@kbn/utils'; import { deleteAll, deleteEmptyFolders, scanDelete, Task, GlobalTask } from '../lib'; export const Clean: GlobalTask = { @@ -259,7 +260,7 @@ export const DeleteBazelPackagesFromBuildRoot: Task = { 'Deleting bazel packages outputs from build folder root as they are now installed as node_modules', async run(config, log, build) { - const bazelPackagesOnBuildRoot = (await discoverBazelPackages()).map((pkg) => + const bazelPackagesOnBuildRoot = (await discoverBazelPackages(REPO_ROOT)).map((pkg) => build.resolvePath(pkg.normalizedRepoRelativeDir) ); diff --git a/src/plugins/controls/common/control_types/time_slider/time_slider_persistable_state.ts b/src/plugins/controls/common/control_types/time_slider/time_slider_persistable_state.ts deleted file mode 100644 index b8b3a5f16b51f6e..000000000000000 --- a/src/plugins/controls/common/control_types/time_slider/time_slider_persistable_state.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - EmbeddableStateWithType, - EmbeddablePersistableStateService, -} from '@kbn/embeddable-plugin/common'; -import { SavedObjectReference } from '@kbn/core/types'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; -import { TimeSliderControlEmbeddableInput } from './types'; - -type TimeSliderInputWithType = Partial & { type: string }; -const dataViewReferenceName = 'timeSliderDataView'; - -export const createTimeSliderInject = (): EmbeddablePersistableStateService['inject'] => { - return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => { - const workingState = { ...state } as EmbeddableStateWithType | TimeSliderInputWithType; - references.forEach((reference) => { - if (reference.name === dataViewReferenceName) { - (workingState as TimeSliderInputWithType).dataViewId = reference.id; - } - }); - return workingState as EmbeddableStateWithType; - }; -}; - -export const createTimeSliderExtract = (): EmbeddablePersistableStateService['extract'] => { - return (state: EmbeddableStateWithType) => { - const workingState = { ...state } as EmbeddableStateWithType | TimeSliderInputWithType; - const references: SavedObjectReference[] = []; - - if ('dataViewId' in workingState) { - references.push({ - name: dataViewReferenceName, - type: DATA_VIEW_SAVED_OBJECT_TYPE, - id: workingState.dataViewId!, - }); - delete workingState.dataViewId; - } - return { state: workingState as EmbeddableStateWithType, references }; - }; -}; diff --git a/src/plugins/controls/common/control_types/time_slider/types.ts b/src/plugins/controls/common/control_types/time_slider/types.ts deleted file mode 100644 index 31272380becde90..000000000000000 --- a/src/plugins/controls/common/control_types/time_slider/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the 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 { DataControlInput } from '../../types'; - -export const TIME_SLIDER_CONTROL = 'timeSlider'; - -export interface TimeSliderControlEmbeddableInput extends DataControlInput { - value?: [number | null, number | null]; -} diff --git a/src/plugins/controls/common/index.ts b/src/plugins/controls/common/index.ts index c3361c29232bd95..a201553c09ec12a 100644 --- a/src/plugins/controls/common/index.ts +++ b/src/plugins/controls/common/index.ts @@ -36,5 +36,3 @@ export { // Control Type exports export { OPTIONS_LIST_CONTROL, type OptionsListEmbeddableInput } from './options_list/types'; export { type RangeSliderEmbeddableInput, RANGE_SLIDER_CONTROL } from './range_slider/types'; - -export { TIME_SLIDER_CONTROL } from './control_types/time_slider/types'; diff --git a/src/plugins/controls/public/__stories__/storybook_control_factories.ts b/src/plugins/controls/public/__stories__/storybook_control_factories.ts index 9b0b41f6393e735..2e27339a9eaf876 100644 --- a/src/plugins/controls/public/__stories__/storybook_control_factories.ts +++ b/src/plugins/controls/public/__stories__/storybook_control_factories.ts @@ -8,7 +8,6 @@ import { OptionsListEmbeddableFactory } from '../options_list'; import { RangeSliderEmbeddableFactory } from '../range_slider'; -import { TimesliderEmbeddableFactory } from '../control_types/time_slider'; import { ControlsService } from '../services/controls'; import { ControlFactory } from '..'; @@ -26,9 +25,4 @@ export const populateStorybookControlFactories = (controlsServiceStub: ControlsS const rangeSliderControlFactory = rangeSliderFactoryStub as unknown as ControlFactory; rangeSliderControlFactory.getDefaultInput = () => ({}); controlsServiceStub.registerControlType(rangeSliderControlFactory); - - const timesliderFactoryStub = new TimesliderEmbeddableFactory(); - const timeSliderControlFactory = timesliderFactoryStub as unknown as ControlFactory; - timeSliderControlFactory.getDefaultInput = () => ({}); - controlsServiceStub.registerControlType(timeSliderControlFactory); }; diff --git a/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx b/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx deleted file mode 100644 index 90ea07dc276bd56..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import React, { FC, useCallback, useState } from 'react'; -import moment from 'moment'; -import { EuiFormControlLayout } from '@elastic/eui'; - -import { TimeSliderProps, TimeSlider } from '../time_slider.component'; - -export default { - title: 'Time Slider', - description: '', -}; - -const TimeSliderWrapper: FC> = (props) => { - const [value, setValue] = useState(props.value); - const onChange = useCallback( - (newValue: [number | null, number | null]) => { - const lowValue = newValue[0]; - const highValue = newValue[1]; - - setValue([lowValue, highValue]); - }, - [setValue] - ); - - return ( -
- - - -
- ); -}; - -const undefinedValue: [null, null] = [null, null]; -const undefinedRange: [undefined, undefined] = [undefined, undefined]; - -export const TimeSliderNoValuesOrRange = () => { - // If range is undefined, that should be inndicate that we are loading the range - return ; -}; - -export const TimeSliderUndefinedRangeNoValue = () => { - // If a range is [undefined, undefined] then it was loaded, but no values were found. - return ; -}; - -export const TimeSliderUndefinedRangeWithValue = () => { - const lastWeek = moment().subtract(7, 'days'); - const now = moment(); - - return ( - - ); -}; - -export const TimeSliderWithRangeAndNoValue = () => { - const lastWeek = moment().subtract(7, 'days'); - const now = moment(); - - return ( - - ); -}; - -export const TimeSliderWithRangeAndLowerValue = () => { - const lastWeek = moment().subtract(7, 'days'); - const now = moment(); - - const threeDays = moment().subtract(3, 'days'); - - return ( - - ); -}; - -export const TimeSliderWithRangeAndUpperValue = () => { - const lastWeek = moment().subtract(7, 'days'); - const now = moment(); - - const threeDays = moment().subtract(3, 'days'); - - return ( - - ); -}; - -export const TimeSliderWithLowRangeOverlap = () => { - const lastWeek = moment().subtract(7, 'days'); - const now = moment(); - - const threeDays = moment().subtract(3, 'days'); - const twoDays = moment().subtract(2, 'days'); - - return ( - - ); -}; - -export const TimeSliderWithLowRangeOverlapAndIgnoredValidation = () => { - const lastWeek = moment().subtract(7, 'days'); - const now = moment(); - - const threeDays = moment().subtract(3, 'days'); - const twoDays = moment().subtract(2, 'days'); - - return ( - - ); -}; - -export const TimeSliderWithRangeLowerThanValue = () => { - const twoWeeksAgo = moment().subtract(14, 'days'); - const lastWeek = moment().subtract(7, 'days'); - - const now = moment(); - const threeDays = moment().subtract(3, 'days'); - - return ( - - ); -}; - -export const TimeSliderWithRangeHigherThanValue = () => { - const twoWeeksAgo = moment().subtract(14, 'days'); - const lastWeek = moment().subtract(7, 'days'); - - const now = moment(); - const threeDays = moment().subtract(3, 'days'); - - return ( - - ); -}; - -export const PartialValueLowerThanRange = () => { - // Selected value is March 8 -> March 9 - // Range is March 11 -> 25 - const eightDaysAgo = moment().subtract(8, 'days'); - - const lastWeek = moment().subtract(7, 'days'); - const today = moment(); - - return ( - - ); -}; - -export const PartialValueHigherThanRange = () => { - // Selected value is March 8 -> March 9 - // Range is March 11 -> 25 - const eightDaysAgo = moment().subtract(8, 'days'); - - const lastWeek = moment().subtract(7, 'days'); - const today = moment(); - - return ( - - ); -}; diff --git a/src/plugins/controls/public/control_types/time_slider/index.ts b/src/plugins/controls/public/control_types/time_slider/index.ts deleted file mode 100644 index 1cd5900164676d2..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { TimesliderEmbeddableFactory } from './time_slider_embeddable_factory'; -export { type TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; -export {} from '../../../common'; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.component.scss b/src/plugins/controls/public/control_types/time_slider/time_slider.component.scss deleted file mode 100644 index 3f8a37ec44d3705..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/time_slider.component.scss +++ /dev/null @@ -1,47 +0,0 @@ -.timeSlider__anchorOverride { - display:block; - >div { - height: 100%; - } -} - -.timeSlider__popoverOverride { - width: 100%; - max-width: 100%; - height: 100%; -} - -.timeSlider__panelOverride { - min-width: $euiSizeXXL * 15; -} - -.timeSlider__anchor { - text-decoration: none; - width: 100%; - background-color: $euiFormBackgroundColor; - box-shadow: none; - @include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true); - overflow: hidden; - height: 100%; - - &:enabled:focus { - background-color: $euiFormBackgroundColor; - } - - .euiText { - background-color: $euiFormBackgroundColor; - } - - .timeSlider__anchorText { - font-weight: $euiFontWeightBold; - } - - .timeSlider__anchorText--default { - color: $euiColorMediumShade; - } - - .timeSlider__anchorText--invalid { - text-decoration: line-through; - color: $euiColorMediumShade; - } -} \ No newline at end of file diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx deleted file mode 100644 index 1bb2f90b44121f4..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { FC, useState, useMemo, useCallback } from 'react'; -import { isNil } from 'lodash'; -import { - EuiText, - EuiLoadingSpinner, - EuiInputPopover, - EuiPopoverTitle, - EuiSpacer, - EuiFlexItem, - EuiFlexGroup, - EuiToolTip, - EuiButtonIcon, -} from '@elastic/eui'; -import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks'; -import moment from 'moment-timezone'; -import { calcAutoIntervalNear } from '@kbn/data-plugin/common'; -import { ValidatedDualRange } from '@kbn/kibana-react-plugin/public'; -import { TimeSliderStrings } from './time_slider_strings'; -import './time_slider.component.scss'; - -function getScaledDateFormat(interval: number): string { - if (interval >= moment.duration(1, 'y').asMilliseconds()) { - return 'YYYY'; - } - - if (interval >= moment.duration(1, 'd').asMilliseconds()) { - return 'MMM D'; - } - - if (interval >= moment.duration(6, 'h').asMilliseconds()) { - return 'Do HH'; - } - - if (interval >= moment.duration(1, 'h').asMilliseconds()) { - return 'HH:mm'; - } - - if (interval >= moment.duration(1, 'm').asMilliseconds()) { - return 'HH:mm'; - } - - if (interval >= moment.duration(1, 's').asMilliseconds()) { - return 'mm:ss'; - } - - return 'ss.SSS'; -} - -export function getInterval(min: number, max: number, steps = 6): number { - const duration = max - min; - let interval = calcAutoIntervalNear(steps, duration).asMilliseconds(); - // Sometimes auto interval is not quite right and returns 2X or 3X requested ticks - // Adjust the interval to get closer to the requested number of ticks - const actualSteps = duration / interval; - if (actualSteps > steps * 1.5) { - const factor = Math.round(actualSteps / steps); - interval *= factor; - } else if (actualSteps < 5) { - interval *= 0.5; - } - return interval; -} - -export interface TimeSliderProps { - id: string; - range?: [number | undefined, number | undefined]; - value: [number | null, number | null]; - onChange: (range: [number | null, number | null]) => void; - dateFormat?: string; - timezone?: string; - fieldName: string; - ignoreValidation?: boolean; -} - -const isValidRange = (maybeRange: TimeSliderProps['range']): maybeRange is [number, number] => { - return maybeRange !== undefined && !isNil(maybeRange[0]) && !isNil(maybeRange[1]); -}; - -const unselectedClass = 'timeSlider__anchorText--default'; -const validClass = 'timeSlider__anchorText'; -const invalidClass = 'timeSlider__anchorText--invalid'; - -export const TimeSlider: FC = (props) => { - const defaultProps = { - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - ignoreValidation: false, - timezone: 'Browser', - ...props, - }; - const { range, value, timezone, dateFormat, fieldName, ignoreValidation } = defaultProps; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const togglePopover = useCallback(() => { - setIsPopoverOpen(!isPopoverOpen); - }, [isPopoverOpen, setIsPopoverOpen]); - - const getTimezone = useCallback(() => { - const detectedTimezone = moment.tz.guess(); - - return timezone === 'Browser' ? detectedTimezone : timezone; - }, [timezone]); - - const epochToKbnDateFormat = useCallback( - (epoch: number) => { - const tz = getTimezone(); - return moment.tz(epoch, tz).format(dateFormat); - }, - [dateFormat, getTimezone] - ); - - // If we don't have a range or we have is loading, show the loading state - const hasRange = range !== undefined; - - // We have values if we have a range or value entry for both position - const hasValues = - (value[0] !== null || (hasRange && range[0] !== undefined)) && - (value[1] !== null || (hasRange && range[1] !== undefined)); - - let valueText: JSX.Element | null = null; - if (hasValues) { - let lower = value[0] !== null ? value[0] : range![0]!; - let upper = value[1] !== null ? value[1] : range![1]!; - - if (value[0] !== null && lower > upper) { - upper = lower; - } else if (value[1] !== null && lower > upper) { - lower = upper; - } - - const hasLowerValueInRange = - value[0] !== null && isValidRange(range) && value[0] >= range[0] && value[0] <= range[1]; - // It's out of range if the upper value is above the upper range or below the lower range - const hasUpperValueInRange = - value[1] !== null && isValidRange(range) && value[1] <= range[1] && value[1] >= range[0]; - - let lowClass = unselectedClass; - let highClass = unselectedClass; - if (value[0] !== null && (hasLowerValueInRange || ignoreValidation)) { - lowClass = validClass; - } else if (value[0] !== null) { - lowClass = invalidClass; - } - - if (value[1] !== null && (hasUpperValueInRange || ignoreValidation)) { - highClass = validClass; - } else if (value[1] !== null) { - highClass = invalidClass; - } - - // if no value then anchorText default - // if hasLowerValueInRange || skipValidation then anchor text - // else strikethrough - - valueText = ( - - {epochToKbnDateFormat(lower)} -   →   - {epochToKbnDateFormat(upper)} - - ); - } - - const button = ( - - ); - - return ( - setIsPopoverOpen(false)} - panelPaddingSize="s" - anchorPosition="downCenter" - disableFocusTrap - attachToAnchor={false} - > - {isValidRange(range) ? ( - - ) : ( - - )} - - ); -}; - -const TimeSliderComponentPopoverNoDocuments: FC = () => { - return {TimeSliderStrings.noDocumentsPopover.getLabel()}; -}; - -export const TimeSliderComponentPopover: FC< - TimeSliderProps & { - range: [number, number]; - getTimezone: () => string; - epochToKbnDateFormat: (epoch: number) => string; - } -> = ({ range, value, onChange, getTimezone, epochToKbnDateFormat, fieldName }) => { - const [lowerBound, upperBound] = range; - let [lowerValue, upperValue] = value; - - if (lowerValue === null) { - lowerValue = lowerBound; - } - - if (upperValue === null) { - upperValue = upperBound; - } - - const fullRange = useMemo( - () => [Math.min(lowerValue!, lowerBound), Math.max(upperValue!, upperBound)], - [lowerValue, lowerBound, upperValue, upperBound] - ); - - const getTicks = useCallback( - (min: number, max: number, interval: number): EuiRangeTick[] => { - const format = getScaledDateFormat(interval); - const tz = getTimezone(); - - let tick = Math.ceil(min / interval) * interval; - const ticks: EuiRangeTick[] = []; - while (tick < max) { - ticks.push({ - value: tick, - label: moment.tz(tick, tz).format(format), - }); - tick += interval; - } - - return ticks; - }, - [getTimezone] - ); - - const ticks = useMemo(() => { - const interval = getInterval(fullRange[0], fullRange[1]); - return getTicks(fullRange[0], fullRange[1], interval); - }, [fullRange, getTicks]); - - const onChangeHandler = useCallback( - ([_min, _max]: [number | string, number | string]) => { - // If a value is undefined and the number that is given here matches the range bounds - // then we will ignore it, becuase they probably didn't actually select that value - const report: [number | null, number | null] = [null, null]; - - let min: number; - let max: number; - if (typeof _min === 'string') { - min = parseFloat(_min); - min = isNaN(min) ? range[0] : min; - } else { - min = _min; - } - - if (typeof _max === 'string') { - max = parseFloat(_max); - max = isNaN(max) ? range[0] : max; - } else { - max = _max; - } - - if (value[0] !== null || min !== range[0]) { - report[0] = min; - } - if (value[1] !== null || max !== range[1]) { - report[1] = max; - } - - onChange(report); - }, - [onChange, value, range] - ); - - const levels = [{ min: range[0], max: range[1], color: 'success' }]; - - return ( - <> - {fieldName} - - {epochToKbnDateFormat(lowerValue)} - {epochToKbnDateFormat(upperValue)} - - - - - - - - - onChange([null, null])} - aria-label={TimeSliderStrings.resetButton.getLabel()} - data-test-subj="timeSlider__clearRangeButton" - /> - - - - - - ); -}; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider.tsx deleted file mode 100644 index 0b519406ccf8d6e..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/time_slider.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { FC, useCallback, useMemo } from 'react'; -import { BehaviorSubject } from 'rxjs'; -import { debounce } from 'lodash'; -import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; - -import { timeSliderReducers } from './time_slider_reducers'; -import { TimeSlider as Component } from './time_slider.component'; -import { TimeSliderReduxState, TimeSliderSubjectState } from './types'; - -interface TimeSliderProps { - componentStateSubject: BehaviorSubject; - dateFormat: string; - timezone: string; - fieldName: string; - ignoreValidation: boolean; -} - -export const TimeSlider: FC = ({ - componentStateSubject, - dateFormat, - timezone, - fieldName, - ignoreValidation, -}) => { - const { - useEmbeddableDispatch, - useEmbeddableSelector: select, - actions: { selectRange }, - } = useReduxEmbeddableContext(); - const dispatch = useEmbeddableDispatch(); - - const availableRange = select((state) => state.componentState.range); - const value = select((state) => state.explicitInput.value); - const id = select((state) => state.explicitInput.id); - - const { min, max } = availableRange - ? availableRange - : ({} as { - min?: number; - max?: number; - }); - - const dispatchChange = useCallback( - (range: [number | null, number | null]) => { - dispatch(selectRange(range)); - }, - [dispatch, selectRange] - ); - - const debouncedDispatchChange = useMemo(() => debounce(dispatchChange, 500), [dispatchChange]); - - const onChangeComplete = useCallback( - (range: [number | null, number | null]) => { - debouncedDispatchChange(range); - }, - [debouncedDispatchChange] - ); - - return ( - - ); -}; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.test.ts b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.test.ts deleted file mode 100644 index 4db5277a0e2f9db..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { of } from 'rxjs'; -import { delay, map } from 'rxjs/operators'; -import { TimeSliderControlEmbeddableInput } from '.'; -import { TimeSliderControlEmbeddable } from './time_slider_embeddable'; -import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; -import { pluginServices } from '../../services'; -import { TestScheduler } from 'rxjs/testing'; -import { buildRangeFilter } from '@kbn/es-query'; -import { ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public'; - -const buildFilter = (range: [number | null, number | null]) => { - const filterPieces: Record = {}; - if (range[0] !== null) { - filterPieces.gte = range[0]; - } - if (range[1] !== null) { - filterPieces.lte = range[1]; - } - - const filter = buildRangeFilter( - stubLogstashDataView.getFieldByName('bytes')!, - filterPieces, - stubLogstashDataView - ); - filter.meta.key = 'bytes'; - - return filter; -}; - -const rangeMin = 20; -const rangeMax = 30; -const range = { min: rangeMin, max: rangeMax }; - -const lowerValue: [number, number] = [15, 25]; -const upperValue: [number, number] = [25, 35]; -const partialLowValue: [number, null] = [25, null]; -const partialHighValue: [null, number] = [null, 25]; -const withinRangeValue: [number, number] = [21, 29]; -const outOfRangeValue: [number, number] = [31, 40]; - -const rangeFilter = buildFilter([rangeMin, rangeMax]); -const lowerValueFilter = buildFilter(lowerValue); -const lowerValuePartialFilter = buildFilter([20, 25]); -const upperValueFilter = buildFilter(upperValue); -const upperValuePartialFilter = buildFilter([25, 30]); - -const partialLowValueFilter = buildFilter(partialLowValue); -const partialHighValueFilter = buildFilter(partialHighValue); -const withinRangeValueFilter = buildFilter(withinRangeValue); -const outOfRangeValueFilter = buildFilter(outOfRangeValue); - -const baseInput: TimeSliderControlEmbeddableInput = { - id: 'id', - fieldName: 'bytes', - dataViewId: stubLogstashDataView.id!, -}; - -const mockReduxEmbeddablePackage = { - createTools: () => {}, -} as unknown as ReduxEmbeddablePackage; - -describe('Time Slider Control Embeddable', () => { - const services = pluginServices.getServices(); - const fetchRange = jest.spyOn(services.data, 'fetchFieldRange'); - const getDataView = jest.spyOn(services.data, 'getDataView'); - const fetchRange$ = jest.spyOn(services.data, 'fetchFieldRange$'); - const getDataView$ = jest.spyOn(services.data, 'getDataView$'); - - beforeEach(() => { - jest.resetAllMocks(); - - fetchRange.mockResolvedValue(range); - fetchRange$.mockReturnValue(of(range).pipe(delay(100))); - getDataView.mockResolvedValue(stubLogstashDataView); - getDataView$.mockReturnValue(of(stubLogstashDataView)); - }); - - describe('outputting filters', () => { - let testScheduler: TestScheduler; - beforeEach(() => { - testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); - }); - - const testFilterOutput = ( - input: any, - expectedFilterAfterRangeFetch: any, - mockRange: { min?: number; max?: number } = range - ) => { - testScheduler.run(({ expectObservable, cold }) => { - fetchRange$.mockReturnValue(cold('--b', { b: mockRange })); - const expectedMarbles = 'a-b'; - const expectedValues = { - a: undefined, - b: expectedFilterAfterRangeFetch ? [expectedFilterAfterRangeFetch] : undefined, - }; - - const embeddable = new TimeSliderControlEmbeddable(mockReduxEmbeddablePackage, input, {}); - const source$ = embeddable.getOutput$().pipe(map((o) => o.filters)); - - expectObservable(source$).toBe(expectedMarbles, expectedValues); - }); - }; - - it('outputs no filter when no value is given', () => { - testFilterOutput(baseInput, undefined); - }); - - it('outputs the value filter after the range is fetched', () => { - testFilterOutput({ ...baseInput, value: withinRangeValue }, withinRangeValueFilter); - }); - - it('outputs a partial filter for a low partial value', () => { - testFilterOutput({ ...baseInput, value: partialLowValue }, partialLowValueFilter); - }); - - it('outputs a partial filter for a high partial value', () => { - testFilterOutput({ ...baseInput, value: partialHighValue }, partialHighValueFilter); - }); - - describe('with validation', () => { - it('outputs a partial value filter if value is below range', () => { - testFilterOutput({ ...baseInput, value: lowerValue }, lowerValuePartialFilter); - }); - - it('outputs a partial value filter if value is above range', () => { - testFilterOutput({ ...baseInput, value: upperValue }, upperValuePartialFilter); - }); - - it('outputs range filter value if value is completely out of range', () => { - testFilterOutput({ ...baseInput, value: outOfRangeValue }, rangeFilter); - }); - - it('outputs no filter when no range available', () => { - testFilterOutput({ ...baseInput, value: withinRangeValue }, undefined, {}); - }); - }); - - describe('with validation off', () => { - it('outputs the lower value filter', () => { - testFilterOutput( - { ...baseInput, ignoreParentSettings: { ignoreValidations: true }, value: lowerValue }, - lowerValueFilter - ); - }); - - it('outputs the uppwer value filter', () => { - testFilterOutput( - { ...baseInput, ignoreParentSettings: { ignoreValidations: true }, value: upperValue }, - upperValueFilter - ); - }); - - it('outputs the out of range filter', () => { - testFilterOutput( - { - ...baseInput, - ignoreParentSettings: { ignoreValidations: true }, - value: outOfRangeValue, - }, - outOfRangeValueFilter - ); - }); - - it('outputs the value filter when no range found', () => { - testFilterOutput( - { - ...baseInput, - ignoreParentSettings: { ignoreValidations: true }, - value: withinRangeValue, - }, - withinRangeValueFilter, - { min: undefined, max: undefined } - ); - }); - }); - }); - - describe('fetching range', () => { - it('fetches range on init', () => { - const testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); - - testScheduler.run(({ cold, expectObservable }) => { - const mockRange = { min: 1, max: 2 }; - fetchRange$.mockReturnValue(cold('--b', { b: mockRange })); - - const expectedMarbles = 'a-b'; - const expectedValues = { - a: undefined, - b: mockRange, - }; - - const embeddable = new TimeSliderControlEmbeddable( - mockReduxEmbeddablePackage, - baseInput, - {} - ); - const source$ = embeddable.getComponentState$().pipe(map((state) => state.range)); - - const { fieldName, ...inputForFetch } = baseInput; - - expectObservable(source$).toBe(expectedMarbles, expectedValues); - expect(fetchRange$).toBeCalledWith(stubLogstashDataView, fieldName, { - ...inputForFetch, - filters: undefined, - query: undefined, - timeRange: undefined, - viewMode: 'edit', - }); - }); - }); - - it('fetches range on input change', () => { - const testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); - - testScheduler.run(({ cold, expectObservable, flush }) => { - const mockRange = { min: 1, max: 2 }; - fetchRange$.mockReturnValue(cold('a', { a: mockRange })); - - const embeddable = new TimeSliderControlEmbeddable( - mockReduxEmbeddablePackage, - baseInput, - {} - ); - const updatedInput = { ...baseInput, fieldName: '@timestamp' }; - - embeddable.updateInput(updatedInput); - - expect(fetchRange$).toBeCalledTimes(2); - expect(fetchRange$.mock.calls[1][1]).toBe(updatedInput.fieldName); - }); - }); - - it('passes input to fetch range to build the query', () => { - const testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); - - testScheduler.run(({ cold, expectObservable, flush }) => { - const mockRange = { min: 1, max: 2 }; - fetchRange$.mockReturnValue(cold('a', { a: mockRange })); - - const input = { - ...baseInput, - query: {} as any, - filters: {} as any, - timeRange: {} as any, - }; - - new TimeSliderControlEmbeddable(mockReduxEmbeddablePackage, input, {}); - - expect(fetchRange$).toBeCalledTimes(1); - const args = fetchRange$.mock.calls[0][2]; - expect(args.query).toBe(input.query); - expect(args.filters).toBe(input.filters); - expect(args.timeRange).toBe(input.timeRange); - }); - }); - - it('does not pass ignored parent settings', () => { - const testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); - - testScheduler.run(({ cold, expectObservable, flush }) => { - const mockRange = { min: 1, max: 2 }; - fetchRange$.mockReturnValue(cold('a', { a: mockRange })); - - const input = { - ...baseInput, - query: '' as any, - filters: {} as any, - timeRange: {} as any, - ignoreParentSettings: { ignoreFilters: true, ignoreQuery: true, ignoreTimerange: true }, - }; - - new TimeSliderControlEmbeddable(mockReduxEmbeddablePackage, input, {}); - - expect(fetchRange$).toBeCalledTimes(1); - const args = fetchRange$.mock.calls[0][2]; - expect(args.query).not.toBe(input.query); - expect(args.filters).not.toBe(input.filters); - expect(args.timeRange).not.toBe(input.timeRange); - }); - }); - }); -}); diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.tsx deleted file mode 100644 index a4098a72dfe1ab7..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.tsx +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { compareFilters, buildRangeFilter, RangeFilterParams } from '@kbn/es-query'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import { isEqual } from 'lodash'; -import deepEqual from 'fast-deep-equal'; -import { merge, Subscription, BehaviorSubject, Observable } from 'rxjs'; -import { map, distinctUntilChanged, skip, take, mergeMap } from 'rxjs/operators'; - -import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public'; -import { ReduxEmbeddableTools, ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/public'; - -import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; -import { TIME_SLIDER_CONTROL } from '../..'; -import { ControlsSettingsService } from '../../services/settings'; -import { ControlsDataService } from '../../services/data'; -import { ControlOutput } from '../..'; -import { pluginServices } from '../../services'; - -import { TimeSlider as TimeSliderComponent } from './time_slider'; -import { timeSliderReducers } from './time_slider_reducers'; -import { TimeSliderReduxState, TimeSliderSubjectState } from './types'; - -const diffDataFetchProps = (current?: any, last?: any) => { - if (!current || !last) return false; - const { filters: currentFilters, ...currentWithoutFilters } = current; - const { filters: lastFilters, ...lastWithoutFilters } = last; - if (!deepEqual(currentWithoutFilters, lastWithoutFilters)) return false; - if (!compareFilters(lastFilters ?? [], currentFilters ?? [])) return false; - return true; -}; - -export class TimeSliderControlEmbeddable extends Embeddable< - TimeSliderControlEmbeddableInput, - ControlOutput -> { - public readonly type = TIME_SLIDER_CONTROL; - public deferEmbeddableLoad = true; - - private subscriptions: Subscription = new Subscription(); - private node?: HTMLElement; - - // Internal data fetching state for this input control. - private dataView?: DataView; - - private componentState: TimeSliderSubjectState; - private componentStateSubject$ = new BehaviorSubject({ - range: undefined, - loading: false, - }); - - // Internal state subject will let us batch updates to the externally accessible state subject - private internalComponentStateSubject$ = new BehaviorSubject({ - range: undefined, - loading: false, - }); - - private internalOutput: ControlOutput; - - private fetchRange$: ControlsDataService['fetchFieldRange$']; - private getDataView$: ControlsDataService['getDataView$']; - private getDateFormat: ControlsSettingsService['getDateFormat']; - private getTimezone: ControlsSettingsService['getTimezone']; - - private reduxEmbeddableTools: ReduxEmbeddableTools< - TimeSliderReduxState, - typeof timeSliderReducers - >; - - constructor( - reduxEmbeddablePackage: ReduxEmbeddablePackage, - input: TimeSliderControlEmbeddableInput, - output: ControlOutput, - parent?: IContainer - ) { - super(input, output, parent); // get filters for initial output... - - const { - data: { fetchFieldRange$, getDataView$ }, - settings: { getDateFormat, getTimezone }, - } = pluginServices.getServices(); - this.fetchRange$ = fetchFieldRange$; - this.getDataView$ = getDataView$; - this.getDateFormat = getDateFormat; - this.getTimezone = getTimezone; - - this.componentState = { loading: true }; - this.updateComponentState(this.componentState, true); - - this.internalOutput = {}; - - // build redux embeddable tools - this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools< - TimeSliderReduxState, - typeof timeSliderReducers - >({ - embeddable: this, - reducers: timeSliderReducers, - }); - - this.initialize(); - } - - private initialize() { - // If value is undefined, then we can be finished with initialization because we're not going to output a filter - if (this.getInput().value === undefined) { - this.setInitializationFinished(); - } - - this.setupSubscriptions(); - } - - private setupSubscriptions() { - // We need to fetch data when any of these values change - const dataFetchPipe = this.getInput$().pipe( - map((newInput) => ({ - lastReloadRequestTime: newInput.lastReloadRequestTime, - dataViewId: newInput.dataViewId, - fieldName: newInput.fieldName, - timeRange: newInput.timeRange, - filters: newInput.filters, - query: newInput.query, - })), - distinctUntilChanged(diffDataFetchProps) - ); - - // When data fetch pipe emits, we start the fetch - this.subscriptions.add(dataFetchPipe.subscribe(this.fetchAvailableTimerange)); - - const availableRangePipe = this.internalComponentStateSubject$.pipe( - map((state) => (state.range ? { min: state.range.min, max: state.range.max } : {})), - distinctUntilChanged((a, b) => isEqual(a, b)) - ); - - this.subscriptions.add( - merge( - this.getInput$().pipe( - skip(1), // Skip the first input value - distinctUntilChanged((a, b) => isEqual(a.value, b.value)) - ), - availableRangePipe.pipe(skip(1)) - ).subscribe(() => { - this.setInitializationFinished(); - this.buildFilter(); - - this.componentStateSubject$.next(this.componentState); - }) - ); - } - - private buildFilter = () => { - const { fieldName, value, ignoreParentSettings } = this.getInput(); - - const min = value ? value[0] : null; - const max = value ? value[1] : null; - const hasRange = - this.componentState.range?.max !== undefined && this.componentState.range?.min !== undefined; - - this.getCurrentDataView$().subscribe((dataView) => { - const range: RangeFilterParams = {}; - let filterMin: number | undefined; - let filterMax: number | undefined; - const field = dataView.getFieldByName(fieldName); - - if (ignoreParentSettings?.ignoreValidations) { - if (min !== null) { - range.gte = min; - } - - if (max !== null) { - range.lte = max; - } - } else { - // If we have a value or a range use the min/max of those, otherwise undefined - if (min !== null && this.componentState.range!.min !== undefined) { - filterMin = Math.max(min || 0, this.componentState.range!.min || 0); - } - - if (max !== null && this.componentState.range!.max) { - filterMax = Math.min( - max || Number.MAX_SAFE_INTEGER, - this.componentState.range!.max || Number.MAX_SAFE_INTEGER - ); - } - - // Last check, if the value is completely outside the range then we will just default to the range - if ( - hasRange && - ((min !== null && min > this.componentState.range!.max!) || - (max !== null && max < this.componentState.range!.min!)) - ) { - filterMin = this.componentState.range!.min; - filterMax = this.componentState.range!.max; - } - - if (hasRange && filterMin !== undefined) { - range.gte = filterMin; - } - if (hasRange && filterMax !== undefined) { - range.lte = filterMax; - } - } - - if (range.lte !== undefined || range.gte !== undefined) { - const rangeFilter = buildRangeFilter(field!, range, dataView); - rangeFilter.meta.key = field?.name; - - this.updateInternalOutput({ filters: [rangeFilter] }, true); - this.updateComponentState({ loading: false }); - } else { - this.updateInternalOutput({ filters: undefined, dataViewId: dataView.id }, true); - this.updateComponentState({ loading: false }); - } - }); - }; - - private updateComponentState(changes: Partial, publish = false) { - this.componentState = { - ...this.componentState, - ...changes, - }; - - this.internalComponentStateSubject$.next(this.componentState); - - if (publish) { - this.componentStateSubject$.next(this.componentState); - } - } - - private updateInternalOutput(changes: Partial, publish = false) { - this.internalOutput = { - ...this.internalOutput, - ...changes, - }; - - if (publish) { - this.updateOutput(this.internalOutput); - } - } - - private getCurrentDataView$ = () => { - const { dataViewId } = this.getInput(); - if (this.dataView && this.dataView.id === dataViewId) - return new Observable((subscriber) => { - subscriber.next(this.dataView); - subscriber.complete(); - }); - - return this.getDataView$(dataViewId); - }; - - private fetchAvailableTimerange = () => { - this.updateComponentState({ loading: true }, true); - this.updateInternalOutput({ loading: true }, true); - - const { fieldName, ignoreParentSettings, query, filters, timeRange, ...input } = - this.getInput(); - - const inputForFetch = { - ...input, - ...(ignoreParentSettings?.ignoreQuery ? {} : { query }), - ...(ignoreParentSettings?.ignoreFilters ? {} : { filters }), - ...(ignoreParentSettings?.ignoreTimerange ? {} : { timeRange }), - }; - - try { - this.getCurrentDataView$() - .pipe( - mergeMap((dataView) => this.fetchRange$(dataView, fieldName, inputForFetch)), - take(1) - ) - .subscribe(({ min, max }) => { - this.updateInternalOutput({ loading: false }); - this.updateComponentState({ - range: { - min: min === null ? undefined : min, - max: max === null ? undefined : max, - }, - loading: false, - }); - }); - } catch (e) { - this.updateComponentState({ loading: false }, true); - this.updateInternalOutput({ loading: false }, true); - } - }; - - public getComponentState$ = () => { - return this.componentStateSubject$; - }; - - public destroy = () => { - super.destroy(); - this.subscriptions.unsubscribe(); - }; - - public reload = () => { - this.fetchAvailableTimerange(); - }; - - public render = (node: HTMLElement) => { - if (this.node) { - ReactDOM.unmountComponentAtNode(this.node); - } - this.node = node; - - const { Wrapper: TimeSliderControlReduxWrapper } = this.reduxEmbeddableTools; - - ReactDOM.render( - - - , - node - ); - }; -} diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable_factory.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable_factory.tsx deleted file mode 100644 index 51048489ccae9b7..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable_factory.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import deepEqual from 'fast-deep-equal'; - -import { lazyLoadReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public'; -import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public'; - -import { TIME_SLIDER_CONTROL } from '../..'; -import { ControlEmbeddable, DataControlField, IEditableControlFactory } from '../../types'; -import { - createOptionsListExtract, - createOptionsListInject, -} from '../../../common/options_list/options_list_persistable_state'; -import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; -import { TimeSliderStrings } from './time_slider_strings'; - -export class TimesliderEmbeddableFactory - implements EmbeddableFactoryDefinition, IEditableControlFactory -{ - public type = TIME_SLIDER_CONTROL; - public canCreateNew = () => false; - - constructor() {} - - public async create(initialInput: TimeSliderControlEmbeddableInput, parent?: IContainer) { - const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage(); - const { TimeSliderControlEmbeddable } = await import('./time_slider_embeddable'); - - return Promise.resolve( - new TimeSliderControlEmbeddable(reduxEmbeddablePackage, initialInput, {}, parent) - ); - } - - public presaveTransformFunction = ( - newInput: Partial, - embeddable?: ControlEmbeddable - ) => { - if ( - embeddable && - ((newInput.fieldName && !deepEqual(newInput.fieldName, embeddable.getInput().fieldName)) || - (newInput.dataViewId && !deepEqual(newInput.dataViewId, embeddable.getInput().dataViewId))) - ) { - // if the field name or data view id has changed in this editing session, selected options are invalid, so reset them. - newInput.value = undefined; - } - return newInput; - }; - - public isFieldCompatible = (dataControlField: DataControlField) => { - if (dataControlField.field.type === 'date') { - dataControlField.compatibleControlTypes.push(this.type); - } - }; - - public isEditable = () => Promise.resolve(false); - - public getDisplayName = () => TimeSliderStrings.getDisplayName(); - public getIconType = () => 'clock'; - public getDescription = () => TimeSliderStrings.getDescription(); - - public inject = createOptionsListInject(); - public extract = createOptionsListExtract(); -} diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts b/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts deleted file mode 100644 index 95b8d87dc902e51..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the 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 { PayloadAction } from '@reduxjs/toolkit'; -import { WritableDraft } from 'immer/dist/types/types-external'; -import { TimeSliderReduxState } from './types'; - -export const timeSliderReducers = { - selectRange: ( - state: WritableDraft, - action: PayloadAction<[number | null, number | null]> - ) => { - state.explicitInput.value = action.payload; - }, -}; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_strings.ts b/src/plugins/controls/public/control_types/time_slider/time_slider_strings.ts deleted file mode 100644 index 2c61d7d43a7976d..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/time_slider_strings.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -export const TimeSliderStrings = { - getDisplayName: () => - i18n.translate('controls.timeSlider.displayName', { - defaultMessage: 'Time slider', - }), - getDescription: () => - i18n.translate('controls.timeSlider.description', { - defaultMessage: 'Add a slider for selecting a time range', - }), - editor: { - getDataViewTitle: () => - i18n.translate('controls.timeSlider.editor.dataViewTitle', { - defaultMessage: 'Data view', - }), - getNoDataViewTitle: () => - i18n.translate('controls.timeSlider.editor.noDataViewTitle', { - defaultMessage: 'Select data view', - }), - getFieldTitle: () => - i18n.translate('controls.timeSlider.editor.fieldTitle', { - defaultMessage: 'Field', - }), - }, - resetButton: { - getLabel: () => - i18n.translate('controls.timeSlider.resetButton.label', { - defaultMessage: 'Reset selections', - }), - }, - noDocumentsPopover: { - getLabel: () => - i18n.translate('controls.timeSlider.noDocuments.label', { - defaultMessage: 'There were no documents found. Range selection unavailable.', - }), - }, -}; diff --git a/src/plugins/controls/public/control_types/time_slider/types.ts b/src/plugins/controls/public/control_types/time_slider/types.ts deleted file mode 100644 index fc147dc3ba9596b..000000000000000 --- a/src/plugins/controls/public/control_types/time_slider/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'; - -import { ControlOutput } from '../../types'; -import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; - -export * from '../../../common/control_types/time_slider/types'; - -// Component state is only used by public components. -export interface TimeSliderSubjectState { - range?: { - min?: number; - max?: number; - }; - loading: boolean; -} - -// public only - redux embeddable state type -export type TimeSliderReduxState = ReduxEmbeddableState< - TimeSliderControlEmbeddableInput, - ControlOutput, - TimeSliderSubjectState ->; diff --git a/src/plugins/controls/public/index.ts b/src/plugins/controls/public/index.ts index f55df5fa0f53a89..ecf430f6cc9277b 100644 --- a/src/plugins/controls/public/index.ts +++ b/src/plugins/controls/public/index.ts @@ -24,12 +24,7 @@ export type { ControlInput, } from '../common/types'; -export { - CONTROL_GROUP_TYPE, - OPTIONS_LIST_CONTROL, - RANGE_SLIDER_CONTROL, - TIME_SLIDER_CONTROL, -} from '../common'; +export { CONTROL_GROUP_TYPE, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '../common'; export { ControlGroupContainer, diff --git a/src/plugins/controls/public/plugin.ts b/src/plugins/controls/public/plugin.ts index da45ba2e68684d0..01375b174a93496 100644 --- a/src/plugins/controls/public/plugin.ts +++ b/src/plugins/controls/public/plugin.ts @@ -14,7 +14,6 @@ import { CONTROL_GROUP_TYPE, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, - // TIME_SLIDER_CONTROL, } from '.'; import { OptionsListEmbeddableFactory, OptionsListEmbeddableInput } from './options_list'; import { RangeSliderEmbeddableFactory, RangeSliderEmbeddableInput } from './range_slider'; @@ -29,13 +28,6 @@ import { ControlInput, } from './types'; -/* -import { - TimesliderEmbeddableFactory, - TimeSliderControlEmbeddableInput, -} from './control_types/time_slider'; -*/ - export class ControlsPlugin implements Plugin< @@ -101,22 +93,6 @@ export class ControlsPlugin rangeSliderFactory ); registerControlType(rangeSliderFactory); - - // Time Slider Control Factory Setup - /* Temporary disabling Time Slider - const timeSliderFactoryDef = new TimesliderEmbeddableFactory(); - const timeSliderFactory = embeddable.registerEmbeddableFactory( - TIME_SLIDER_CONTROL, - timeSliderFactoryDef - )(); - this.transferEditorFunctions( - timeSliderFactoryDef, - timeSliderFactory - ); - - - registerControlType(timeSliderFactory); - */ }); return { diff --git a/src/plugins/controls/server/control_group/control_group_telemetry.test.ts b/src/plugins/controls/server/control_group/control_group_telemetry.test.ts index 140b58fd790fca0..1e66bb15fbc2bd4 100644 --- a/src/plugins/controls/server/control_group/control_group_telemetry.test.ts +++ b/src/plugins/controls/server/control_group/control_group_telemetry.test.ts @@ -24,7 +24,7 @@ const rawControlAttributes2: RawControlGroupAttributes = { controlStyle: 'oneLine', chainingSystem: 'NONE', panelsJSON: - '{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"b47916fd-fc03-4dcd-bef1-5c3b7a315723":{"order":1,"width":"auto","type":"timeSlider","explicitInput":{"title":"timestamp","fieldName":"timestamp","id":"b47916fd-fc03-4dcd-bef1-5c3b7a315723","enhancements":{}}},"f6b076c6-9ef5-483e-b08d-d313d60d4b8c":{"order":2,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceMiles","fieldName":"DistanceMiles","id":"f6b076c6-9ef5-483e-b08d-d313d60d4b8c","enhancements":{}}}}', + '{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"f6b076c6-9ef5-483e-b08d-d313d60d4b8c":{"order":2,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceMiles","fieldName":"DistanceMiles","id":"f6b076c6-9ef5-483e-b08d-d313d60d4b8c","enhancements":{}}}}', ignoreParentSettingsJSON: '{"ignoreFilters":true,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}', }; @@ -34,7 +34,7 @@ const rawControlAttributes3: RawControlGroupAttributes = { controlStyle: 'oneLine', chainingSystem: 'HIERARCHICAL', panelsJSON: - '{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"b47916fd-fc03-4dcd-bef1-5c3b7a315723":{"order":1,"width":"auto","type":"timeSlider","explicitInput":{"title":"timestamp","fieldName":"timestamp","id":"b47916fd-fc03-4dcd-bef1-5c3b7a315723","enhancements":{}}},"ee325e9e-6ec1-41f9-953f-423d59850d44":{"order":2,"width":"auto","type":"optionsListControl","explicitInput":{"title":"Carrier","fieldName":"Carrier","id":"ee325e9e-6ec1-41f9-953f-423d59850d44","enhancements":{}}},"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b":{"order":3,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestCityName","fieldName":"DestCityName","id":"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b","enhancements":{}}}}', + '{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"ee325e9e-6ec1-41f9-953f-423d59850d44":{"order":2,"width":"auto","type":"optionsListControl","explicitInput":{"title":"Carrier","fieldName":"Carrier","id":"ee325e9e-6ec1-41f9-953f-423d59850d44","enhancements":{}}},"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b":{"order":3,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestCityName","fieldName":"DestCityName","id":"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b","enhancements":{}}}}', ignoreParentSettingsJSON: '{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}', }; @@ -97,7 +97,7 @@ describe('Control group telemetry function', () => { }); test('counts all telemetry over multiple runs', () => { - expect(finalTelemetry.total).toBe(10); + expect(finalTelemetry.total).toBe(8); }); test('counts control types over multiple runs.', () => { @@ -110,10 +110,6 @@ describe('Control group telemetry function', () => { details: {}, total: 3, }, - timeSlider: { - details: {}, - total: 2, - }, }); }); diff --git a/src/plugins/controls/server/control_types/time_slider/time_slider_embeddable_factory.ts b/src/plugins/controls/server/control_types/time_slider/time_slider_embeddable_factory.ts deleted file mode 100644 index 8e8920f12f5545e..000000000000000 --- a/src/plugins/controls/server/control_types/time_slider/time_slider_embeddable_factory.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the 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 { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/server'; -import { TIME_SLIDER_CONTROL } from '../../../common'; -import { - createTimeSliderExtract, - createTimeSliderInject, -} from '../../../common/control_types/time_slider/time_slider_persistable_state'; - -export const timeSliderPersistableStateServiceFactory = (): EmbeddableRegistryDefinition => { - return { - id: TIME_SLIDER_CONTROL, - extract: createTimeSliderExtract(), - inject: createTimeSliderInject(), - }; -}; diff --git a/src/plugins/controls/server/plugin.ts b/src/plugins/controls/server/plugin.ts index 00d968881579631..019430166ff6fa9 100644 --- a/src/plugins/controls/server/plugin.ts +++ b/src/plugins/controls/server/plugin.ts @@ -14,7 +14,6 @@ import { setupOptionsListSuggestionsRoute } from './options_list/options_list_su import { controlGroupContainerPersistableStateServiceFactory } from './control_group/control_group_container_factory'; import { optionsListPersistableStateServiceFactory } from './options_list/options_list_embeddable_factory'; import { rangeSliderPersistableStateServiceFactory } from './range_slider/range_slider_embeddable_factory'; -// import { timeSliderPersistableStateServiceFactory } from './control_types/time_slider/time_slider_embeddable_factory'; interface SetupDeps { embeddable: EmbeddableSetup; @@ -24,14 +23,11 @@ interface SetupDeps { export class ControlsPlugin implements Plugin { public setup(core: CoreSetup, { embeddable, unifiedSearch }: SetupDeps) { - embeddable.registerEmbeddableFactory(optionsListPersistableStateServiceFactory()); - embeddable.registerEmbeddableFactory(rangeSliderPersistableStateServiceFactory()); - // Temporary disabling Time Slider - // embeddable.registerEmbeddableFactory(timeSliderPersistableStateServiceFactory()); - embeddable.registerEmbeddableFactory( controlGroupContainerPersistableStateServiceFactory(embeddable) ); + embeddable.registerEmbeddableFactory(optionsListPersistableStateServiceFactory()); + embeddable.registerEmbeddableFactory(rangeSliderPersistableStateServiceFactory()); setupOptionsListSuggestionsRoute(core, unifiedSearch.autocomplete.getAutocompleteSettings); return {}; diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 31c360e76107896..fce2680f5fc030d 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -493,8 +493,9 @@ export function getUiSettings( description: i18n.translate('data.advancedSettings.autocompleteValueSuggestionMethodText', { defaultMessage: 'The method used for querying suggestions for values in KQL autocomplete. Select terms_enum to use the ' + - 'Elasticsearch terms enum API for improved autocomplete suggestion performance. Select terms_agg to use an ' + - 'Elasticsearch terms aggregation. {learnMoreLink}', + 'Elasticsearch terms enum API for improved autocomplete suggestion performance. (Note that terms_enum is ' + + 'incompatible with Document Level Security.) Select terms_agg to use an Elasticsearch terms aggregation. ' + + '{learnMoreLink}', values: { learnMoreLink: `` + diff --git a/src/plugins/vis_types/table/public/components/table_vis_cell.tsx b/src/plugins/vis_types/table/public/components/table_vis_cell.tsx index e11700330b33433..586a534ef16e753 100644 --- a/src/plugins/vis_types/table/public/components/table_vis_cell.tsx +++ b/src/plugins/vis_types/table/public/components/table_vis_cell.tsx @@ -15,9 +15,10 @@ import { FormattedColumns } from '../types'; export const createTableVisCell = (rows: DatatableRow[], formattedColumns: FormattedColumns, autoFitRowToContent?: boolean) => ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { - const rowValue = rows[rowIndex][columnId]; + // incoming data might change and put the current page out of bounds - check whether row actually exists + const rowValue = rows[rowIndex]?.[columnId]; const column = formattedColumns[columnId]; - const content = column.formatter.convert(rowValue, 'html'); + const content = column?.formatter.convert(rowValue, 'html'); const cellContent = (
{ - const rowValue = rows[rowIndex][columnId]; + // incoming data might change and put the current page out of bounds - check whether row actually exists + const rowValue = rows[rowIndex]?.[columnId]; if (rowValue == null) return null; const cellContent = formattedColumn.formatter.convert(rowValue); @@ -96,7 +97,8 @@ export const createGridColumns = ( ); }, ({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => { - const rowValue = rows[rowIndex][columnId]; + // incoming data might change and put the current page out of bounds - check whether row actually exists + const rowValue = rows[rowIndex]?.[columnId]; if (rowValue == null) return null; const cellContent = formattedColumn.formatter.convert(rowValue); diff --git a/test/functional/apps/dashboard_elements/controls/replace_controls.ts b/test/functional/apps/dashboard_elements/controls/replace_controls.ts index 2d1f4509f04cef4..ff4efebf9cac96e 100644 --- a/test/functional/apps/dashboard_elements/controls/replace_controls.ts +++ b/test/functional/apps/dashboard_elements/controls/replace_controls.ts @@ -6,11 +6,7 @@ * Side Public License, v 1. */ -import { - OPTIONS_LIST_CONTROL, - RANGE_SLIDER_CONTROL, - TIME_SLIDER_CONTROL, -} from '@kbn/controls-plugin/common'; +import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -49,12 +45,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }; - const replaceWithTimeSlider = async (controlId: string) => { - await changeFieldType(controlId, '@timestamp', TIME_SLIDER_CONTROL); - await testSubjects.waitForDeleted('timeSlider-loading-spinner'); - await dashboardControls.verifyControlType(controlId, 'timeSlider'); - }; - describe('Replacing controls', async () => { let controlId: string; @@ -89,12 +79,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('with range slider', async () => { await replaceWithRangeSlider(controlId); }); - - /** Because the time slider is temporarily disabled as of https://github.com/elastic/kibana/pull/130978, - ** I simply skipped all time slider tests for now :) **/ - it.skip('with time slider', async () => { - await replaceWithTimeSlider(controlId); - }); }); describe('Replace range slider', async () => { @@ -116,35 +100,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('with options list', async () => { await replaceWithOptionsList(controlId); }); - - it.skip('with time slider', async () => { - await replaceWithTimeSlider(controlId); - }); - }); - - describe.skip('Replace time slider', async () => { - beforeEach(async () => { - await dashboardControls.clearAllControls(); - await dashboardControls.createControl({ - controlType: TIME_SLIDER_CONTROL, - dataViewTitle: 'animals-*', - fieldName: '@timestamp', - }); - await testSubjects.waitForDeleted('timeSlider-loading-spinner'); - controlId = (await dashboardControls.getAllControlIds())[0]; - }); - - afterEach(async () => { - await dashboard.clearUnsavedChanges(); - }); - - it('with options list', async () => { - await replaceWithOptionsList(controlId); - }); - - it('with range slider', async () => { - await replaceWithRangeSlider(controlId); - }); }); }); } diff --git a/test/functional/apps/visualize/group1/index.ts b/test/functional/apps/visualize/group1/index.ts index aee4595d8f0a0c7..7aa4f5517fd3084 100644 --- a/test/functional/apps/visualize/group1/index.ts +++ b/test/functional/apps/visualize/group1/index.ts @@ -12,12 +12,13 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('visualize app - group1', () => { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); diff --git a/test/functional/apps/visualize/group2/_histogram_request_start.ts b/test/functional/apps/visualize/group2/_histogram_request_start.ts index a12474d9ebc2ee6..d01f7ee95f4871e 100644 --- a/test/functional/apps/visualize/group2/_histogram_request_start.ts +++ b/test/functional/apps/visualize/group2/_histogram_request_start.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); - + const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects([ 'common', 'visualize', @@ -26,7 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('histogram agg onSearchRequestStart', function () { before(async function () { // loading back default data - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); diff --git a/test/functional/apps/visualize/group2/index.ts b/test/functional/apps/visualize/group2/index.ts index ea5ad24e2f87338..4925c3ef73598c4 100644 --- a/test/functional/apps/visualize/group2/index.ts +++ b/test/functional/apps/visualize/group2/index.ts @@ -12,12 +12,13 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('visualize app', () => { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); diff --git a/test/functional/apps/visualize/group3/index.ts b/test/functional/apps/visualize/group3/index.ts index e1f86ca1798a611..4ce3def5c52e121 100644 --- a/test/functional/apps/visualize/group3/index.ts +++ b/test/functional/apps/visualize/group3/index.ts @@ -18,7 +18,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); diff --git a/test/functional/apps/visualize/group4/index.ts b/test/functional/apps/visualize/group4/index.ts index 347648970641525..00270952245709c 100644 --- a/test/functional/apps/visualize/group4/index.ts +++ b/test/functional/apps/visualize/group4/index.ts @@ -12,12 +12,13 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('visualize app', () => { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); diff --git a/test/functional/apps/visualize/group5/index.ts b/test/functional/apps/visualize/group5/index.ts index eafa39962ff568a..cabdda79c207b12 100644 --- a/test/functional/apps/visualize/group5/index.ts +++ b/test/functional/apps/visualize/group5/index.ts @@ -12,12 +12,13 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('visualize app', () => { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); diff --git a/test/functional/apps/visualize/group6/index.ts b/test/functional/apps/visualize/group6/index.ts index 05fe3b232d37084..03027c7a31a8ade 100644 --- a/test/functional/apps/visualize/group6/index.ts +++ b/test/functional/apps/visualize/group6/index.ts @@ -12,12 +12,13 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('visualize app', () => { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts index d7f674753058d83..8ee970693b7a924 100644 --- a/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts @@ -19,7 +19,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index c7e786f4b123a47..efffdaf1e77823d 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -18,8 +18,8 @@ const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_P const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; const COPY_PANEL_TO_DATA_TEST_SUBJ = 'embeddablePanelAction-copyToDashboard'; -const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'; const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary'; +const UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-unlinkFromLibrary'; export class DashboardPanelActionsService extends FtrService { private readonly log = this.ctx.getService('log'); @@ -190,11 +190,12 @@ export class DashboardPanelActionsService extends FtrService { async unlinkFromLibary(parent?: WebElementWrapper) { this.log.debug('unlinkFromLibrary'); - const libraryNotification = parent - ? await this.testSubjects.findDescendant(LIBRARY_NOTIFICATION_TEST_SUBJ, parent) - : await this.testSubjects.find(LIBRARY_NOTIFICATION_TEST_SUBJ); - await libraryNotification.click(); - await this.testSubjects.click('libraryNotificationUnlinkButton'); + await this.openContextMenu(parent); + const exists = await this.testSubjects.exists(UNLINK_FROM_LIBRARY_TEST_SUBJ); + if (!exists) { + await this.clickContextMenuMoreItem(); + } + await this.testSubjects.click(UNLINK_FROM_LIBRARY_TEST_SUBJ); } async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress.json b/x-pack/plugins/apm/ftr_e2e/cypress.json index 1791baaa5aae49b..848a10efed66820 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress.json +++ b/x-pack/plugins/apm/ftr_e2e/cypress.json @@ -6,11 +6,14 @@ "screenshotsFolder": "./cypress/screenshots", "supportFile": "./cypress/support/index.ts", "videosFolder": "./cypress/videos", + "requestTimeout": 10000, + "responseTimeout": 40000, "defaultCommandTimeout": 30000, "execTimeout": 120000, "pageLoadTimeout": 120000, "viewportHeight": 900, "viewportWidth": 1440, "video": false, - "screenshotOnRunFailure": false + "screenshotOnRunFailure": false, + "experimentalSessionAndOrigin": true } diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.spec.ts index 1a58eb1ca4fda87..d1159efd0fc90ce 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.spec.ts @@ -10,9 +10,9 @@ import { opbeans } from '../../../fixtures/synthtrace/opbeans'; const start = '2021-10-10T00:00:00.000Z'; const end = '2021-10-10T00:15:00.000Z'; -describe.skip('Comparison feature flag', () => { - before(async () => { - await synthtrace.index( +describe('Comparison feature flag', () => { + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -20,29 +20,33 @@ describe.skip('Comparison feature flag', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); describe('when comparison feature is enabled', () => { beforeEach(() => { cy.loginAsEditorUser(); + + cy.updateAdvancedSettings({ + 'observability:enableComparisonByDefault': true, + }); }); it('shows the comparison feature enabled in services overview', () => { - cy.visit('/app/apm/services'); + cy.visitKibana('/app/apm/services'); cy.get('input[type="checkbox"]#comparison').should('be.checked'); cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled'); }); - it('shows the comparison feature enabled in services overview', () => { - cy.visit('/app/apm/dependencies'); + it('shows the comparison feature enabled in dependencies overview', () => { + cy.visitKibana('/app/apm/dependencies'); cy.get('input[type="checkbox"]#comparison').should('be.checked'); cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled'); }); it('shows the comparison feature disabled in service map overview page', () => { - cy.visit('/app/apm/service-map'); + cy.visitKibana('/app/apm/service-map'); cy.get('input[type="checkbox"]#comparison').should('be.checked'); cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled'); }); @@ -50,11 +54,11 @@ describe.skip('Comparison feature flag', () => { describe('when comparison feature is disabled', () => { beforeEach(() => { - cy.loginAsEditorUser().then(() => { - // Disables comparison feature on advanced settings - cy.updateAdvancedSettings({ - 'observability:enableComparisonByDefault': false, - }); + cy.loginAsEditorUser(); + + // Disables comparison feature on advanced settings + cy.updateAdvancedSettings({ + 'observability:enableComparisonByDefault': false, }); }); @@ -65,7 +69,7 @@ describe.skip('Comparison feature flag', () => { }); it('shows the comparison feature disabled in services overview', () => { - cy.visit('/app/apm/services'); + cy.visitKibana('/app/apm/services'); cy.get('input[type="checkbox"]#comparison').should('not.be.checked'); cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); }); @@ -74,14 +78,14 @@ describe.skip('Comparison feature flag', () => { cy.intercept('GET', '/internal/apm/dependencies/top_dependencies?*').as( 'topDependenciesRequest' ); - cy.visit('/app/apm/dependencies'); - cy.wait('@topDependenciesRequest', { requestTimeout: 10000 }); + cy.visitKibana('/app/apm/dependencies'); + cy.wait('@topDependenciesRequest'); cy.get('input[type="checkbox"]#comparison').should('not.be.checked'); cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); }); it('shows the comparison feature disabled in service map overview page', () => { - cy.visit('/app/apm/service-map'); + cy.visitKibana('/app/apm/service-map'); cy.get('input[type="checkbox"]#comparison').should('not.be.checked'); cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/infrastructure_page.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/infrastructure_page.spec.ts index 46d654ef2a4aa90..439b95a796b7127 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/infrastructure_page.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/infrastructure_page.spec.ts @@ -27,9 +27,9 @@ const nodeServiceInfraPageHref = url.format({ query: { rangeFrom: start, rangeTo: end }, }); -describe.skip('Infrastructure page', () => { - before(async () => { - await synthtrace.index( +describe('Infrastructure page', () => { + before(() => { + synthtrace.index( generateData({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -37,8 +37,8 @@ describe.skip('Infrastructure page', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { @@ -47,7 +47,7 @@ describe.skip('Infrastructure page', () => { describe('when data is loaded', () => { it('has no detectable a11y violations on load', () => { - cy.visit(goServiceInfraPageHref); + cy.visitKibana(goServiceInfraPageHref); cy.contains('Infrastructure'); // set skipFailures to true to not fail the test when there are accessibility failures checkA11y({ skipFailures: true }); @@ -55,7 +55,7 @@ describe.skip('Infrastructure page', () => { describe('when container ids, pod names and host names are returned by the api call', () => { it('shows all tabs', () => { - cy.visit(goServiceInfraPageHref); + cy.visitKibana(goServiceInfraPageHref); cy.contains('Containers'); cy.contains('Pods'); cy.contains('Hosts'); @@ -64,14 +64,14 @@ describe.skip('Infrastructure page', () => { describe('when only host names are returned by the api call', () => { it('shows only Hosts tab', () => { - cy.visit(javaServiceInfraPageHref); + cy.visitKibana(javaServiceInfraPageHref); cy.contains('Hosts'); }); }); describe('when none infrastructure attributes are returned by the api call', () => { it('shows no data message', () => { - cy.visit(nodeServiceInfraPageHref); + cy.visitKibana(nodeServiceInfraPageHref); cy.contains('No results match your search criteria.'); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts index 655376eb3eb6341..7b97813cb121954 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts @@ -57,7 +57,7 @@ describe.skip('when navigating to integration page', () => { const integrationsPath = '/app/integrations/browse'; cy.loginAsEditorUser(); - cy.visit(integrationsPath); + cy.visitKibana(integrationsPath); // open integration policy form cy.get('[data-test-subj="integration-card:epr:apm:featured').click(); @@ -79,17 +79,17 @@ describe.skip('when navigating to integration page', () => { }); it('should display Tail-based section on latest version', () => { - cy.visit('/app/fleet/integrations/apm/add-integration'); + cy.visitKibana('/app/fleet/integrations/apm/add-integration'); cy.contains('Tail-based sampling').should('exist'); }); it('should hide Tail-based section for 8.0.0 apm package', () => { - cy.visit('/app/fleet/integrations/apm-8.0.0/add-integration'); + cy.visitKibana('/app/fleet/integrations/apm-8.0.0/add-integration'); cy.contains('Tail-based sampling').should('not.exist'); }); it('should Display Debug section', () => { - cy.visit('/app/fleet/integrations/apm-8.0.0/add-integration'); + cy.visitKibana('/app/fleet/integrations/apm-8.0.0/add-integration'); cy.contains('Debug settings').should('exist'); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts index c7f33301a5dfc38..ae08bc1ea9b63e7 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts @@ -5,60 +5,55 @@ * 2.0. */ -const apmIndicesSaveURL = '/internal/apm/settings/apm-indices/save'; - describe('No data screen', () => { describe('bypass no data screen on settings pages', () => { - beforeEach(() => { - cy.loginAsEditorUser(); - }); - before(() => { - // Change default indices - cy.request({ - url: apmIndicesSaveURL, - method: 'POST', - body: { - sourcemap: 'foo-*', - error: 'foo-*', - onboarding: 'foo-*', - span: 'foo-*', - transaction: 'foo-*', - metric: 'foo-*', - }, - headers: { - 'kbn-xsrf': true, - }, - auth: { user: 'editor', pass: 'changeme' }, + // Change indices + setApmIndices({ + sourcemap: 'foo-*', + error: 'foo-*', + onboarding: 'foo-*', + span: 'foo-*', + transaction: 'foo-*', + metric: 'foo-*', }); }); + beforeEach(() => { + cy.loginAsEditorUser(); + }); + it('shows no data screen instead of service inventory', () => { - cy.visit('/app/apm/'); + cy.visitKibana('/app/apm/'); cy.contains('Welcome to Elastic Observability!'); }); + it('shows settings page', () => { - cy.visit('/app/apm/settings'); + cy.visitKibana('/app/apm/settings'); cy.contains('Welcome to Elastic Observability!').should('not.exist'); cy.get('h1').contains('Settings'); }); after(() => { // reset to default indices - cy.request({ - url: apmIndicesSaveURL, - method: 'POST', - body: { - sourcemap: '', - error: '', - onboarding: '', - span: '', - transaction: '', - metric: '', - }, - headers: { 'kbn-xsrf': true }, - auth: { user: 'editor', pass: 'changeme' }, + setApmIndices({ + sourcemap: '', + error: '', + onboarding: '', + span: '', + transaction: '', + metric: '', }); }); }); }); + +function setApmIndices(body: Record) { + cy.request({ + url: '/internal/apm/settings/apm-indices/save', + method: 'POST', + body, + headers: { 'kbn-xsrf': true }, + auth: { user: 'editor', pass: 'changeme' }, + }); +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts index b145369535225c9..ec01fc2df2da30e 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts @@ -6,10 +6,12 @@ */ function deleteAllRules() { + cy.log('Delete all rules'); cy.request({ log: false, method: 'GET', url: '/api/alerting/rules/_find', + auth: { user: 'editor', pass: 'changeme' }, }).then(({ body }) => { if (body.data.length > 0) { cy.log(`Deleting rules`); @@ -21,12 +23,21 @@ function deleteAllRules() { log: false, method: 'DELETE', url: `/api/alerting/rule/${id}`, + auth: { user: 'editor', pass: 'changeme' }, }); }); }); } describe('Rules', () => { + beforeEach(() => { + deleteAllRules(); + }); + + after(() => { + deleteAllRules(); + }); + describe('Error count', () => { const ruleName = 'Error count threshold'; const comboBoxInputSelector = @@ -36,18 +47,11 @@ describe('Rules', () => { describe('when created from APM', () => { describe('when created from Service Inventory', () => { - before(() => { + it('creates a rule', () => { cy.loginAsEditorUser(); - deleteAllRules(); - }); - - after(() => { - deleteAllRules(); - }); - it('creates a rule', () => { // Create a rule in APM - cy.visit('/app/apm/services'); + cy.visitKibana('/app/apm/services'); cy.contains('Alerts and rules').click(); cy.contains('Create error count rule').click(); @@ -67,18 +71,13 @@ describe('Rules', () => { }); describe('when created from Stack management', () => { - before(() => { + it('creates a rule', () => { cy.loginAsEditorUser(); - deleteAllRules(); - }); - after(() => { - deleteAllRules(); - }); - - it('creates a rule', () => { // Go to stack management - cy.visit('/app/management/insightsAndAlerting/triggersActions/rules'); + cy.visitKibana( + '/app/management/insightsAndAlerting/triggersActions/rules' + ); // Create a rule cy.contains('button', 'Create rule').click(); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts index a2f5e055e80a84a..23154492c9f4418 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts @@ -55,10 +55,10 @@ function generateData({ } describe('Agent configuration', () => { - before(async () => { + before(() => { const { rangeFrom, rangeTo } = timeRange; - await synthtrace.index( + synthtrace.index( generateData({ from: new Date(rangeFrom).getTime(), to: new Date(rangeTo).getTime(), @@ -67,13 +67,13 @@ describe('Agent configuration', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { cy.loginAsEditorUser(); - cy.visit(agentConfigHref); + cy.visitKibana(agentConfigHref); }); it('persists service enviroment when clicking on edit button', () => { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts index e5c409cb6da0d23..53dbb82c1c6ae2c 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts @@ -13,13 +13,13 @@ describe('Custom links', () => { }); it('shows empty message and create button', () => { - cy.visit(basePath); + cy.visitKibana(basePath); cy.contains('No links found'); cy.contains('Create custom link'); }); it('creates custom link', () => { - cy.visit(basePath); + cy.visitKibana(basePath); const emptyPrompt = cy.get('[data-test-subj="customLinksEmptyPrompt"]'); cy.contains('Create custom link').click(); cy.contains('Create link'); @@ -36,7 +36,7 @@ describe('Custom links', () => { }); it('clears filter values when field is selected', () => { - cy.visit(basePath); + cy.visitKibana(basePath); cy.contains('Create custom link').click(); cy.get('[data-test-subj="filter-0"]').select('service.name'); cy.get( diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts index 76c43ef03f3322f..cfcabe85b5b2a73 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts @@ -6,11 +6,11 @@ */ describe('APM deep links', () => { - before(() => { + beforeEach(() => { cy.loginAsViewerUser(); }); it('navigates to apm links on search elastic', () => { - cy.visit('/'); + cy.visitKibana('/'); cy.get('[data-test-subj="nav-search-input"]').type('APM'); cy.contains('APM'); cy.contains('APM / Services'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts index 8042a1a07f8a2e8..e3746489936ab86 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts @@ -17,8 +17,8 @@ const timeRange = { }; describe.skip('Dependencies', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -26,8 +26,8 @@ describe.skip('Dependencies', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { @@ -36,7 +36,7 @@ describe.skip('Dependencies', () => { describe('top-level dependencies page', () => { it('has a list of dependencies and you can navigate to the page for one', () => { - cy.visit(`/app/apm/services?${new URLSearchParams(timeRange)}`); + cy.visitKibana(`/app/apm/services?${new URLSearchParams(timeRange)}`); cy.contains('nav a', 'Dependencies').click(); // `force: true` because Cypress says the element is 0x0 @@ -46,7 +46,7 @@ describe.skip('Dependencies', () => { }); it('has no detectable a11y violations on load', () => { - cy.visit( + cy.visitKibana( `/app/apm/services/opbeans-java/dependencies?${new URLSearchParams( timeRange )}` @@ -59,7 +59,7 @@ describe.skip('Dependencies', () => { describe.skip('dependency overview page', () => { it('shows dependency information and you can navigate to a page for an upstream service', () => { - cy.visit( + cy.visitKibana( `/app/apm/dependencies/overview?${new URLSearchParams({ ...timeRange, dependencyName: 'postgresql', @@ -76,7 +76,7 @@ describe.skip('Dependencies', () => { }); it('has no detectable a11y violations on load', () => { - cy.visit( + cy.visitKibana( `/app/apm/dependencies/overview?${new URLSearchParams({ ...timeRange, dependencyName: 'postgresql', @@ -90,7 +90,7 @@ describe.skip('Dependencies', () => { describe('service overview page', () => { it('shows dependency information and you can navigate to a page for a dependency', () => { - cy.visit( + cy.visitKibana( `/app/apm/services/opbeans-java/overview?${new URLSearchParams( timeRange )}` @@ -104,7 +104,7 @@ describe.skip('Dependencies', () => { describe('service dependencies tab', () => { it('shows dependency information and you can navigate to a page for a dependency', () => { - cy.visit( + cy.visitKibana( `/app/apm/services/opbeans-java/overview?${new URLSearchParams( timeRange )}` diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts index 59c0d870295179e..28b3068503644be 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts @@ -27,8 +27,8 @@ describe('Error details', () => { }); describe('when data is loaded', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( generateData({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -36,12 +36,12 @@ describe('Error details', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); it('has no detectable a11y violations on load', () => { - cy.visit(errorDetailsPageHref); + cy.visitKibana(errorDetailsPageHref); cy.contains('Error group 00000'); // set skipFailures to true to not fail the test when there are accessibility failures checkA11y({ skipFailures: true }); @@ -49,7 +49,7 @@ describe('Error details', () => { describe('when error has no occurrences', () => { it('shows an empty message', () => { - cy.visit( + cy.visitKibana( url.format({ pathname: '/app/apm/services/opbeans-java/errors/0000000000000000000000000Error%201', @@ -66,13 +66,13 @@ describe('Error details', () => { describe('when error has data', () => { it('shows errors distribution chart', () => { - cy.visit(errorDetailsPageHref); + cy.visitKibana(errorDetailsPageHref); cy.contains('Error group 00000'); cy.get('[data-test-subj="errorDistribution"]').contains('Occurrences'); }); it('shows top erroneous transactions table', () => { - cy.visit(errorDetailsPageHref); + cy.visitKibana(errorDetailsPageHref); cy.contains('Top 5 affected transactions'); cy.get('[data-test-subj="topErroneousTransactionsTable"]') .contains('a', 'GET /apple 🍎') @@ -81,14 +81,14 @@ describe('Error details', () => { }); it('shows a Stacktrace and Metadata tabs', () => { - cy.visit(errorDetailsPageHref); + cy.visitKibana(errorDetailsPageHref); cy.contains('button', 'Exception stack trace'); cy.contains('button', 'Metadata'); }); describe('when clicking on related transaction sample', () => { it('should redirects to the transaction details page', () => { - cy.visit(errorDetailsPageHref); + cy.visitKibana(errorDetailsPageHref); cy.contains('Error group 00000'); cy.contains('a', 'GET /apple 🍎').click(); cy.url().should('include', 'opbeans-java/transactions/view'); @@ -97,7 +97,7 @@ describe('Error details', () => { describe('when clicking on View x occurences in discover', () => { it.skip('should redirects the user to discover', () => { - cy.visit(errorDetailsPageHref); + cy.visitKibana(errorDetailsPageHref); cy.contains('View 1 occurrence in Discover').click(); cy.url().should('include', 'app/discover'); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts index b000a3a8c2f3f49..301b3384ee2eb86 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts @@ -29,8 +29,8 @@ describe('Errors page', () => { }); describe('when data is loaded', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( generateData({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -38,12 +38,12 @@ describe('Errors page', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); it('has no detectable a11y violations on load', () => { - cy.visit(javaServiceErrorsPageHref); + cy.visitKibana(javaServiceErrorsPageHref); cy.contains('Error occurrences'); // set skipFailures to true to not fail the test when there are accessibility failures checkA11y({ skipFailures: true }); @@ -51,7 +51,7 @@ describe('Errors page', () => { describe('when service has no errors', () => { it('shows empty message', () => { - cy.visit(nodeServiceErrorsPageHref); + cy.visitKibana(nodeServiceErrorsPageHref); cy.contains('opbeans-node'); cy.contains('No errors found'); }); @@ -59,28 +59,28 @@ describe('Errors page', () => { describe('when service has errors', () => { it('shows errors distribution chart', () => { - cy.visit(javaServiceErrorsPageHref); + cy.visitKibana(javaServiceErrorsPageHref); cy.contains('Error occurrences'); }); it('shows failed transaction rate chart', () => { - cy.visit(javaServiceErrorsPageHref); + cy.visitKibana(javaServiceErrorsPageHref); cy.contains('Failed transaction rate'); }); it('errors table is populated', () => { - cy.visit(javaServiceErrorsPageHref); + cy.visitKibana(javaServiceErrorsPageHref); cy.contains('Error 0'); }); it('clicking on an error in the list navigates to error detail page', () => { - cy.visit(javaServiceErrorsPageHref); + cy.visitKibana(javaServiceErrorsPageHref); cy.contains('a', 'Error 1').click(); cy.contains('div', 'Error 1'); }); it('clicking on type adds a filter in the kuerybar', () => { - cy.visit(javaServiceErrorsPageHref); + cy.visitKibana(javaServiceErrorsPageHref); cy.get('[data-test-subj="headerFilterKuerybar"]') .invoke('val') .should('be.empty'); @@ -97,13 +97,13 @@ describe('Errors page', () => { }); it('sorts by ocurrences', () => { - cy.visit(javaServiceErrorsPageHref); + cy.visitKibana(javaServiceErrorsPageHref); cy.contains('span', 'Occurrences').click(); cy.url().should('include', '&sortField=occurrences&sortDirection=asc'); }); it('sorts by latest occurrences', () => { - cy.visit(javaServiceErrorsPageHref); + cy.visitKibana(javaServiceErrorsPageHref); cy.contains('span', 'Last seen').click(); cy.url().should('include', '&sortField=lastSeen&sortDirection=asc'); }); @@ -112,9 +112,8 @@ describe('Errors page', () => { }); describe('Check detailed statistics API with multiple errors', () => { - before(async () => { - cy.loginAsViewerUser(); - await synthtrace.index( + before(() => { + synthtrace.index( generateErrors({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -123,8 +122,12 @@ describe('Check detailed statistics API with multiple errors', () => { ); }); - after(async () => { - await synthtrace.clean(); + beforeEach(() => { + cy.loginAsViewerUser(); + }); + + after(() => { + synthtrace.clean(); }); it('calls detailed API with visible items only', () => { @@ -136,7 +139,7 @@ describe('Check detailed statistics API with multiple errors', () => { 'POST', '/internal/apm/services/opbeans-java/errors/groups/detailed_statistics?*' ).as('errorsDetailedStatistics'); - cy.visit(`${javaServiceErrorsPageHref}&pageSize=10`); + cy.visitKibana(`${javaServiceErrorsPageHref}&pageSize=10`); cy.wait('@errorsMainStatistics'); cy.get('.euiPagination__list').children().should('have.length', 5); cy.wait('@errorsDetailedStatistics').then((payload) => { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index 42ca9319b4ea341..be09ae3f06122ab 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -24,8 +24,8 @@ const serviceInventoryHref = url.format({ }); describe.skip('Home page', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -33,8 +33,8 @@ describe.skip('Home page', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { @@ -42,7 +42,7 @@ describe.skip('Home page', () => { }); it('Redirects to service page with comparisonEnabled, environment, rangeFrom, rangeTo and offset added to the URL', () => { - cy.visit('/app/apm'); + cy.visitKibana('/app/apm'); cy.url().should( 'include', @@ -51,7 +51,7 @@ describe.skip('Home page', () => { }); it('includes services with only metric documents', () => { - cy.visit( + cy.visitKibana( `${serviceInventoryHref}&kuery=not%20(processor.event%3A%22transaction%22)` ); cy.contains('opbeans-java'); @@ -60,7 +60,7 @@ describe.skip('Home page', () => { describe('navigations', () => { it('navigates to service overview page with transaction type', () => { - cy.visit(serviceInventoryHref); + cy.visitKibana(serviceInventoryHref); cy.contains('Services'); cy.contains('opbeans-rum').click({ force: true }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts index 03972eb0d0f207f..c4e87ac15fbe194 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts @@ -20,8 +20,8 @@ const specialServiceName = 'service 1 / ? # [ ] @ ! $ & ( ) * + , ; = < > % {} | ^ ` <>'; describe('Service inventory - header filters', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( generateData({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -30,8 +30,8 @@ describe('Service inventory - header filters', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { @@ -40,7 +40,7 @@ describe('Service inventory - header filters', () => { describe('Filtering by kuerybar', () => { it('filters by service.name with special characters', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('Services'); cy.contains('opbeans-node'); cy.contains('service 1'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts index 6575d015abde2ec..5b98284cdf52a1e 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts @@ -44,12 +44,9 @@ const mainAliasNames = mainApiRequestsToIntercept.map( ); describe('When navigating to the service inventory', () => { - before(async () => { - cy.loginAsViewerUser(); - cy.visit(serviceInventoryHref); - + before(() => { const { rangeFrom, rangeTo } = timeRange; - await synthtrace.index( + synthtrace.index( opbeans({ from: new Date(rangeFrom).getTime(), to: new Date(rangeTo).getTime(), @@ -57,8 +54,13 @@ describe('When navigating to the service inventory', () => { ); }); - after(async () => { - await synthtrace.clean(); + beforeEach(() => { + cy.loginAsViewerUser(); + cy.visitKibana(serviceInventoryHref); + }); + + after(() => { + synthtrace.clean(); }); it('has no detectable a11y violations on load', () => { @@ -92,7 +94,7 @@ describe('When navigating to the service inventory', () => { ); cy.loginAsViewerUser(); - cy.visit(serviceInventoryHref); + cy.visitKibana(serviceInventoryHref); }); it('with the correct environment when changing the environment', () => { @@ -135,11 +137,9 @@ describe('When navigating to the service inventory', () => { }); describe('Check detailed statistics API with multiple services', () => { - before(async () => { - cy.loginAsViewerUser(); + before(() => { const { rangeFrom, rangeTo } = timeRange; - - await synthtrace.index( + synthtrace.index( generateMultipleServicesData({ from: new Date(rangeFrom).getTime(), to: new Date(rangeTo).getTime(), @@ -147,8 +147,12 @@ describe('Check detailed statistics API with multiple services', () => { ); }); - after(async () => { - await synthtrace.clean(); + beforeEach(() => { + cy.loginAsViewerUser(); + }); + + after(() => { + synthtrace.clean(); }); it('calls detailed API with visible items only', () => { @@ -157,7 +161,7 @@ describe('Check detailed statistics API with multiple services', () => { ); cy.intercept('GET', '/internal/apm/services?*').as('mainStatisticsRequest'); - cy.visit( + cy.visitKibana( `${serviceInventoryHref}&pageSize=10&sortField=serviceName&sortDirection=asc` ); cy.wait('@mainStatisticsRequest'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts index a2674d056e317cd..a70620ee18f829b 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts @@ -23,8 +23,8 @@ const apiToIntercept = { }; describe('Service overview - aws lambda', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( generateData({ start: new Date(start).getTime(), end: new Date(end).getTime(), @@ -32,19 +32,16 @@ describe('Service overview - aws lambda', () => { ); }); - after(async () => { - await synthtrace.clean(); - }); - - beforeEach(() => { - cy.loginAsViewerUser(); + after(() => { + synthtrace.clean(); }); it('displays a cold start rate chart and not a transaction breakdown chart', () => { const { endpoint, name } = apiToIntercept; - cy.intercept('GET', endpoint).as(name); - cy.visit(serviceOverviewHref); + + cy.loginAsViewerUser(); + cy.visitKibana(serviceOverviewHref); cy.wait(`@${name}`); cy.contains('Cold start rate'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.ts index 8d7cfd4e65288ae..b175eb0430ed4c0 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.ts @@ -18,8 +18,8 @@ const serviceOverviewHref = url.format({ }); describe('Errors table', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -27,8 +27,8 @@ describe('Errors table', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { @@ -36,20 +36,20 @@ describe('Errors table', () => { }); it('errors table is populated', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-java'); cy.contains('[MockError] Foo'); }); it('navigates to the errors page', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-java'); cy.contains('a', 'View errors').click(); cy.url().should('include', '/opbeans-java/errors'); }); it('clicking on type adds a filter in the kuerybar and navigates to errors page', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.get('[data-test-subj="headerFilterKuerybar"]') .invoke('val') .should('be.empty'); @@ -64,7 +64,7 @@ describe('Errors table', () => { }); it('navigates to error detail page', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('a', '[MockError] Foo').click(); cy.contains('div', 'Exception message'); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts index 3b7b0c87a7f846e..cf5102ee6e37e2c 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts @@ -59,14 +59,14 @@ const apisToIntercept = [ ]; describe('Service overview - header filters', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime() }) ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); describe('Filtering by transaction type', () => { @@ -74,7 +74,7 @@ describe('Service overview - header filters', () => { cy.loginAsViewerUser(); }); it('changes url when selecting different value', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-node'); cy.url().should('not.include', 'transactionType'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( @@ -93,7 +93,7 @@ describe('Service overview - header filters', () => { apisToIntercept.map(({ endpoint, name }) => { cy.intercept('GET', endpoint).as(name); }); - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' @@ -122,7 +122,7 @@ describe('Service overview - header filters', () => { cy.loginAsViewerUser(); }); it('filters by transaction.name', () => { - cy.visit( + cy.visitKibana( url.format({ pathname: '/app/apm/services/opbeans-java/overview', query: { rangeFrom: start, rangeTo: end }, diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts index 1fc83b3e2eb0acb..c3b5f37c32acc77 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts @@ -41,19 +41,19 @@ describe('Instances table', () => { cy.loginAsViewerUser(); }); - // describe('when data is not loaded', () => { - // it('shows empty message', () => { - // cy.visit(serviceOverviewHref); - // cy.contains('opbeans-java'); - // cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains( - // 'No items found' - // ); - // }); - // }); + describe.skip('when data is not loaded', () => { + it('shows empty message', () => { + cy.visitKibana(serviceOverviewHref); + cy.contains('opbeans-java'); + cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains( + 'No items found' + ); + }); + }); describe('when data is loaded', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -61,12 +61,12 @@ describe('Instances table', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); it('has data in the table', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-java'); cy.contains(serviceNodeName); }); @@ -75,7 +75,7 @@ describe('Instances table', () => { cy.intercept('GET', endpoint).as(name); }); - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-java'); cy.wait('@instancesMainRequest'); @@ -96,7 +96,7 @@ describe('Instances table', () => { cy.intercept('GET', endpoint).as(name); }); - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-java'); cy.wait('@instancesMainRequest'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index f61ad0c0761c67d..d73713290b4eff5 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -86,8 +86,8 @@ const aliasNamesWithComparison = apiRequestsToInterceptWithComparison.map( const aliasNames = [...aliasNamesNoComparison, ...aliasNamesWithComparison]; describe('Service Overview', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -95,14 +95,14 @@ describe('Service Overview', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); describe('renders', () => { - before(() => { + beforeEach(() => { cy.loginAsViewerUser(); - cy.visit(baseUrl); + cy.visitKibana(baseUrl); }); it('renders all components on the page', () => { @@ -122,7 +122,6 @@ describe('Service Overview', () => { describe('transactions', () => { beforeEach(() => { cy.loginAsViewerUser(); - cy.visit(baseUrl); }); it('persists transaction type selected when clicking on Transactions tab', () => { @@ -130,6 +129,9 @@ describe('Service Overview', () => { 'GET', '/internal/apm/services/opbeans-node/transaction_types?*' ).as('transactionTypesRequest'); + + cy.visitKibana(baseUrl); + cy.wait('@transactionTypesRequest'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( @@ -148,11 +150,14 @@ describe('Service Overview', () => { ); }); - it.skip('persists transaction type selected when clicking on View Transactions link', () => { + it('persists transaction type selected when clicking on View Transactions link', () => { cy.intercept( 'GET', '/internal/apm/services/opbeans-node/transaction_types?*' ).as('transactionTypesRequest'); + + cy.visitKibana(baseUrl); + cy.wait('@transactionTypesRequest'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', @@ -173,20 +178,20 @@ describe('Service Overview', () => { }); describe('when RUM service', () => { - before(() => { + it('hides dependency tab when RUM service', () => { cy.loginAsViewerUser(); - cy.visit( + + cy.intercept('GET', '/internal/apm/services/opbeans-rum/agent?*').as( + 'agentRequest' + ); + + cy.visitKibana( url.format({ pathname: '/app/apm/services/opbeans-rum/overview', query: { rangeFrom: start, rangeTo: end }, }) ); - }); - it('hides dependency tab when RUM service', () => { - cy.intercept('GET', '/internal/apm/services/opbeans-rum/agent?*').as( - 'agentRequest' - ); cy.contains('Overview'); cy.contains('Transactions'); cy.contains('Error'); @@ -204,17 +209,18 @@ describe('Service Overview', () => { describe('Calls APIs', () => { beforeEach(() => { cy.loginAsViewerUser(); - cy.visit(baseUrl); + apiRequestsToIntercept.map(({ endpoint, aliasName }) => { cy.intercept('GET', endpoint).as(aliasName); }); apiRequestsToInterceptWithComparison.map(({ endpoint, aliasName }) => { cy.intercept('GET', endpoint).as(aliasName); }); + cy.visitKibana(baseUrl); }); it.skip('with the correct environment when changing the environment', () => { - cy.wait(aliasNames, { requestTimeout: 10000 }); + cy.wait(aliasNames); cy.intercept('GET', 'internal/apm/suggestions?*').as( 'suggestionsRequest' @@ -241,11 +247,11 @@ describe('Service Overview', () => { it('when clicking the refresh button', () => { cy.contains('Refresh').click(); - cy.wait(aliasNames, { requestTimeout: 10000 }); + cy.wait(aliasNames); }); it.skip('when selecting a different time range and clicking the update button', () => { - cy.wait(aliasNames, { requestTimeout: 10000 }); + cy.wait(aliasNames); const timeStart = moment(start).subtract(5, 'm').toISOString(); const timeEnd = moment(end).subtract(5, 'm').toISOString(); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts index 25591cd358bd541..611455163eca3a0 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts @@ -51,8 +51,8 @@ const apisToIntercept = [ ]; describe.skip('Service overview: Time Comparison', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -60,8 +60,8 @@ describe.skip('Service overview: Time Comparison', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { @@ -69,14 +69,14 @@ describe.skip('Service overview: Time Comparison', () => { }); it('enables by default the time comparison feature with Last 24 hours selected', () => { - cy.visit(serviceOverviewPath); + cy.visitKibana(serviceOverviewPath); cy.url().should('include', 'comparisonEnabled=true'); cy.url().should('include', 'offset=1d'); }); describe('when comparison is toggled off', () => { it('disables select box', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-java'); // Comparison is enabled by default @@ -91,7 +91,7 @@ describe.skip('Service overview: Time Comparison', () => { apisToIntercept.map(({ endpoint, name }) => { cy.intercept('GET', endpoint).as(name); }); - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.get('[data-test-subj="comparisonSelect"]').should('be.enabled'); const offset = `offset=1d`; @@ -125,7 +125,7 @@ describe.skip('Service overview: Time Comparison', () => { apisToIntercept.map(({ endpoint, name }) => { cy.intercept('GET', endpoint).as(name); }); - cy.visit(serviceOverviewPath); + cy.visitKibana(serviceOverviewPath); cy.contains('opbeans-java'); // opens the page with "Day before" selected cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d'); @@ -136,7 +136,7 @@ describe.skip('Service overview: Time Comparison', () => { }); it('changes comparison type when a new time range is selected', () => { - cy.visit(serviceOverviewHref); + cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-java'); // Time comparison default value cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d'); @@ -189,7 +189,7 @@ describe.skip('Service overview: Time Comparison', () => { apisToIntercept.map(({ endpoint, name }) => { cy.intercept('GET', endpoint).as(name); }); - cy.visit( + cy.visitKibana( url.format({ pathname: serviceOverviewPath, query: { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts index 0ced34d8a1b7c95..9fd2cfe6eab0b43 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts @@ -296,7 +296,7 @@ function getConsumerMultiple({ * ----Span E * ------span.links= producer-external-only / Span B | producer-consumer / Transaction C */ -export async function generateSpanLinksData() { +export function generateSpanLinksData() { const producerInternalOnly = getProducerInternalOnly(); const producerExternalOnly = getProducerExternalOnly(); const producerConsumer = getProducerConsumer({ @@ -309,7 +309,7 @@ export async function generateSpanLinksData() { producerExternalOnlySpanBSpanLink: producerExternalOnly.spanBSpanLink, }); - await synthtrace.index( + synthtrace.index( new EntityArrayIterable(producerInternalOnly.apmFields).merge( new EntityArrayIterable(producerExternalOnly.apmFields), new EntityArrayIterable(producerConsumer.apmFields), diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts index 3dff8f075f187c7..cddba048e8a1808 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts @@ -35,17 +35,17 @@ describe('Span links', () => { describe('when data is loaded', () => { let ids: Awaited>; - before(async () => { - ids = await generateSpanLinksData(); + before(() => { + ids = generateSpanLinksData(); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); describe('span links count on trace waterfall', () => { it('Shows two children and no parents on producer-internal-only Span A', () => { - cy.visit( + cy.visitKibana( getServiceInventoryUrl({ serviceName: 'producer-internal-only' }) ); cy.contains('Transaction A').click(); @@ -59,7 +59,7 @@ describe('Span links', () => { }); it('Shows one parent and one children on producer-external-only Span B', () => { - cy.visit( + cy.visitKibana( getServiceInventoryUrl({ serviceName: 'producer-external-only' }) ); cy.contains('Transaction B').click(); @@ -73,7 +73,9 @@ describe('Span links', () => { }); it('Shows one parent and one children on producer-consumer Transaction C', () => { - cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' })); + cy.visitKibana( + getServiceInventoryUrl({ serviceName: 'producer-consumer' }) + ); cy.contains('Transaction C').click(); cy.contains('2 Span links'); cy.get( @@ -85,7 +87,9 @@ describe('Span links', () => { }); it('Shows no parent and one children on producer-consumer Span C', () => { - cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' })); + cy.visitKibana( + getServiceInventoryUrl({ serviceName: 'producer-consumer' }) + ); cy.contains('Transaction C').click(); cy.contains('1 Span link'); cy.get( @@ -97,7 +101,9 @@ describe('Span links', () => { }); it('Shows two parents and one children on consumer-multiple Transaction D', () => { - cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' })); + cy.visitKibana( + getServiceInventoryUrl({ serviceName: 'consumer-multiple' }) + ); cy.contains('Transaction D').click(); cy.contains('2 Span links'); cy.get( @@ -109,7 +115,9 @@ describe('Span links', () => { }); it('Shows two parents and one children on consumer-multiple Span E', () => { - cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' })); + cy.visitKibana( + getServiceInventoryUrl({ serviceName: 'consumer-multiple' }) + ); cy.contains('Transaction D').click(); cy.contains('2 Span links'); cy.get( @@ -123,7 +131,7 @@ describe('Span links', () => { describe('span link flyout', () => { it('Shows children details on producer-internal-only Span A', () => { - cy.visit( + cy.visitKibana( getServiceInventoryUrl({ serviceName: 'producer-internal-only' }) ); cy.contains('Transaction A').click(); @@ -154,7 +162,7 @@ describe('Span links', () => { }); it('Shows children and parents details on producer-external-only Span B', () => { - cy.visit( + cy.visitKibana( getServiceInventoryUrl({ serviceName: 'producer-external-only' }) ); cy.contains('Transaction B').click(); @@ -178,7 +186,9 @@ describe('Span links', () => { }); it('Shows children and parents details on producer-consumer Transaction C', () => { - cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' })); + cy.visitKibana( + getServiceInventoryUrl({ serviceName: 'producer-consumer' }) + ); cy.contains('Transaction C').click(); cy.get( `[aria-controls="${ids.producerConsumerIds.transactionCId}"]` @@ -210,7 +220,9 @@ describe('Span links', () => { }); it('Shows children and parents details on producer-consumer Span C', () => { - cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' })); + cy.visitKibana( + getServiceInventoryUrl({ serviceName: 'producer-consumer' }) + ); cy.contains('Transaction C').click(); cy.contains('Span C').click(); cy.get('[data-test-subj="spanLinksTab"]').click(); @@ -232,7 +244,9 @@ describe('Span links', () => { }); it('Shows children and parents details on consumer-multiple Transaction D', () => { - cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' })); + cy.visitKibana( + getServiceInventoryUrl({ serviceName: 'consumer-multiple' }) + ); cy.contains('Transaction D').click(); cy.get( `[aria-controls="${ids.producerMultipleIds.transactionDId}"]` @@ -266,7 +280,9 @@ describe('Span links', () => { }); it('Shows children and parents details on consumer-multiple Span E', () => { - cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' })); + cy.visitKibana( + getServiceInventoryUrl({ serviceName: 'consumer-multiple' }) + ); cy.contains('Transaction D').click(); cy.contains('Span E').click(); cy.get('[data-test-subj="spanLinksTab"]').click(); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/transaction_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/transaction_details.spec.ts index c5eeda6645ce810..ee2aade9ec1dff2 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/transaction_details.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/transaction_details.spec.ts @@ -17,8 +17,8 @@ const timeRange = { }; describe('Transaction details', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -26,13 +26,13 @@ describe('Transaction details', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { cy.loginAsViewerUser(); - cy.visit( + cy.visitKibana( `/app/apm/services/opbeans-java/transactions/view?${new URLSearchParams({ ...timeRange, transactionName: 'GET /api/product', diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts index 671b35741f91b38..83753b7fe25954a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts @@ -19,8 +19,8 @@ const serviceTransactionsHref = url.format({ }); describe('Transactions Overview', () => { - before(async () => { - await synthtrace.index( + before(() => { + synthtrace.index( opbeans({ from: new Date(start).getTime(), to: new Date(end).getTime(), @@ -28,8 +28,8 @@ describe('Transactions Overview', () => { ); }); - after(async () => { - await synthtrace.clean(); + after(() => { + synthtrace.clean(); }); beforeEach(() => { @@ -37,7 +37,7 @@ describe('Transactions Overview', () => { }); it('has no detectable a11y violations on load', () => { - cy.visit(serviceTransactionsHref); + cy.visitKibana(serviceTransactionsHref); cy.get('a:contains(Transactions)').should( 'have.attr', 'aria-selected', @@ -48,7 +48,7 @@ describe('Transactions Overview', () => { }); it('persists transaction type selected when navigating to Overview tab', () => { - cy.visit(serviceTransactionsHref); + cy.visitKibana(serviceTransactionsHref); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts index e2ebe9c2bdda812..cb8d9218b4ec1e2 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts @@ -6,9 +6,9 @@ */ describe('APM tutorial', () => { - before(() => { + beforeEach(() => { cy.loginAsViewerUser(); - cy.visit('/app/home#/tutorial/apm'); + cy.visitKibana('/app/home#/tutorial/apm'); }); it('includes section for APM Server', () => { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 0559b07b1cbf29d..37182e328ebf37b 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -21,21 +21,24 @@ Cypress.Commands.add('loginAsEditorUser', () => { Cypress.Commands.add( 'loginAs', ({ username, password }: { username: string; password: string }) => { - cy.log(`Logging in as ${username}`); - const kibanaUrl = Cypress.env('KIBANA_URL'); - return cy.request({ - log: false, - method: 'POST', - url: `${kibanaUrl}/internal/security/login`, - body: { - providerType: 'basic', - providerName: 'basic', - currentURL: `${kibanaUrl}/login`, - params: { username, password }, - }, - headers: { - 'kbn-xsrf': 'e2e_test', - }, + cy.log(`Calling 'loginAs'`); + cy.session([username, password], () => { + cy.log(`Logging in as ${username}`); + const kibanaUrl = Cypress.env('KIBANA_URL'); + cy.request({ + log: false, + method: 'POST', + url: `${kibanaUrl}/internal/security/login`, + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: `${kibanaUrl}/login`, + params: { username, password }, + }, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + }); }); } ); @@ -45,6 +48,14 @@ Cypress.Commands.add('changeTimeRange', (value: string) => { cy.contains(value).click(); }); +Cypress.Commands.add('visitKibana', (url: string) => { + cy.visit(url); + cy.get('[data-test-subj="kbnLoadingMessage"]').should('exist'); + cy.get('[data-test-subj="kbnLoadingMessage"]').should('not.exist', { + timeout: 50000, + }); +}); + Cypress.Commands.add( 'selectAbsoluteTimeRange', (start: string, end: string) => { @@ -96,6 +107,7 @@ Cypress.Commands.add( headers: { 'kbn-xsrf': 'e2e_test', }, + auth: { user: 'editor', pass: 'changeme' }, }); } ); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts index f46a54142de0ea8..27720210b668aac 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts @@ -14,6 +14,7 @@ declare namespace Cypress { password: string; }): Cypress.Chainable>; changeTimeRange(value: string): void; + visitKibana(url: string): void; selectAbsoluteTimeRange(start: string, end: string): void; expectAPIsToHaveBeenCalledWith(params: { apisIntercepted: string[]; diff --git a/x-pack/plugins/apm/ftr_e2e/synthtrace.ts b/x-pack/plugins/apm/ftr_e2e/synthtrace.ts index 6fb880c40b0cc0a..a421edea04a6e91 100644 --- a/x-pack/plugins/apm/ftr_e2e/synthtrace.ts +++ b/x-pack/plugins/apm/ftr_e2e/synthtrace.ts @@ -8,11 +8,6 @@ import type { EntityIterable } from '@kbn/apm-synthtrace'; export const synthtrace = { index: (events: EntityIterable) => - new Promise((resolve) => { - cy.task('synthtrace:index', events.toArray()).then(resolve); - }), - clean: () => - new Promise((resolve) => { - cy.task('synthtrace:clean').then(resolve); - }), + cy.task('synthtrace:index', events.toArray()), + clean: () => cy.task('synthtrace:clean'), }; diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts index b57628a3f326b74..1843affb9bfc327 100644 --- a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts +++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts @@ -54,7 +54,7 @@ describe('getDeprecations', () => { get: () => ({ id: 'foo', - package_policies: [''], + package_policies: [{ package: { name: 'system' } }], } as AgentPolicy), }, }), diff --git a/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts b/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts index 42e11f08d9d80e7..9ef90e4175a6c02 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts @@ -58,7 +58,10 @@ export const getCspAgentPolicies = async ( packagePolicies: PackagePolicy[], agentPolicyService: AgentPolicyServiceInterface ): Promise => - agentPolicyService.getByIds(soClient, uniq(map(packagePolicies, 'policy_id'))); + agentPolicyService.getByIds(soClient, uniq(map(packagePolicies, 'policy_id')), { + withPackagePolicies: true, + ignoreMissing: true, + }); export const getCspPackagePolicies = ( soClient: SavedObjectsClientContract, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index 8ffeb38b6b57619..4bc00f5e0209d39 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -111,11 +111,12 @@ const createBenchmarks = ( ); return Promise.all( agentPolicies.flatMap((agentPolicy) => { - const cspPackagesOnAgent = agentPolicy.package_policies - .map((pckPolicyId) => { - if (typeof pckPolicyId === 'string') return cspPackagePoliciesMap.get(pckPolicyId); - }) - .filter(isNonNullable); + const cspPackagesOnAgent = + agentPolicy.package_policies + ?.map(({ id: pckPolicyId }) => { + return cspPackagePoliciesMap.get(pckPolicyId); + }) + .filter(isNonNullable) ?? []; const benchmarks = cspPackagesOnAgent.map(async (cspPackage) => { const cspRulesStatus = await addPackagePolicyCspRules(soClient, cspPackage); const agentPolicyStatus = { diff --git a/x-pack/plugins/enterprise_search/common/types/indices.ts b/x-pack/plugins/enterprise_search/common/types/indices.ts index 38f4b9873eba2e1..08f4125ef8efaba 100644 --- a/x-pack/plugins/enterprise_search/common/types/indices.ts +++ b/x-pack/plugins/enterprise_search/common/types/indices.ts @@ -18,6 +18,7 @@ import { Crawler } from './crawler'; export interface ElasticsearchIndex { count: number; // Elasticsearch _count health?: HealthStatus; + hidden: boolean; name: IndexName; status?: IndicesStatsIndexMetadataState; total: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_creation_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_creation_logic.mock.ts index 6cfba782698b5c7..42e9c546d85aea3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_creation_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_creation_logic.mock.ts @@ -38,6 +38,7 @@ export const mockElasticsearchIndices: ElasticsearchIndexWithPrivileges[] = [ { count: 0, health: 'yellow', + hidden: false, status: 'open', name: 'search-my-index-1', uuid: 'ydlR_QQJTeyZP66tzQSmMQ', @@ -56,6 +57,7 @@ export const mockElasticsearchIndices: ElasticsearchIndexWithPrivileges[] = [ { count: 100, health: 'green', + hidden: false, status: 'open', name: 'my-index-2', uuid: '4dlR_QQJTe2ZP6qtzQSmMQ', @@ -74,6 +76,7 @@ export const mockElasticsearchIndices: ElasticsearchIndexWithPrivileges[] = [ { count: 100, health: 'green', + hidden: false, status: 'open', name: 'search-my-index-2', uuid: '4dlR_QQJTe2ZP6qtzQSmMQ', @@ -92,6 +95,7 @@ export const mockElasticsearchIndices: ElasticsearchIndexWithPrivileges[] = [ { count: 100, health: 'green', + hidden: false, status: 'open', name: 'alias-my-index-2', uuid: '4dlR_QQJTe2ZP6qtzQSmMQ', @@ -110,6 +114,7 @@ export const mockElasticsearchIndices: ElasticsearchIndexWithPrivileges[] = [ { count: 100, health: 'green', + hidden: false, status: 'open', name: 'index-without-read-privilege', uuid: '4dlR_QQJTe2ZP6qtzQSmMQ', @@ -128,6 +133,7 @@ export const mockElasticsearchIndices: ElasticsearchIndexWithPrivileges[] = [ { count: 100, health: 'green', + hidden: false, status: 'open', name: 'index-without-manage-privilege', uuid: '4dlR_QQJTe2ZP6qtzQSmMQ', @@ -146,6 +152,7 @@ export const mockElasticsearchIndices: ElasticsearchIndexWithPrivileges[] = [ { count: 100, health: 'green', + hidden: false, status: 'open', name: 'alias-without-manage-privilege', uuid: '4dlR_QQJTe2ZP6qtzQSmMQ', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx index 95ed3875888f4a6..b9fd23ac904f0bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx @@ -12,6 +12,7 @@ import { useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; +import { ESINDEX_QUERY_PARAMETER } from '../../../shared/constants'; import { parseQueryParams } from '../../../shared/query_params'; import { ENGINES_TITLE } from '../engines'; import { AppSearchPageTemplate } from '../layout'; @@ -25,15 +26,19 @@ import { SelectEngineType } from './select_engine_type'; export const EngineCreation: React.FC = () => { const { search } = useLocation() as Location; - const { method } = parseQueryParams(search); + const { method, ...params } = parseQueryParams(search); const { engineType, currentEngineCreationStep } = useValues(EngineCreationLogic); - const { setIngestionMethod } = useActions(EngineCreationLogic); + const { setIngestionMethod, initializeWithESIndex } = useActions(EngineCreationLogic); useEffect(() => { if (typeof method === 'string') { setIngestionMethod(method); } + const esIndexParam = params[ESINDEX_QUERY_PARAMETER]; + if (typeof esIndexParam === 'string') { + initializeWithESIndex(esIndexParam); + } }, []); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts index dbf7efa907c74ef..347846686b5cdef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts @@ -42,6 +42,7 @@ interface EngineCreationActions { setSelectedIndex(selectedIndexName: string): { selectedIndexName: string }; setEngineType(engineType: EngineType): { engineType: EngineType }; setIsAliasAllowed(isAliasAllowed: boolean): { isAliasAllowed: boolean }; + initializeWithESIndex(indexName: string): { indexName: string }; } interface EngineCreationValues { @@ -82,6 +83,7 @@ export const EngineCreationLogic = kea ({ engineType }), setCreationStep: (currentEngineCreationStep) => currentEngineCreationStep, setIsAliasAllowed: (isAliasAllowed) => ({ isAliasAllowed }), + initializeWithESIndex: (indexName) => ({ indexName }), }, reducers: { ingestionMethod: [ @@ -118,6 +120,10 @@ export const EngineCreationLogic = kea + indexName.length === 0 || indexName.startsWith('search-') + ? '' + : `search-${indexName}-alias`, }, ], isAliasAllowed: [ @@ -145,18 +151,21 @@ export const EngineCreationLogic = kea selectedIndexName, onSubmitError: () => '', + initializeWithESIndex: (_, { indexName }) => indexName, }, ], engineType: [ 'appSearch', { setEngineType: (_, { engineType }) => engineType, + initializeWithESIndex: () => 'elasticsearch', }, ], currentEngineCreationStep: [ EngineCreationSteps.SelectStep, { setCreationStep: (_, currentEngineCreationStep) => currentEngineCreationStep, + initializeWithESIndex: () => EngineCreationSteps.ConfigureStep, }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts index ba72c8ada0dd1c8..e39ff051f962b76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts @@ -11,6 +11,7 @@ import { ElasticsearchIndexWithIngestion } from '../../../../common/types/indice export const indices: ElasticsearchIndexWithIngestion[] = [ { count: 1, + hidden: false, name: 'api', total: { docs: { @@ -41,6 +42,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ sync_now: false, }, count: 1, + hidden: false, name: 'connector', total: { docs: { @@ -56,6 +58,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ id: '3', index_name: 'crawler', }, + hidden: false, name: 'crawler', total: { docs: { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts index 9ade186d55380c0..bc226baa77f7fdb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts @@ -20,6 +20,7 @@ export const apiIndex: ApiViewIndex = { ingestionMethod: IngestionMethod.API, ingestionStatus: IngestionStatus.CONNECTED, lastUpdated: null, + hidden: false, name: 'api', total: { docs: { @@ -50,6 +51,7 @@ export const connectorIndex: ConnectorViewIndex = { sync_now: false, }, count: 1, + hidden: false, ingestionMethod: IngestionMethod.CONNECTOR, ingestionStatus: IngestionStatus.INCOMPLETE, lastUpdated: 'never', @@ -68,6 +70,7 @@ export const crawlerIndex: CrawlerViewIndex = { id: '3', index_name: 'crawler', }, + hidden: false, ingestionMethod: IngestionMethod.CRAWLER, ingestionStatus: IngestionStatus.INCOMPLETE, lastUpdated: null, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/create_engine_menu_item.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/create_engine_menu_item.tsx new file mode 100644 index 000000000000000..a5667327a8208aa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/create_engine_menu_item.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiBetaBadge, EuiContextMenuItem, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { APP_SEARCH_PLUGIN } from '../../../../../../../common/constants'; +import { ENGINE_CREATION_PATH } from '../../../../../app_search/routes'; +import { ESINDEX_QUERY_PARAMETER } from '../../../../../shared/constants'; +import { generateEncodedPath } from '../../../../../shared/encode_path_params'; +import { KibanaLogic } from '../../../../../shared/kibana'; + +export interface CreateEngineMenuItemProps { + indexName?: string; + isHiddenIndex?: boolean; +} + +export const CreateEngineMenuItem: React.FC = ({ + indexName, + isHiddenIndex, +}) => { + const engineCreationPath = !indexName + ? `${APP_SEARCH_PLUGIN.URL}${ENGINE_CREATION_PATH}` + : generateEncodedPath(`${APP_SEARCH_PLUGIN.URL}${ENGINE_CREATION_PATH}?:indexKey=:indexName`, { + indexKey: ESINDEX_QUERY_PARAMETER, + indexName, + }); + + return ( + + + { + KibanaLogic.values.navigateToUrl(engineCreationPath, { + shouldNotCreateHref: true, + }); + }} + disabled={isHiddenIndex} + > + +

+ {i18n.translate('xpack.enterpriseSearch.content.index.searchEngines.createEngine', { + defaultMessage: 'Create an App Search engine', + })} +

+
+
+
+ + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/header_actions.tsx index 3809483e053e70f..9f04dd7ba16ce5f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/header_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/header_actions.tsx @@ -18,5 +18,5 @@ import { SyncButton } from './sync_button'; export const getHeaderActions = (indexData?: ElasticsearchIndexWithIngestion) => [ ...(isCrawlerIndex(indexData) ? [] : []), ...(isConnectorIndex(indexData) ? [] : []), - , + , ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/search_engines_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/search_engines_popover.tsx index d535d37a4a8ee87..2ac1682f8676cbf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/search_engines_popover.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/search_engines_popover.tsx @@ -15,17 +15,26 @@ import { EuiContextMenuPanel, EuiContextMenuItem, EuiText, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { APP_SEARCH_PLUGIN } from '../../../../../../../common/constants'; -import { ENGINE_CREATION_PATH } from '../../../../../app_search/routes'; import { KibanaLogic } from '../../../../../shared/kibana'; +import { CreateEngineMenuItem } from './create_engine_menu_item'; import { SearchEnginesPopoverLogic } from './search_engines_popover_logic'; -export const SearchEnginesPopover: React.FC = () => { +export interface SearchEnginesPopoverProps { + indexName?: string; + isHiddenIndex?: boolean; +} + +export const SearchEnginesPopover: React.FC = ({ + indexName, + isHiddenIndex, +}) => { const { isSearchEnginesPopoverOpen } = useValues(SearchEnginesPopoverLogic); const { toggleSearchEnginesPopover } = useActions(SearchEnginesPopoverLogic); @@ -60,22 +69,20 @@ export const SearchEnginesPopover: React.FC = () => {

, - { - KibanaLogic.values.navigateToUrl(APP_SEARCH_PLUGIN.URL + ENGINE_CREATION_PATH, { - shouldNotCreateHref: true, - }); - }} - > - -

- {i18n.translate('xpack.enterpriseSearch.content.index.searchEngines.createEngine', { - defaultMessage: 'Create a new App Search engine', - })} -

-
-
, + isHiddenIndex ? ( + + ) : ( + + ), ]} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts index 6075f6e9822d3e8..fa9eb67cf9fe493 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts @@ -7,6 +7,7 @@ export * from './actions'; export * from './labels'; +export * from './query_params'; export * from './tables'; export * from './units'; export { DEFAULT_META } from './default_meta'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/query_params.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/query_params.ts new file mode 100644 index 000000000000000..1a95b9734043e8b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/query_params.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ESINDEX_QUERY_PARAMETER = 'esindex'; diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/fetch_indices.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/fetch_indices.mock.ts index 5305cc8ecdb5561..a860069af9e937e 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/fetch_indices.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/fetch_indices.mock.ts @@ -15,6 +15,7 @@ export const mockSingleIndexStatsResponse = { indices: { 'search-regular-index': { health: 'green', + hidden: false, status: 'open', total: { docs: { @@ -115,6 +116,7 @@ export const getIndexReturnValue = (indexName: string) => { alias: indexName.startsWith('alias') || indexName.startsWith('search-alias'), count: 100, name: indexName, + hidden: indexName.includes('hidden'), privileges: { manage: true, read: true }, total: { ...mockMultiStatsResponse.indices[indexName].total, diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_index.test.ts b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_index.test.ts index 9bf9f2b2d2f1d3e..49384f564a9885c 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_index.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_index.test.ts @@ -43,6 +43,7 @@ describe('fetchIndex lib function', () => { indices: { index_name: { health: 'green', + hidden: false, size: new ByteSizeValue(108000).toString(), status: 'open', total: { @@ -63,6 +64,7 @@ describe('fetchIndex lib function', () => { aliases: [], count: 100, health: 'green', + hidden: false, name: 'index_name', status: 'open', total: { diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_indices.test.ts b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_indices.test.ts index 1fc8f4cc071864d..4a01295fbeaa866 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_indices.test.ts @@ -86,6 +86,7 @@ describe('fetchIndices lib function', () => { alias: false, count: 100, health: 'green', + hidden: false, name: 'search-regular-index', privileges: { manage: true, read: true }, status: 'open', @@ -135,6 +136,7 @@ describe('fetchIndices lib function', () => { alias: false, count: 100, health: 'green', + hidden: false, name: 'search-regular-index', privileges: { manage: true, read: true }, status: 'open', @@ -195,6 +197,7 @@ describe('fetchIndices lib function', () => { { count: 100, health: 'green', + hidden: false, name: 'index-without-prefix', status: 'open', alias: false, @@ -213,6 +216,7 @@ describe('fetchIndices lib function', () => { { count: 100, health: 'green', + hidden: false, name: 'search-aliased', status: 'open', alias: true, @@ -231,6 +235,7 @@ describe('fetchIndices lib function', () => { { count: 100, health: 'green', + hidden: false, name: 'search-double-aliased', status: 'open', alias: true, @@ -249,6 +254,7 @@ describe('fetchIndices lib function', () => { { count: 100, health: 'green', + hidden: false, name: 'second-index', status: 'open', alias: false, @@ -298,6 +304,7 @@ describe('fetchIndices lib function', () => { { count: 100, health: 'green', + hidden: false, name: 'index-without-prefix', status: 'open', alias: false, @@ -316,6 +323,7 @@ describe('fetchIndices lib function', () => { { count: 100, health: 'green', + hidden: false, name: 'second-index', status: 'open', alias: false, @@ -350,6 +358,7 @@ describe('fetchIndices lib function', () => { { count: 100, health: undefined, + hidden: false, name: 'search-regular-index', status: undefined, alias: false, diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_indices.ts b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_indices.ts index 28a5373d50ac56a..5a1bfec87b9b713 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_indices.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_indices.ts @@ -13,13 +13,16 @@ import { import { ByteSizeValue } from '@kbn/config-schema'; import { IScopedClusterClient } from '@kbn/core/server'; -import { ElasticsearchIndexWithPrivileges } from '../../../common/types'; +import { + ElasticsearchIndex, + ElasticsearchIndexWithPrivileges, +} from '../../../common/types/indices'; export const mapIndexStats = ( indexData: IndicesIndexState, indexStats: IndicesStatsIndicesStats, indexName: string -) => { +): Omit & { aliases: string[] } => { const aliases = Object.keys(indexData.aliases!); const sizeInBytes = new ByteSizeValue(indexStats?.total?.store?.size_in_bytes ?? 0).toString(); @@ -38,6 +41,7 @@ export const mapIndexStats = ( return { aliases, health: indexStats?.health, + hidden: Boolean(indexData.settings?.index?.hidden), name: indexName, status: indexStats?.status, total, @@ -132,17 +136,17 @@ export const fetchIndices = async ( return mapIndexStats(indexData, indexStats, indexName); }) .flatMap(({ name, aliases, ...indexData }) => { - const indicesAndAliases = [] as ElasticsearchIndexWithPrivileges[]; + const indicesAndAliases: ElasticsearchIndexWithPrivileges[] = []; if (includeAliases) { aliases.forEach((alias) => { if (alias.startsWith(alwaysShowSearchPattern)) { indicesAndAliases.push({ + ...indexData, alias: true, count: indexCounts[alias] ?? 0, name: alias, privileges: { manage: false, read: false, ...indexPrivileges[name] }, - ...indexData, }); } }); @@ -160,23 +164,23 @@ export const fetchIndices = async ( }) .flatMap(({ name, aliases, ...indexData }) => { // expand aliases and add to results - const indicesAndAliases = [] as ElasticsearchIndexWithPrivileges[]; + const indicesAndAliases: ElasticsearchIndexWithPrivileges[] = []; indicesAndAliases.push({ + ...indexData, alias: false, count: indexCounts[name] ?? 0, name, privileges: { manage: false, read: false, ...indexPrivileges[name] }, - ...indexData, }); if (includeAliases) { aliases.forEach((alias) => { indicesAndAliases.push({ + ...indexData, alias: true, count: indexCounts[alias] ?? 0, name: alias, privileges: { manage: false, read: false, ...indexPrivileges[name] }, - ...indexData, }); }); } @@ -191,7 +195,7 @@ export const fetchIndices = async ( const itemsToInclude = alwaysShowIndices.filter(({ name }) => indexNamesToInclude.includes(name)); const indicesData = alwaysShowSearchPattern - ? ([...regularIndexData, ...itemsToInclude] as ElasticsearchIndexWithPrivileges[]) + ? [...regularIndexData, ...itemsToInclude] : regularIndexData; return indicesData.filter( diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index ced5c17f55f6ffe..d514d5b56469018 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -101,6 +101,8 @@ export type { UpgradePackagePolicyResponseItem, UpgradePackagePolicyBaseResponse, UpgradePackagePolicyDryRunResponseItem, + BulkGetPackagePoliciesResponse, + BulkGetAgentPoliciesResponse, // Models Agent, AgentStatus, diff --git a/x-pack/plugins/fleet/common/services/limited_package.ts b/x-pack/plugins/fleet/common/services/limited_package.ts index 601f680c8bf0390..e4e9dd090941d7b 100644 --- a/x-pack/plugins/fleet/common/services/limited_package.ts +++ b/x-pack/plugins/fleet/common/services/limited_package.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { PackageInfo, AgentPolicy, PackagePolicy } from '../types'; +import type { PackageInfo, AgentPolicy } from '../types'; export const isPackageLimited = (packageInfo: PackageInfo): boolean => { return (packageInfo.policy_templates || []).some( @@ -17,10 +17,10 @@ export const doesAgentPolicyAlreadyIncludePackage = ( agentPolicy: AgentPolicy, packageName: string ): boolean => { - if (agentPolicy.package_policies.length && typeof agentPolicy.package_policies[0] === 'string') { + if (!agentPolicy.package_policies) { throw new Error('Unable to read full package policy information'); } - return (agentPolicy.package_policies as PackagePolicy[]) + return agentPolicy.package_policies .map((packagePolicy) => packagePolicy.package?.name || '') .includes(packageName); }; diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 95551ddeea7c0f3..323d7d1f8b3786a 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -118,6 +118,10 @@ export const agentPolicyRouteService = { return AGENT_POLICY_API_ROUTES.LIST_PATTERN; }, + getBulkGetPath: () => { + return AGENT_POLICY_API_ROUTES.BULK_GET_PATTERN; + }, + getInfoPath: (agentPolicyId: string) => { return AGENT_POLICY_API_ROUTES.INFO_PATTERN.replace('{agentPolicyId}', agentPolicyId); }, diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 44e6b35c02eec59..ea22f73a2e5f973 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -35,7 +35,7 @@ export interface NewAgentPolicy { export interface AgentPolicy extends Omit { id: string; status: ValueOf; - package_policies: string[] | PackagePolicy[]; + package_policies?: PackagePolicy[]; is_managed: boolean; // required for created policy updated_at: string; updated_by: string; diff --git a/x-pack/plugins/fleet/cypress/downloads/downloads.html b/x-pack/plugins/fleet/cypress/downloads/downloads.html new file mode 100644 index 000000000000000..772778ea352e585 Binary files /dev/null and b/x-pack/plugins/fleet/cypress/downloads/downloads.html differ diff --git a/x-pack/plugins/fleet/cypress/tasks/fleet.ts b/x-pack/plugins/fleet/cypress/tasks/fleet.ts index 53f2846b3a981b0..4a23daf7a838099 100644 --- a/x-pack/plugins/fleet/cypress/tasks/fleet.ts +++ b/x-pack/plugins/fleet/cypress/tasks/fleet.ts @@ -15,9 +15,15 @@ import { } from '../screens/fleet'; export function createAgentPolicy() { + cy.intercept({ + url: '/api/fleet/agent_policies?sys_monitoring=true', + method: 'POST', + }).as('postAgentPolicy'); cy.getBySel(ADD_AGENT_BUTTON_TOP).click(); cy.getBySel(STANDALONE_TAB).click(); cy.getBySel(CREATE_POLICY_BUTTON).click(); + + cy.wait('@postAgentPolicy'); cy.getBySel(AGENT_FLYOUT_CLOSE_BUTTON).click(); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/index.tsx index c51a7415cd1a699..19ff6a5d00d277b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/index.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; -import type { AgentPolicy, PackagePolicy } from '../../../../../types'; +import type { AgentPolicy } from '../../../../../types'; import { useBreadcrumbs } from '../../../../../hooks'; import { NoPackagePolicies } from './no_package_policies'; @@ -16,14 +16,14 @@ import { PackagePoliciesTable } from './package_policies_table'; export const PackagePoliciesView = memo<{ agentPolicy: AgentPolicy }>(({ agentPolicy }) => { useBreadcrumbs('policy_details', { policyName: agentPolicy.name }); - if (agentPolicy.package_policies.length === 0) { + if (!agentPolicy.package_policies || agentPolicy.package_policies.length === 0) { return ; } return ( ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx index ba6b7f4ad31c4ba..9b0d9051a32ed80 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx @@ -89,6 +89,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { sortField: sorting?.field, sortOrder: sorting?.direction, kuery: search, + full: true, }); // Some policies retrieved, set up table props diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index a9e3429732b3cd8..6bf9506d6ba28b9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; -import type { Agent, AgentPolicy, PackagePolicy, SimplifiedAgentStatus } from '../../../types'; +import type { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../types'; import { usePagination, useAuthz, @@ -35,8 +35,12 @@ import { sendGetAgentTags, } from '../../../hooks'; import { AgentEnrollmentFlyout, AgentPolicySummaryLine } from '../../../components'; -import { AgentStatusKueryHelper, isAgentUpgradeable } from '../../../services'; -import { AGENTS_PREFIX, FLEET_SERVER_PACKAGE, SO_SEARCH_LIMIT } from '../../../constants'; +import { + AgentStatusKueryHelper, + isAgentUpgradeable, + policyHasFleetServer, +} from '../../../services'; +import { AGENTS_PREFIX, SO_SEARCH_LIMIT } from '../../../constants'; import { AgentReassignAgentPolicyModal, AgentHealth, @@ -389,10 +393,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return false; } - return agentPolicy.package_policies.some( - (ap: string | PackagePolicy) => - typeof ap !== 'string' && ap.package?.name === FLEET_SERVER_PACKAGE - ); + return policyHasFleetServer(agentPolicy); }, [agentToUnenroll, agentPoliciesIndexedById]); // Fleet server unhealthy status diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index a77c45a6b5110a8..693dee99ebf69ae 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -717,11 +717,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos namespace: 'default', description: 'Default agent policy created by Kibana', status: 'active', - package_policies: [ - '4d09bd78-b0ad-4238-9fa3-d87d3c887c73', - '2babac18-eb8e-4ce4-b53b-4b7c5f507019', - 'e8a37031-2907-44f6-89d2-98bd493f60dc', - ], + package_policies: [], is_managed: false, monitoring_enabled: ['logs', 'metrics'], revision: 6, @@ -735,7 +731,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos namespace: 'default', description: 'Protect EU from COVID', status: 'active', - package_policies: ['e8a37031-2907-44f6-89d2-98bd493f60cd'], + package_policies: [], is_managed: false, monitoring_enabled: ['logs', 'metrics'], revision: 2, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts index 4f6f456bf82cdfd..399016ebf204269 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts @@ -14,7 +14,6 @@ import type { GetPackagePoliciesResponse, } from '../../../../../types'; import { agentPolicyRouteService } from '../../../../../services'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { useGetPackagePolicies, useConditionalRequest } from '../../../../../hooks'; import type { SendConditionalRequestConfig } from '../../../../../hooks'; @@ -52,37 +51,31 @@ export const usePackagePoliciesWithAgentPolicy = ( resendRequest, } = useGetPackagePolicies(query); - const agentPoliciesFilter = useMemo(() => { + const agentPoliciesIds = useMemo(() => { if (!packagePoliciesData?.items.length) { - return ''; + return []; } // Build a list of package_policies for which we need Agent Policies for. Since some package // policies can exist within the same Agent Policy, we don't need to (in some cases) include // the entire list of package_policy ids. - const includedAgentPolicies = new Set(); - - return `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${packagePoliciesData.items - .filter((packagePolicy) => { - if (includedAgentPolicies.has(packagePolicy.policy_id)) { - return false; - } - includedAgentPolicies.add(packagePolicy.policy_id); - return true; - }) - .map((packagePolicy) => packagePolicy.id) - .join(' or ')}) `; + return Array.from( + new Set( + packagePoliciesData.items.map((packagePolicy) => packagePolicy.policy_id) + ).values() + ); }, [packagePoliciesData]); const { data: agentPoliciesData, isLoading: isLoadingAgentPolicies } = useConditionalRequest({ - path: agentPolicyRouteService.getListPath(), - method: 'get', - query: { - perPage: 100, - kuery: agentPoliciesFilter, + path: agentPolicyRouteService.getBulkGetPath(), + method: 'post', + body: { + ids: agentPoliciesIds, + full: true, + ignoreMissing: true, }, - shouldSendRequest: !!packagePoliciesData?.items.length, + shouldSendRequest: agentPoliciesIds.length > 0, } as SendConditionalRequestConfig); const [enrichedData, setEnrichedData] = useState(); diff --git a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx index 4789770b7046f38..5a0b6285c71db1a 100644 --- a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx @@ -52,8 +52,8 @@ export const usePackageInstallations = () => { const updatableIntegrations = useMemo>( () => (agentPolicyData?.items || []).reduce((result, policy) => { - policy.package_policies.forEach((pkgPolicy: PackagePolicy | string) => { - if (typeof pkgPolicy === 'string' || !pkgPolicy.package) return false; + policy.package_policies?.forEach((pkgPolicy: PackagePolicy) => { + if (!pkgPolicy.package) return false; const { name, version } = pkgPolicy.package; const installedPackage = allInstalledPackages.find( (installedPkg) => diff --git a/x-pack/plugins/fleet/public/services/has_fleet_server.ts b/x-pack/plugins/fleet/public/services/has_fleet_server.ts index e1100d6447aa209..43724d121b90f34 100644 --- a/x-pack/plugins/fleet/public/services/has_fleet_server.ts +++ b/x-pack/plugins/fleet/public/services/has_fleet_server.ts @@ -9,8 +9,11 @@ import { FLEET_SERVER_PACKAGE } from '../constants'; import type { AgentPolicy, PackagePolicy } from '../types'; export function policyHasFleetServer(agentPolicy: AgentPolicy) { - return agentPolicy.package_policies?.some( - (ap: string | PackagePolicy) => - typeof ap !== 'string' && ap.package?.name === FLEET_SERVER_PACKAGE + if (!agentPolicy.package_policies) { + return false; + } + + return agentPolicy.package_policies.some( + (ap: PackagePolicy) => ap.package?.name === FLEET_SERVER_PACKAGE ); } diff --git a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts index 30bdf452cde12d0..b4afbec06787481 100644 --- a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts @@ -248,9 +248,7 @@ describe('Fleet preconfiguration reset', () => { it('Works if the preconfigured policies already exists with a missing package policy', async () => { const soClient = kbnServer.coreStart.savedObjects.createInternalRepository(); - await soClient.update('ingest-agent-policies', POLICY_ID, { - package_policies: [], - }); + await soClient.update('ingest-agent-policies', POLICY_ID, {}); const resetAPI = getSupertestWithAdminUser( kbnServer.root, @@ -268,7 +266,6 @@ describe('Fleet preconfiguration reset', () => { expect.arrayContaining([ expect.objectContaining({ name: 'Elastic Cloud agent policy 0001', - package_policies: expect.arrayContaining([expect.stringMatching(/.*/)]), }), expect.objectContaining({ name: 'Second preconfigured policy', diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index b4e5f83d2cb0608..a76988506cad21f 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -121,6 +121,7 @@ export const createPackagePolicyServiceMock = (): jest.Mocked & { - package_configs: string[] | PackagePolicy[]; + Omit & { + package_configs: string[]; + package_policies?: string[]; }, - AgentPolicy + Omit & { + package_policies?: string[]; + } > = (agentPolicyDoc) => { agentPolicyDoc.attributes.package_policies = agentPolicyDoc.attributes.package_configs; // @ts-expect-error diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_5_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_5_0.ts new file mode 100644 index 000000000000000..9cbcc85c46936fe --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_5_0.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectMigrationFn } from '@kbn/core/server'; + +import type { AgentPolicy } from '../../types'; + +export const migrateAgentPolicyToV850: SavedObjectMigrationFn< + Exclude & { + package_policies: string[]; + }, + AgentPolicy +> = (agentPolicyDoc) => { + // @ts-expect-error + delete agentPolicyDoc.attributes.package_policies; + + return agentPolicyDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts index 0db543e4357a464..a60b5c68d05a9ea 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts @@ -31,8 +31,10 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { expect(permissions).toBeUndefined(); }); - it('Throw an error for string package policies', async () => { - await expect(() => storedPackagePoliciesToAgentPermissions(soClient, ['foo'])).rejects.toThrow( + it('Throw an error if package policies is not an array', async () => { + await expect(() => + storedPackagePoliciesToAgentPermissions(soClient, undefined) + ).rejects.toThrow( /storedPackagePoliciesToAgentPermissions should be called with a PackagePolicy/ ); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts index ee17494ca64547e..b961eb8691f888c 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts @@ -20,19 +20,19 @@ export const DEFAULT_CLUSTER_PERMISSIONS = ['monitor']; export async function storedPackagePoliciesToAgentPermissions( soClient: SavedObjectsClientContract, - packagePolicies: string[] | PackagePolicy[] + packagePolicies?: PackagePolicy[] ): Promise { - if (packagePolicies.length === 0) { - return; - } - // I'm not sure what permissions to return for this case, so let's return the defaults - if (typeof packagePolicies[0] === 'string') { + if (!packagePolicies) { throw new Error( 'storedPackagePoliciesToAgentPermissions should be called with a PackagePolicy' ); } + if (packagePolicies.length === 0) { + return; + } + const permissionEntries = (packagePolicies as PackagePolicy[]).map>( async (packagePolicy) => { if (!packagePolicy.package) { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 4d348af471772eb..8dd8c885af3e425 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -75,6 +75,8 @@ const mockedOutputService = outputService as jest.Mocked; const mockedDownloadSourceService = downloadSourceService as jest.Mocked< typeof downloadSourceService >; +const mockedPackagePolicyService = packagePolicyService as jest.Mocked; + const mockedGetFullAgentPolicy = getFullAgentPolicy as jest.Mock< ReturnType >; @@ -144,6 +146,11 @@ describe('agent policy', () => { beforeEach(() => { soClient = getSavedObjectMock({ revision: 1, package_policies: ['package-1'] }); + mockedPackagePolicyService.findAllForAgentPolicy.mockReturnValue([ + { + id: 'package-1', + }, + ] as any); esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; (getAgentsByKuery as jest.Mock).mockResolvedValue({ @@ -153,10 +160,10 @@ describe('agent policy', () => { perPage: 10, }); - (packagePolicyService.delete as jest.Mock).mockResolvedValue([ + mockedPackagePolicyService.delete.mockResolvedValue([ { id: 'package-1', - }, + } as any, ]); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 4008100518984ba..9c70e4a58c388ed 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { uniq, omit, isEqual, keyBy } from 'lodash'; +import { omit, isEqual, keyBy } from 'lodash'; import uuidv5 from 'uuid/v5'; import { safeDump } from 'js-yaml'; import pMap from 'p-map'; @@ -186,8 +186,9 @@ class AgentPolicyService { } public hasAPMIntegration(agentPolicy: AgentPolicy) { - return agentPolicy.package_policies.some( - (p) => typeof p !== 'string' && p.package?.name === FLEET_APM_PACKAGE + return ( + agentPolicy.package_policies && + agentPolicy.package_policies.some((p) => p.package?.name === FLEET_APM_PACKAGE) ); } @@ -261,13 +262,7 @@ class AgentPolicyService { if (withPackagePolicies) { agentPolicy.package_policies = - (await packagePolicyService.getByIDs( - soClient, - (agentPolicySO.attributes.package_policies as string[]) || [], - { - ignoreMissing: true, - } - )) || []; + (await packagePolicyService.findAllForAgentPolicy(soClient, id)) || []; } return agentPolicy; @@ -458,7 +453,7 @@ class AgentPolicyService { ); // Copy all package policies and append (copy n) to their names - if (baseAgentPolicy.package_policies.length) { + if (baseAgentPolicy.package_policies) { const newPackagePolicies = await pMap( baseAgentPolicy.package_policies as PackagePolicy[], async (packagePolicy: PackagePolicy) => { @@ -610,75 +605,6 @@ class AgentPolicyService { return res; } - public async assignPackagePolicies( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - id: string, - packagePolicyIds: string[], - options: { user?: AuthenticatedUser; bumpRevision: boolean; force?: boolean } = { - bumpRevision: true, - } - ): Promise { - const oldAgentPolicy = await this.get(soClient, id, false); - - if (!oldAgentPolicy) { - throw new Error('Agent policy not found'); - } - - if (oldAgentPolicy.is_managed && !options?.force) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot update integrations of hosted agent policy ${id}` - ); - } - - return await this._update( - soClient, - esClient, - id, - { - package_policies: uniq( - [...((oldAgentPolicy.package_policies || []) as string[])].concat(packagePolicyIds) - ), - }, - options?.user, - { bumpRevision: options.bumpRevision } - ); - } - - public async unassignPackagePolicies( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - id: string, - packagePolicyIds: string[], - options?: { user?: AuthenticatedUser; force?: boolean } - ) { - const oldAgentPolicy = await this.get(soClient, id, false); - - if (!oldAgentPolicy) { - throw new Error('Agent policy not found'); - } - - if (oldAgentPolicy.is_managed && !options?.force) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot remove integrations of hosted agent policy ${id}` - ); - } - - return await this._update( - soClient, - esClient, - id, - { - package_policies: uniq( - [...((oldAgentPolicy.package_policies || []) as string[])].filter( - (packagePolicyId) => !packagePolicyIds.includes(packagePolicyId) - ) - ), - }, - options?.user - ); - } - public async delete( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, @@ -705,12 +631,14 @@ class AgentPolicyService { throw new Error('Cannot delete agent policy that is assigned to agent(s)'); } - if (agentPolicy.package_policies && agentPolicy.package_policies.length) { + const packagePolicies = await packagePolicyService.findAllForAgentPolicy(soClient, id); + + if (packagePolicies.length) { const deletedPackagePolicies: DeletePackagePoliciesResponse = await packagePolicyService.delete( soClient, esClient, - agentPolicy.package_policies as string[], + packagePolicies.map((p) => p.id), { force: options?.force, skipUnassignFromAgentPolicies: true, diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 0e9c2fda047d1c6..f7668f8fe19bf08 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -55,6 +55,7 @@ import { PackagePolicyValidationError, PackagePolicyRestrictionRelatedError, PackagePolicyNotFoundError, + HostedAgentPolicyRestrictionRelatedError, } from '../errors'; import { NewPackagePolicySchema, PackagePolicySchema, UpdatePackagePolicySchema } from '../types'; import type { @@ -75,7 +76,7 @@ import { outputService } from './output'; import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; import { getAssetsData } from './epm/packages/assets'; import { compileTemplate } from './epm/agent/agent'; -import { normalizeKuery } from './saved_object'; +import { escapeSearchQueryPhrase, normalizeKuery } from './saved_object'; import { appContextService } from '.'; import { removeOldAssets } from './epm/packages/cleanup'; import type { PackageUpdateEvent, UpdateEventType } from './upgrade_sender'; @@ -120,6 +121,7 @@ class PackagePolicyService implements PackagePolicyServiceInterface { throw new IngestManagerError('You cannot add APM to a policy using a logstash output'); } } + await validateIsNotHostedPolicy(soClient, packagePolicy.policy_id, options?.force); // trailing whitespace causes issues creating API keys packagePolicy.name = packagePolicy.name.trim(); @@ -196,18 +198,11 @@ class PackagePolicyService implements PackagePolicyServiceInterface { { ...options, id: packagePolicyId } ); - // Assign it to the given agent policy - await agentPolicyService.assignPackagePolicies( - soClient, - esClient, - packagePolicy.policy_id, - [newSo.id], - { + if (options?.bumpRevision ?? true) { + await agentPolicyService.bumpRevision(soClient, esClient, packagePolicy.policy_id, { user: options?.user, - bumpRevision: options?.bumpRevision ?? true, - force: options?.force, - } - ); + }); + } return { id: newSo.id, @@ -221,8 +216,9 @@ class PackagePolicyService implements PackagePolicyServiceInterface { esClient: ElasticsearchClient, packagePolicies: NewPackagePolicy[], agentPolicyId: string, - options?: { user?: AuthenticatedUser; bumpRevision?: boolean } + options?: { user?: AuthenticatedUser; bumpRevision?: boolean; force: true } ): Promise { + await validateIsNotHostedPolicy(soClient, agentPolicyId); const isoDate = new Date().toISOString(); // eslint-disable-next-line @typescript-eslint/naming-convention const { saved_objects } = await soClient.bulkCreate( @@ -254,16 +250,12 @@ class PackagePolicyService implements PackagePolicyServiceInterface { const newSos = saved_objects.filter((so) => !so.error && so.attributes); // Assign it to the given agent policy - await agentPolicyService.assignPackagePolicies( - soClient, - esClient, - agentPolicyId, - newSos.map((newSo) => newSo.id), - { + + if (options?.bumpRevision ?? true) { + await agentPolicyService.bumpRevision(soClient, esClient, agentPolicyId, { user: options?.user, - bumpRevision: options?.bumpRevision ?? true, - } - ); + }); + } return newSos.map((newSo) => ({ id: newSo.id, @@ -292,6 +284,26 @@ class PackagePolicyService implements PackagePolicyServiceInterface { }; } + public async findAllForAgentPolicy( + soClient: SavedObjectsClientContract, + agentPolicyId: string + ): Promise { + const packagePolicySO = await soClient.find({ + type: SAVED_OBJECT_TYPE, + filter: `${SAVED_OBJECT_TYPE}.attributes.policy_id:${escapeSearchQueryPhrase(agentPolicyId)}`, + perPage: SO_SEARCH_LIMIT, + }); + if (!packagePolicySO) { + return []; + } + + return packagePolicySO.saved_objects.map((so) => ({ + id: so.id, + version: so.version, + ...so.attributes, + })); + } + public async getByIDs( soClient: SavedObjectsClientContract, ids: string[], @@ -501,6 +513,13 @@ class PackagePolicyService implements PackagePolicyServiceInterface { throw new PackagePolicyRestrictionRelatedError(`Cannot delete package policy ${id}`); } + await validateIsNotHostedPolicy( + soClient, + packagePolicy?.policy_id, + options?.force, + 'Cannot remove integrations of hosted agent policy' + ); + const agentPolicy = await agentPolicyService .get(soClient, packagePolicy.policy_id) .catch((err) => { @@ -513,19 +532,12 @@ class PackagePolicyService implements PackagePolicyServiceInterface { throw err; }); + await soClient.delete(SAVED_OBJECT_TYPE, id); if (agentPolicy && !options?.skipUnassignFromAgentPolicies) { - await agentPolicyService.unassignPackagePolicies( - soClient, - esClient, - packagePolicy.policy_id, - [packagePolicy.id], - { - user: options?.user, - force: options?.force, - } - ); + await agentPolicyService.bumpRevision(soClient, esClient, packagePolicy.policy_id, { + user: options?.user, + }); } - await soClient.delete(SAVED_OBJECT_TYPE, id); result.push({ id, name: packagePolicy.name, @@ -1281,6 +1293,11 @@ export interface PackagePolicyServiceInterface { get(soClient: SavedObjectsClientContract, id: string): Promise; + findAllForAgentPolicy( + soClient: SavedObjectsClientContract, + agentPolicyId: string + ): Promise; + getByIDs( soClient: SavedObjectsClientContract, ids: string[], @@ -1607,6 +1624,25 @@ export function preconfigurePackageInputs( return resultingPackagePolicy; } +async function validateIsNotHostedPolicy( + soClient: SavedObjectsClientContract, + id: string, + force = false, + errorMessage?: string +) { + const agentPolicy = await agentPolicyService.get(soClient, id, false); + + if (!agentPolicy) { + throw new Error('Agent policy not found'); + } + + if (agentPolicy.is_managed && !force) { + throw new HostedAgentPolicyRestrictionRelatedError( + errorMessage ?? `Cannot update integrations of hosted agent policy ${id}` + ); + } +} + function deepMergeVars(original: any, override: any, keepOriginalValue = false): any { if (!original.vars) { original.vars = { ...override.vars }; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index c0b24f927d8488f..245ab2316cb0228 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -238,7 +238,8 @@ jest.mock('./epm/kibana/index_pattern/install'); jest.mock('./package_policy', () => ({ ...jest.requireActual('./package_policy'), packagePolicyService: { - getByIDs: jest.fn().mockReturnValue([]), + ...jest.requireActual('./package_policy').packagePolicyService, + findAllForAgentPolicy: jest.fn().mockReturnValue([]), listIds: jest.fn().mockReturnValue({ items: [] }), create: jest .fn() @@ -280,8 +281,8 @@ const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn( describe('policy preconfiguration', () => { beforeEach(() => { - mockedPackagePolicyService.getByIDs.mockReset(); mockedPackagePolicyService.create.mockReset(); + mockedPackagePolicyService.findAllForAgentPolicy.mockReset(); mockInstalledPackages.clear(); mockInstallPackageErrors.clear(); mockConfiguredPolicies.clear(); @@ -365,7 +366,7 @@ describe('policy preconfiguration', () => { it('should not add new package policy to existing non managed policies', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedPackagePolicyService.getByIDs.mockResolvedValue([ + mockedPackagePolicyService.findAllForAgentPolicy.mockResolvedValue([ { name: 'test_package1' } as PackagePolicy, ]); @@ -415,7 +416,7 @@ describe('policy preconfiguration', () => { it('should add new package policy to existing managed policies', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedPackagePolicyService.getByIDs.mockResolvedValue([ + mockedPackagePolicyService.findAllForAgentPolicy.mockResolvedValue([ { name: 'test_package1' } as PackagePolicy, ]); @@ -475,7 +476,7 @@ describe('policy preconfiguration', () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedPackagePolicyService.getByIDs.mockResolvedValue([ + mockedPackagePolicyService.findAllForAgentPolicy.mockResolvedValue([ { name: 'Renamed package policy', id: 'test_package1' } as PackagePolicy, ]); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx index 81b73b8f267b733..c4583d131dc1cce 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx @@ -63,7 +63,8 @@ export const createGridColumns = ( rowIndex, columnId, }: Pick) => { - const rowValue = table.rows[rowIndex][columnId]; + // incoming data might change and put the current page out of bounds - check whether row actually exists + const rowValue = table.rows[rowIndex]?.[columnId]; const column = columnsReverseLookup?.[columnId]; const contentsIsDefined = rowValue != null; diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx index c1986b9ff7ef597..7ab05fc5ce24f22 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx @@ -739,7 +739,9 @@ describe('DatatableComponent', () => { it('enables pagination', async () => { const { data, args } = sampleArgs(); - args.pageSize = 10; + data.rows = new Array(10).fill({ a: 'shoes', b: 1588024800000, c: 3 }); + + args.pageSize = 2; const wrapper = mount( { expect(updatedConfig?.pageSize).toBe(args.pageSize); }); + it('resets page position if rows change so page will be empty', async () => { + const { data, args } = sampleArgs(); + + data.rows = new Array(10).fill({ a: 'shoes', b: 1588024800000, c: 3 }); + + args.pageSize = 2; + + const wrapper = mount( + x as unknown as IFieldFormat} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={{ get: jest.fn() } as unknown as IUiSettingsClient} + renderMode="edit" + interactive + renderComplete={renderComplete} + /> + ); + const newIndex = 3; + act(() => wrapper.find(EuiDataGrid).prop('pagination')?.onChangePage(newIndex)); + wrapper.update(); + + expect(wrapper.find(EuiDataGrid).prop('pagination')?.pageIndex).toBe(newIndex); + + wrapper.setProps({ + data: { + ...data, + rows: new Array(20).fill({ a: 'shoes', b: 1588024800000, c: 3 }), + }, + }); + + await waitForWrapperUpdate(wrapper); + + // keeps existing page if more data is added + expect(wrapper.find(EuiDataGrid).prop('pagination')?.pageIndex).toBe(newIndex); + + wrapper.setProps({ + data: { + ...data, + rows: new Array(3).fill({ a: 'shoes', b: 1588024800000, c: 3 }), + }, + }); + + await waitForWrapperUpdate(wrapper); + // resets to the last page if the current page becomes out of bounds + expect(wrapper.find(EuiDataGrid).prop('pagination')?.pageIndex).toBe(1); + }); + it('disables pagination by default', async () => { const { data, args } = sampleArgs(); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx index fe0a7da701f2a1c..275eca93d6cbb5d 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx @@ -114,6 +114,25 @@ export const DatatableComponent = (props: DatatableRenderProps) => { const firstTableRef = useRef(firstLocalTable); firstTableRef.current = firstLocalTable; + useEffect(() => { + if (!pagination?.pageIndex && !pagination?.pageSize) return; + const lastPageIndex = Math.ceil(firstLocalTable.rows.length / pagination.pageSize) - 1; + /** + * When the underlying data changes, there might be a case when actual pagination page + * doesn't exist anymore - if the number of rows has decreased. + * Set the last page as an actual. + */ + setPagination((pag) => { + if (!pag) { + return pag; + } + return { + pageIndex: pag.pageIndex > lastPageIndex ? lastPageIndex : pag.pageIndex, + pageSize: pag.pageSize, + }; + }); + }, [pagination?.pageIndex, pagination?.pageSize, firstLocalTable.rows.length]); + const untransposedDataRef = useRef(props.untransposedData); untransposedDataRef.current = props.untransposedData; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_policy_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_policy_generator.ts index 82294ac754fce33..5b99a28e96ee023 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_policy_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_policy_generator.ts @@ -22,7 +22,6 @@ export class FleetAgentPolicyGenerator extends BaseDataGenerator void; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; - utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; additionalFilters?: React.ReactNode; hasAlertsCrud?: boolean; unit?: (n: number) => string; @@ -86,7 +85,6 @@ const StatefulEventsViewerComponent: React.FC = ({ rowRenderers, start, scopeId, - utilityBar, additionalFilters, hasAlertsCrud = false, unit, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/index.test.tsx new file mode 100644 index 000000000000000..ec4fdb5cb6e8d77 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/index.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; + +import { AdditionalFiltersAction } from '.'; +import { TestProviders } from '../../../../common/mock/test_providers'; + +jest.useFakeTimers(); +jest.mock('../../../../common/lib/kibana'); + +describe('AdditionalFiltersAction', () => { + describe('UtilityBarAdditionalFiltersContent', () => { + test('does not show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is false', async () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + render( + + + + ); + // click the filters button to popup the checkbox to make it visible + const additionalFiltersButton = screen.findByTestId('additionalFilters-popover'); + fireEvent.click(await additionalFiltersButton); + + // The check box should be false + expect(await screen.findByTestId('showBuildingBlockAlertsCheckbox')).not.toBeChecked(); + }); + + test('does not show the showOnlyThreatIndicatorAlerts checked if the showOnlyThreatIndicatorAlerts is true', async () => { + render( + + + + ); + // click the filters button to popup the checkbox to make it visible + const additionalFiltersButton = screen.findByTestId('additionalFilters-popover'); + fireEvent.click(await additionalFiltersButton); + + expect(await screen.findByTestId('showOnlyThreatIndicatorAlertsCheckbox')).toBeChecked(); + }); + + test('does show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is true', async () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + render( + + + + ); + // click the filters button to popup the checkbox to make it visible + const additionalFiltersButton = screen.findByTestId('additionalFilters-popover'); + fireEvent.click(await additionalFiltersButton); + + // The check box should be true + expect(await screen.findByTestId('showBuildingBlockAlertsCheckbox')).toBeChecked(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/index.tsx new file mode 100644 index 000000000000000..ef780f783e92410 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/index.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; +import styled from 'styled-components'; + +import { UtilityBarAction } from '../../../../common/components/utility_bar'; +import * as i18n from './translations'; + +const UtilityBarFlexGroup = styled(EuiFlexGroup)` + min-width: 175px; +`; + +const AdditionalFiltersItem = styled(EuiFlexItem)` + padding: ${({ theme }) => theme.eui.euiSizeS}; +`; + +const BuildingBlockContainer = styled(AdditionalFiltersItem)` + background: ${({ theme }) => theme.eui.euiColorHighlight}; +`; + +export const AdditionalFiltersAction = ({ + areEventsLoading, + onShowBuildingBlockAlertsChanged, + showBuildingBlockAlerts, + onShowOnlyThreatIndicatorAlertsChanged, + showOnlyThreatIndicatorAlerts, +}: { + areEventsLoading: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; + showBuildingBlockAlerts: boolean; + onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void; + showOnlyThreatIndicatorAlerts: boolean; +}) => { + const UtilityBarAdditionalFiltersContent = useCallback( + (closePopover: () => void) => ( + + + ) => { + closePopover(); + onShowBuildingBlockAlertsChanged(e.target.checked); + }} + checked={showBuildingBlockAlerts} + color="text" + data-test-subj="showBuildingBlockAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} + /> + + + ) => { + closePopover(); + onShowOnlyThreatIndicatorAlertsChanged(e.target.checked); + }} + checked={showOnlyThreatIndicatorAlerts} + color="text" + data-test-subj="showOnlyThreatIndicatorAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS} + /> + + + ), + [ + onShowBuildingBlockAlertsChanged, + onShowOnlyThreatIndicatorAlertsChanged, + showBuildingBlockAlerts, + showOnlyThreatIndicatorAlerts, + ] + ); + + return ( + + {i18n.ADDITIONAL_FILTERS_ACTIONS} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/translations.ts new file mode 100644 index 000000000000000..eb421c67ff39a54 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/additional_filters_action/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ADDITIONAL_FILTERS_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle', + { + defaultMessage: 'Additional filters', + } +); + +export const ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle', + { + defaultMessage: 'Include building block alerts', + } +); + +export const ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showOnlyThreatIndicatorAlerts', + { + defaultMessage: 'Show only threat indicator alerts', + } +); + +export const TAKE_ACTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.takeActionTitle', + { + defaultMessage: 'Take action', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx deleted file mode 100644 index f4372631cf0f45d..000000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { shallow, mount } from 'enzyme'; - -import type { AlertsUtilityBarProps } from '.'; -import { AlertsUtilityBar } from '.'; -import { TestProviders } from '../../../../common/mock/test_providers'; - -jest.useFakeTimers(); -jest.mock('../../../../common/lib/kibana'); - -describe('AlertsUtilityBar', () => { - test('renders correctly', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find('[dataTestSubj="alertActionPopover"]')).toBeTruthy(); - }); - - describe('UtilityBarAdditionalFiltersContent', () => { - test('does not show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is false', () => { - const onShowBuildingBlockAlertsChanged = jest.fn(); - const wrapper = mount( - - - - ); - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // The check box should be false - expect( - wrapper - .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') - .first() - .prop('checked') - ).toEqual(false); - }); - - test('does not show the showOnlyThreatIndicatorAlerts checked if the showThreatMatchOnly is false', () => { - const wrapper = mount( - - - - ); - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // The check box should be false - expect( - wrapper - .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') - .first() - .prop('checked') - ).toEqual(false); - }); - - test('does show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is true', () => { - const onShowBuildingBlockAlertsChanged = jest.fn(); - const wrapper = mount( - - - - ); - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // The check box should be true - expect( - wrapper - .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') - .first() - .prop('checked') - ).toEqual(true); - }); - - test('does show the showOnlyThreatIndicatorAlerts checked if the showOnlyThreatIndicatorAlerts is true', () => { - const wrapper = mount( - - - - ); - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // The check box should be true - expect( - wrapper - .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') - .first() - .prop('checked') - ).toEqual(true); - }); - - test('calls the onShowBuildingBlockAlertsChanged when the check box is clicked', () => { - const onShowBuildingBlockAlertsChanged = jest.fn(); - const wrapper = mount( - - - - ); - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // check the box - wrapper - .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') - .first() - .simulate('change', { target: { checked: true } }); - - // Make sure our callback is called - expect(onShowBuildingBlockAlertsChanged).toHaveBeenCalled(); - }); - - test('calls the onShowOnlyThreatIndicatorAlertsChanged when the check box is clicked', () => { - const onShowOnlyThreatIndicatorAlertsChanged = jest.fn(); - const wrapper = mount( - - - - ); - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // check the box - wrapper - .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') - .first() - .simulate('change', { target: { checked: true } }); - - // Make sure our callback is called - expect(onShowOnlyThreatIndicatorAlertsChanged).toHaveBeenCalled(); - }); - - test('can update showBuildingBlockAlerts from false to true', () => { - const Proxy = (props: AlertsUtilityBarProps) => ( - - - - ); - - const wrapper = mount( - - ); - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // The check box should false now since we initially set the showBuildingBlockAlerts to false - expect( - wrapper - .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') - .first() - .prop('checked') - ).toEqual(false); - - wrapper.setProps({ showBuildingBlockAlerts: true }); - wrapper.update(); - - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // The check box should be true now since we changed the showBuildingBlockAlerts from false to true - expect( - wrapper - .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') - .first() - .prop('checked') - ).toEqual(true); - }); - - test('can update showOnlyThreatIndicatorAlerts from false to true', () => { - const Proxy = (props: AlertsUtilityBarProps) => ( - - - - ); - - const wrapper = mount( - - ); - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // The check box should false now since we initially set the showBuildingBlockAlerts to false - expect( - wrapper - .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') - .first() - .prop('checked') - ).toEqual(false); - - wrapper.setProps({ showOnlyThreatIndicatorAlerts: true }); - wrapper.update(); - - // click the filters button to popup the checkbox to make it visible - wrapper - .find('[data-test-subj="additionalFilters"] button') - .first() - .simulate('click') - .update(); - - // The check box should be true now since we changed the showBuildingBlockAlerts from false to true - expect( - wrapper - .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') - .first() - .prop('checked') - ).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx deleted file mode 100644 index 91425ab90e60b67..000000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty } from 'lodash/fp'; -import React, { useCallback } from 'react'; -import numeral from '@elastic/numeral'; - -import { EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; -import styled from 'styled-components'; - -import type { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { Link } from '../../../../common/components/link_icon'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarSpacer, - UtilityBarText, -} from '../../../../common/components/utility_bar'; -import * as i18n from './translations'; -import { useUiSetting$ } from '../../../../common/lib/kibana'; -import type { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; -import type { UpdateAlertsStatus } from '../types'; -import { FILTER_CLOSED, FILTER_ACKNOWLEDGED, FILTER_OPEN } from '../alerts_filter_group'; - -export interface AlertsUtilityBarProps { - areEventsLoading: boolean; - clearSelection: () => void; - currentFilter: Status; - hasIndexMaintenance: boolean; - hasIndexWrite: boolean; - onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; - onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void; - selectAll: () => void; - selectedEventIds: Readonly>; - showBuildingBlockAlerts: boolean; - showClearSelection: boolean; - showOnlyThreatIndicatorAlerts: boolean; - totalCount: number; - updateAlertsStatus: UpdateAlertsStatus; -} - -const UtilityBarFlexGroup = styled(EuiFlexGroup)` - min-width: 175px; -`; - -const AdditionalFiltersItem = styled(EuiFlexItem)` - padding: ${({ theme }) => theme.eui.euiSizeS}; -`; - -const BuildingBlockContainer = styled(AdditionalFiltersItem)` - background: ${({ theme }) => theme.eui.euiColorHighlight}; -`; - -const AlertsUtilityBarComponent: React.FC = ({ - areEventsLoading, - clearSelection, - currentFilter, - hasIndexMaintenance, - hasIndexWrite, - onShowBuildingBlockAlertsChanged, - onShowOnlyThreatIndicatorAlertsChanged, - selectAll, - selectedEventIds, - showBuildingBlockAlerts, - showClearSelection, - showOnlyThreatIndicatorAlerts, - totalCount, - updateAlertsStatus, -}) => { - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - - const handleUpdateStatus = useCallback( - async (selectedStatus: Status) => { - await updateAlertsStatus({ - alertIds: Object.keys(selectedEventIds), - status: currentFilter, - selectedStatus, - }); - }, - [currentFilter, selectedEventIds, updateAlertsStatus] - ); - - const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); - const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( - defaultNumberFormat - ); - - const UtilityBarPopoverContent = (closePopover: () => void) => ( - - {currentFilter !== FILTER_OPEN && ( - - { - closePopover(); - handleUpdateStatus('open'); - }} - color="text" - data-test-subj="openSelectedAlertsButton" - > - {i18n.BATCH_ACTION_OPEN_SELECTED} - - - )} - - {currentFilter !== FILTER_CLOSED && ( - - { - closePopover(); - handleUpdateStatus('closed'); - }} - color="text" - data-test-subj="closeSelectedAlertsButton" - > - {i18n.BATCH_ACTION_CLOSE_SELECTED} - - - )} - - {currentFilter !== FILTER_ACKNOWLEDGED && ( - - { - closePopover(); - handleUpdateStatus('acknowledged'); - }} - color="text" - data-test-subj="markSelectedAlertsAcknowledgedButton" - > - {i18n.BATCH_ACTION_ACKNOWLEDGED_SELECTED} - - - )} - - ); - - const handleSelectAllAlertsClick = useCallback(() => { - if (!showClearSelection) { - selectAll(); - } else { - clearSelection(); - } - }, [clearSelection, selectAll, showClearSelection]); - - return ( - <> - - - - - {i18n.SHOWING_ALERTS(formattedTotalCount, totalCount)} - - - - - {hasIndexWrite && hasIndexMaintenance && ( - <> - - {i18n.SELECTED_ALERTS( - showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, - showClearSelection ? totalCount : Object.keys(selectedEventIds).length - )} - - - - {i18n.TAKE_ACTION} - - - - {showClearSelection - ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_ALERTS(formattedTotalCount, totalCount)} - - - )} - - - - - - - ); -}; - -export const AlertsUtilityBar = React.memo( - AlertsUtilityBarComponent, - (prevProps, nextProps) => - prevProps.areEventsLoading === nextProps.areEventsLoading && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection && - prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts && - prevProps.showOnlyThreatIndicatorAlerts === nextProps.showOnlyThreatIndicatorAlerts -); - -export const AditionalFiltersAction = ({ - areEventsLoading, - onShowBuildingBlockAlertsChanged, - showBuildingBlockAlerts, - onShowOnlyThreatIndicatorAlertsChanged, - showOnlyThreatIndicatorAlerts, -}: { - areEventsLoading: boolean; - onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; - showBuildingBlockAlerts: boolean; - onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void; - showOnlyThreatIndicatorAlerts: boolean; -}) => { - const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( - - - ) => { - closePopover(); - onShowBuildingBlockAlertsChanged(e.target.checked); - }} - checked={showBuildingBlockAlerts} - color="text" - data-test-subj="showBuildingBlockAlertsCheckbox" - label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} - /> - - - ) => { - closePopover(); - onShowOnlyThreatIndicatorAlertsChanged(e.target.checked); - }} - checked={showOnlyThreatIndicatorAlerts} - color="text" - data-test-subj="showOnlyThreatIndicatorAlertsCheckbox" - label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS} - /> - - - ); - - return ( - - {i18n.ADDITIONAL_FILTERS_ACTIONS} - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts deleted file mode 100644 index 5da63a5ab95984d..000000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const SHOWING_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) => - i18n.translate('xpack.securitySolution.detectionEngine.alerts.utilityBar.showingAlertsTitle', { - values: { totalAlertsFormatted, totalAlerts }, - defaultMessage: - 'Showing {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', - }); - -export const SELECTED_ALERTS = (selectedAlertsFormatted: string, selectedAlerts: number) => - i18n.translate('xpack.securitySolution.detectionEngine.alerts.utilityBar.selectedAlertsTitle', { - values: { selectedAlertsFormatted, selectedAlerts }, - defaultMessage: - 'Selected {selectedAlertsFormatted} {selectedAlerts, plural, =1 {alert} other {alerts}}', - }); - -export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) => - i18n.translate('xpack.securitySolution.detectionEngine.alerts.utilityBar.selectAllAlertsTitle', { - values: { totalAlertsFormatted, totalAlerts }, - defaultMessage: - 'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', - }); - -export const ADDITIONAL_FILTERS_ACTIONS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle', - { - defaultMessage: 'Additional filters', - } -); - -export const ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle', - { - defaultMessage: 'Include building block alerts', - } -); - -export const ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showOnlyThreatIndicatorAlerts', - { - defaultMessage: 'Show only threat indicator alerts', - } -); - -export const CLEAR_SELECTION = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle', - { - defaultMessage: 'Clear selection', - } -); - -export const TAKE_ACTION = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.takeActionTitle', - { - defaultMessage: 'Take action', - } -); - -export const BATCH_ACTION_OPEN_SELECTED = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.openSelectedTitle', - { - defaultMessage: 'Open selected', - } -); - -export const BATCH_ACTION_CLOSE_SELECTED = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.closeSelectedTitle', - { - defaultMessage: 'Close selected', - } -); - -export const BATCH_ACTION_ACKNOWLEDGED_SELECTED = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.acknowledgedSelectedTitle', - { - defaultMessage: 'Mark as acknowledged', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index 304f0ac1808214f..34378594a952dc2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -33,13 +33,11 @@ describe('AlertsTableComponent', () => { loadingEventIds={[]} selectedEventIds={{}} isSelectAllChecked={false} - clearSelected={jest.fn()} - setEventsLoading={jest.fn()} - setEventsDeleted={jest.fn()} showBuildingBlockAlerts={false} onShowBuildingBlockAlertsChanged={jest.fn()} showOnlyThreatIndicatorAlerts={false} onShowOnlyThreatIndicatorAlertsChanged={jest.fn()} + dispatch={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 9c11f4499db95c7..d4dead20989a6e9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -6,22 +6,15 @@ */ import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import type { ConnectedProps } from 'react-redux'; import { connect, useDispatch } from 'react-redux'; -import type { Dispatch } from 'redux'; import type { Filter } from '@kbn/es-query'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import type { RowRendererId, TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; -import { - displayErrorToast, - displaySuccessToast, - useStateToaster, -} from '../../../common/components/toasters'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; import { defaultCellActions } from '../../../common/lib/cell_actions/default_cell_actions'; @@ -29,7 +22,6 @@ import { useKibana } from '../../../common/lib/kibana'; import type { inputsModel, State } from '../../../common/store'; import { inputsSelectors } from '../../../common/store'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import * as i18nCommon from '../../../common/translations'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; @@ -38,8 +30,7 @@ import { timelineActions, timelineSelectors } from '../../../timelines/store/tim import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import type { TimelineModel } from '../../../timelines/store/timeline/model'; import { columns, RenderCellValue } from '../../configurations/security_solution_detections'; -import { updateAlertStatusAction } from './actions'; -import { AditionalFiltersAction, AlertsUtilityBar } from './alerts_utility_bar'; +import { AdditionalFiltersAction } from './additional_filters_action'; import { alertsDefaultModel, buildAlertStatusFilter, @@ -47,13 +38,6 @@ import { } from './default_config'; import { buildTimeRangeFilter } from './helpers'; import * as i18n from './translations'; -import type { - SetEventsDeletedProps, - SetEventsLoadingProps, - UpdateAlertsStatusCallback, - UpdateAlertsStatusProps, -} from './types'; - interface OwnProps { defaultFilters?: Filter[]; from: string; @@ -73,7 +57,6 @@ interface OwnProps { type AlertsTableComponentProps = OwnProps & PropsFromRedux; export const AlertsTableComponent: React.FC = ({ - clearSelected, defaultFilters, from, globalFilters, @@ -86,9 +69,6 @@ export const AlertsTableComponent: React.FC = ({ onRuleChange, onShowBuildingBlockAlertsChanged, onShowOnlyThreatIndicatorAlertsChanged, - selectedEventIds, - setEventsDeleted, - setEventsLoading, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts, timelineId, @@ -96,15 +76,12 @@ export const AlertsTableComponent: React.FC = ({ filterGroup = 'open', }) => { const dispatch = useDispatch(); - const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const { browserFields, indexPattern: indexPatterns, selectedPatterns, } = useSourcererDataView(SourcererScopeName.detections); const kibana = useKibana(); - const [, dispatchToaster] = useStateToaster(); - const { addWarning } = useAppToasts(); const ACTION_BUTTON_COUNT = 5; const getGlobalQuery = useCallback( @@ -123,7 +100,6 @@ export const AlertsTableComponent: React.FC = ({ ], kqlQuery: globalQuery, kqlMode: globalQuery.language, - isEventViewer: true, }); } return null; @@ -140,66 +116,6 @@ export const AlertsTableComponent: React.FC = ({ endDate: to, }); - const setEventsLoadingCallback = useCallback( - ({ eventIds, isLoading }: SetEventsLoadingProps) => { - setEventsLoading({ id: timelineId, eventIds, isLoading }); - }, - [setEventsLoading, timelineId] - ); - - const setEventsDeletedCallback = useCallback( - ({ eventIds, isDeleted }: SetEventsDeletedProps) => { - setEventsDeleted({ id: timelineId, eventIds, isDeleted }); - }, - [setEventsDeleted, timelineId] - ); - - const onAlertStatusUpdateSuccess = useCallback( - (updated: number, conflicts: number, status: Status) => { - if (conflicts > 0) { - // Partial failure - addWarning({ - title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts), - text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts), - }); - } else { - let title = ''; - switch (status) { - case 'closed': - title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated); - break; - case 'open': - title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated); - break; - case 'acknowledged': - case 'in-progress': - title = i18n.ACKNOWLEDGED_ALERT_SUCCESS_TOAST(updated); - } - displaySuccessToast(title, dispatchToaster); - } - }, - [addWarning, dispatchToaster] - ); - - const onAlertStatusUpdateFailure = useCallback( - (status: Status, error: Error) => { - let title = ''; - switch (status) { - case 'closed': - title = i18n.CLOSED_ALERT_FAILED_TOAST; - break; - case 'open': - title = i18n.OPENED_ALERT_FAILED_TOAST; - break; - case 'acknowledged': - case 'in-progress': - title = i18n.ACKNOWLEDGED_ALERT_FAILED_TOAST; - } - displayErrorToast(title, [error.message], dispatchToaster); - }, - [dispatchToaster] - ); - // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar useEffect(() => { if (isSelectAllChecked) { @@ -209,107 +125,12 @@ export const AlertsTableComponent: React.FC = ({ selectAll: false, }) ); - } else { - setShowClearSelectionAction(false); } }, [dispatch, isSelectAllChecked, timelineId]); - // Callback for clearing entire selection from utility bar - const clearSelectionCallback = useCallback(() => { - clearSelected({ id: timelineId }); - dispatch( - timelineActions.setTGridSelectAll({ - id: timelineId, - selectAll: false, - }) - ); - setShowClearSelectionAction(false); - }, [clearSelected, dispatch, timelineId]); - - // Callback for selecting all events on all pages from utility bar - // Dispatches to stateful_body's selectAll via TimelineTypeContext props - // as scope of response data required to actually set selectedEvents - const selectAllOnAllPagesCallback = useCallback(() => { - dispatch( - timelineActions.setTGridSelectAll({ - id: timelineId, - selectAll: true, - }) - ); - setShowClearSelectionAction(true); - }, [dispatch, timelineId]); - - const updateAlertsStatusCallback: UpdateAlertsStatusCallback = useCallback( - async ( - refetchQuery: inputsModel.Refetch, - { status, selectedStatus }: UpdateAlertsStatusProps - ) => { - await updateAlertStatusAction({ - query: showClearSelectionAction - ? getGlobalQuery(buildAlertStatusFilter(status))?.filterQuery - : undefined, - alertIds: Object.keys(selectedEventIds), - selectedStatus, - setEventsDeleted: setEventsDeletedCallback, - setEventsLoading: setEventsLoadingCallback, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - }); - refetchQuery(); - }, - [ - getGlobalQuery, - selectedEventIds, - setEventsDeletedCallback, - setEventsLoadingCallback, - showClearSelectionAction, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - ] - ); - - // Callback for creating the AlertsUtilityBar which receives totalCount from EventsViewer component - const utilityBarCallback = useCallback( - (refetchQuery: inputsModel.Refetch, totalCount: number) => { - return ( - 0} - clearSelection={clearSelectionCallback} - currentFilter={filterGroup} - hasIndexMaintenance={hasIndexMaintenance} - hasIndexWrite={hasIndexWrite} - onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} - onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsChanged} - selectAll={selectAllOnAllPagesCallback} - selectedEventIds={selectedEventIds} - showBuildingBlockAlerts={showBuildingBlockAlerts} - showClearSelection={showClearSelectionAction} - showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts} - totalCount={totalCount} - updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} - /> - ); - }, - [ - clearSelectionCallback, - filterGroup, - hasIndexMaintenance, - hasIndexWrite, - loadingEventIds.length, - onShowBuildingBlockAlertsChanged, - onShowOnlyThreatIndicatorAlertsChanged, - selectAllOnAllPagesCallback, - selectedEventIds, - showBuildingBlockAlerts, - showClearSelectionAction, - showOnlyThreatIndicatorAlerts, - updateAlertsStatusCallback, - ] - ); - const additionalFiltersComponent = useMemo( () => ( - 0} onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} showBuildingBlockAlerts={showBuildingBlockAlerts} @@ -387,7 +208,6 @@ export const AlertsTableComponent: React.FC = ({ rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.detections} start={from} - utilityBar={utilityBarCallback} /> ); }; @@ -414,29 +234,7 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -const mapDispatchToProps = (dispatch: Dispatch) => ({ - clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), - setEventsLoading: ({ - id, - eventIds, - isLoading, - }: { - id: string; - eventIds: string[]; - isLoading: boolean; - }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), - setEventsDeleted: ({ - id, - eventIds, - isDeleted, - }: { - id: string; - eventIds: string[]; - isDeleted: boolean; - }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); +const connector = connect(makeMapStateToProps); type PropsFromRedux = ConnectedProps; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index ea66ebb132d8a6b..197d655d2420bfd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -10,7 +10,7 @@ import { EuiButton, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { useResponderActionItem } from '../endpoint_responder'; import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; +import { TAKE_ACTION } from '../alerts_table/additional_filters_action/translations'; import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; import { useInvestigateInTimeline } from '../alerts_table/timeline_actions/use_investigate_in_timeline'; diff --git a/x-pack/plugins/security_solution/public/management/mocks/fleet_mocks.ts b/x-pack/plugins/security_solution/public/management/mocks/fleet_mocks.ts index 4370bd9e2421505..1e5cc83bb0bcc8c 100644 --- a/x-pack/plugins/security_solution/public/management/mocks/fleet_mocks.ts +++ b/x-pack/plugins/security_solution/public/management/mocks/fleet_mocks.ts @@ -10,7 +10,10 @@ import type { CheckPermissionsResponse, GetAgentPoliciesResponse, GetAgentStatusResponse, + GetPackagePoliciesResponse, GetPackagesResponse, + BulkGetPackagePoliciesResponse, + BulkGetAgentPoliciesResponse, } from '@kbn/fleet-plugin/common'; import { AGENT_API_ROUTES, @@ -25,6 +28,7 @@ import { httpHandlerMockFactory } from '../../common/mock/endpoint/http_handler_ import { EndpointDocGenerator } from '../../../common/endpoint/generate_data'; import type { GetPolicyListResponse, GetPolicyResponse } from '../pages/policy/types'; import { FleetAgentPolicyGenerator } from '../../../common/endpoint/data_generators/fleet_agent_policy_generator'; +import { FleetPackagePolicyGenerator } from '../../../common/endpoint/data_generators/fleet_package_policy_generator'; interface KqlArgumentType { type: string; @@ -216,8 +220,141 @@ export const fleetGetAgentPolicyListHttpMock = return { items: requiredPolicyIds.map((packagePolicyId) => { - return agentPolicyGenerator.generate({ - package_policies: [packagePolicyId], + return agentPolicyGenerator.generate({}); + }), + perPage: Math.max(requiredPolicyIds.length, 10), + total: requiredPolicyIds.length, + page: 1, + }; + }, + }, + ]); + +export type FleetBulkGetAgentPolicyListHttpMockInterface = ResponseProvidersInterface<{ + agentPolicy: () => BulkGetAgentPoliciesResponse; +}>; +export const fleetBulkGetAgentPolicyListHttpMock = + httpHandlerMockFactory([ + { + id: 'agentPolicy', + path: AGENT_POLICY_API_ROUTES.BULK_GET_PATTERN, + method: 'post', + handler: ({ body }) => { + const generator = new EndpointDocGenerator('seed'); + const agentPolicyGenerator = new FleetAgentPolicyGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const requiredPolicyIds: string[] = [ + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the first endpoint metadata generated is using. This is needed especially when testing the + // Endpoint Details flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + endpointMetadata.Endpoint.policy.applied.id, + + // In addition, some of our UI logic looks for the existence of certain Endpoint Integration policies + // using the Agents Policy API (normally when checking IDs since query by ids is not supported via API) + // so also add the first two package policy IDs that the `fleetGetEndpointPackagePolicyListHttpMock()` + // method above creates (which Trusted Apps HTTP mocks also use) + // FIXME: remove hard-coded IDs below and get them from the new FleetPackagePolicyGenerator (#2262) + 'ddf6570b-9175-4a6d-b288-61a09771c647', + 'b8e616ae-44fc-4be7-846c-ce8fa5c082dd', + ]; + + return { + items: requiredPolicyIds.map((packagePolicyId) => { + return agentPolicyGenerator.generate({}); + }), + }; + }, + }, + ]); + +export type FleetBulkGetPackagePoliciesListHttpMockInterface = ResponseProvidersInterface<{ + packagePolicies: () => BulkGetPackagePoliciesResponse; +}>; +export const fleetBulkGetPackagePoliciesListHttpMock = + httpHandlerMockFactory([ + { + id: 'packagePolicies', + path: PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN, + method: 'post', + handler: ({ body }) => { + const generator = new EndpointDocGenerator('seed'); + const fleetPackagePolicyGenerator = new FleetPackagePolicyGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const requiredPolicyIds: string[] = [ + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the first endpoint metadata generated is using. This is needed especially when testing the + // Endpoint Details flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + endpointMetadata.Endpoint.policy.applied.id, + + // In addition, some of our UI logic looks for the existence of certain Endpoint Integration policies + // using the Agents Policy API (normally when checking IDs since query by ids is not supported via API) + // so also add the first two package policy IDs that the `fleetGetEndpointPackagePolicyListHttpMock()` + // method above creates (which Trusted Apps HTTP mocks also use) + // FIXME: remove hard-coded IDs below and get them from the new FleetPackagePolicyGenerator (#2262) + 'ddf6570b-9175-4a6d-b288-61a09771c647', + 'b8e616ae-44fc-4be7-846c-ce8fa5c082dd', + + // And finally, include any kql filters for package policies ids + ...getPackagePoliciesFromKueryString( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${( + JSON.parse(body?.toString() ?? '{}')?.ids as string[] + ).join(' or ')} )` + ), + ]; + + return { + items: requiredPolicyIds.map((packagePolicyId) => { + return fleetPackagePolicyGenerator.generate({ + id: packagePolicyId, + }); + }), + }; + }, + }, + ]); + +export type FleetGetPackagePoliciesListHttpMockInterface = ResponseProvidersInterface<{ + packagePolicies: () => GetPackagePoliciesResponse; +}>; +export const fleetGetPackagePoliciesListHttpMock = + httpHandlerMockFactory([ + { + id: 'packagePolicies', + path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, + method: 'get', + handler: ({ query }) => { + const generator = new EndpointDocGenerator('seed'); + const fleetPackagePolicyGenerator = new FleetPackagePolicyGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const requiredPolicyIds: string[] = [ + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the first endpoint metadata generated is using. This is needed especially when testing the + // Endpoint Details flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + endpointMetadata.Endpoint.policy.applied.id, + + // In addition, some of our UI logic looks for the existence of certain Endpoint Integration policies + // using the Agents Policy API (normally when checking IDs since query by ids is not supported via API) + // so also add the first two package policy IDs that the `fleetGetEndpointPackagePolicyListHttpMock()` + // method above creates (which Trusted Apps HTTP mocks also use) + // FIXME: remove hard-coded IDs below and get them from the new FleetPackagePolicyGenerator (#2262) + 'ddf6570b-9175-4a6d-b288-61a09771c647', + 'b8e616ae-44fc-4be7-846c-ce8fa5c082dd', + + // And finally, include any kql filters for package policies ids + ...getPackagePoliciesFromKueryString( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${(query?.ids as string[]).join( + ' or ' + )} )` + ), + ]; + + return { + items: requiredPolicyIds.map((packagePolicyId) => { + return fleetPackagePolicyGenerator.generate({ + id: packagePolicyId, }); }), perPage: Math.max(requiredPolicyIds.length, 10), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 4cc923cf8d86bd9..7dfd55664acc705 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -38,6 +38,9 @@ import { fleetGetAgentPolicyListHttpMock, fleetGetCheckPermissionsHttpMock, fleetGetPackageListHttpMock, + fleetBulkGetPackagePoliciesListHttpMock, + fleetBulkGetAgentPolicyListHttpMock, + fleetGetPackagePoliciesListHttpMock, } from '../../mocks'; type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{ @@ -133,6 +136,9 @@ export const endpointListFleetApisHttpMock = composeHttpHandlerMocks([ fleetGetPackageListHttpMock, fleetGetAgentPolicyListHttpMock, + fleetBulkGetPackagePoliciesListHttpMock, + fleetBulkGetAgentPolicyListHttpMock, + fleetGetPackagePoliciesListHttpMock, fleetGetCheckPermissionsHttpMock, ]); type EndpointPageHttpMockInterface = EndpointMetadataHttpMocksInterface & diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 70b31f88b2f7640..7d1fd0a3d77fee0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -50,6 +50,7 @@ import { jest.mock('../../../services/policies/ingest', () => ({ sendGetAgentConfigList: () => Promise.resolve({ items: [] }), sendGetAgentPolicyList: () => Promise.resolve({ items: [] }), + sendBulkGetPackagePolicies: () => Promise.resolve({ items: [] }), sendGetEndpointSecurityPackage: () => Promise.resolve({ version: '1.1.1' }), sendGetFleetAgentsWithEndpoint: () => Promise.resolve({ total: 0 }), })); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 2194e51ec5aaf6f..fb0e1949ad75728 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -9,7 +9,6 @@ import type { DataViewBase, Query } from '@kbn/es-query'; import type { CoreStart, HttpStart } from '@kbn/core/public'; import type { Dispatch } from 'redux'; import semverGte from 'semver/functions/gte'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import { BASE_POLICY_RESPONSE_ROUTE, HOST_METADATA_GET_ROUTE, @@ -41,7 +40,7 @@ import { createLoadingResourceState, } from '../../../state'; import { - sendGetAgentPolicyList, + sendBulkGetPackagePolicies, sendGetEndpointSecurityPackage, sendGetFleetAgentsWithEndpoint, } from '../../../services/policies/ingest'; @@ -173,19 +172,12 @@ const getAgentAndPoliciesForEndpointsList = async ( // Package Ids that it uses, thus if a reference exists there, then the package policy (policy) // exists. const policiesFound = ( - await sendGetAgentPolicyList(http, { - query: { - kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${policyIdsToCheck.join( - ' or ' - )})`, - }, - }) + await sendBulkGetPackagePolicies(http, policyIdsToCheck) ).items.reduce( - (list, agentPolicy) => { - (agentPolicy.package_policies as string[]).forEach((packagePolicy) => { - list.packagePolicy[packagePolicy as string] = true; - list.agentPolicy[packagePolicy as string] = agentPolicy.id; - }); + (list, packagePolicy) => { + list.packagePolicy[packagePolicy.id as string] = true; + list.agentPolicy[packagePolicy.id as string] = packagePolicy.policy_id; + return list; }, { packagePolicy: {}, agentPolicy: {} } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index 03a0fdda897976d..5a983574f7545cb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -11,6 +11,7 @@ import type { GetAgentPoliciesResponseItem, GetPackagesResponse, GetAgentsResponse, + BulkGetPackagePoliciesResponse, } from '@kbn/fleet-plugin/common/types/rest_spec'; import type { GetHostPolicyResponse, @@ -120,9 +121,6 @@ const endpointListApiPathHandlerMocks = ({ // Do policies referenced in endpoint list exist // just returns 1 single agent policy that includes all of the packagePolicy IDs provided [INGEST_API_AGENT_POLICIES]: (): GetAgentPoliciesResponse => { - (agentPolicy.package_policies as string[]).push( - ...endpointPackagePolicies.map((packagePolicy) => packagePolicy.id) - ); return { items: [agentPolicy], total: 10, @@ -146,6 +144,13 @@ const endpointListApiPathHandlerMocks = ({ }; }, + // List of Policies (package policies) for onboarding + [`${INGEST_API_PACKAGE_POLICIES}/_bulk_get`]: (): BulkGetPackagePoliciesResponse => { + return { + items: endpointPackagePolicies, + }; + }, + // List of Agents using Endpoint [INGEST_API_FLEET_AGENTS]: (): GetAgentsResponse => { return { @@ -209,4 +214,15 @@ export const setEndpointListApiMockImplementation: ( throw new Error(`MOCK: api request does not have a mocked handler: ${path}`); }); + + mockedHttpService.post.mockImplementation(async (...args) => { + const [path] = args; + if (typeof path === 'string') { + if (apiHandlers[path]) { + return apiHandlers[path](); + } + } + + throw new Error(`MOCK: api request does not have a mocked handler: ${path}`); + }); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 132b5684fa8c0ac..1d47f70227df373 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -1066,9 +1066,11 @@ describe('when on the endpoint list page', () => { const packagePolicy = docGenerator.generatePolicyPackagePolicy(); packagePolicy.id = hosts[0].metadata.Endpoint.policy.applied.id; + const agentPolicy = generator.generateAgentPolicy(); agentPolicyId = agentPolicy.id; agentId = hosts[0].metadata.elastic.agent.id; + packagePolicy.policy_id = agentPolicyId; setEndpointListApiMockImplementation(coreStart.http, { endpointsResults: hostInfo, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index 66137aab4cea1e7..5b74d99d6abcdf8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -12,7 +12,7 @@ import { createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { sendGetEndpointSpecificPackagePolicies } from '../../../services/policies/policies'; import { sendGetEndpointSpecificPackagePoliciesMock } from '../../../services/policies/test_mock_utils'; import { PolicyList } from './policy_list'; -import { sendGetAgentPolicyList } from '../../../services/policies/ingest'; +import { sendBulkGetAgentPolicyList } from '../../../services/policies/ingest'; import type { GetPolicyListResponse } from '../types'; import { getEndpointListPath, getPoliciesPath } from '../../../common/routing'; import { APP_UI_ID } from '../../../../../common/constants'; @@ -22,7 +22,7 @@ jest.mock('../../../services/policies/ingest'); const getPackagePolicies = sendGetEndpointSpecificPackagePolicies as jest.Mock; -const getAgentPolicies = sendGetAgentPolicyList as jest.Mock; +const mockedSendBulkGetAgentPolicies = sendBulkGetAgentPolicyList as jest.Mock; describe('When on the policy list page', () => { let render: () => ReturnType; @@ -77,19 +77,19 @@ describe('When on the policy list page', () => { beforeEach(async () => { getPackagePolicies.mockReturnValue(policies); - getAgentPolicies.mockReturnValue({ + mockedSendBulkGetAgentPolicies.mockReturnValue({ items: [ - { package_policies: [policies.items[0].id], agents: 4 }, - { package_policies: [policies.items[1].id], agents: 2 }, - { package_policies: [policies.items[2].id], agents: 5 }, - { package_policies: [policies.items[3].id], agents: 1 }, - { package_policies: [policies.items[4].id], agents: 3 }, + { package_policies: [{ id: policies.items[0].id }], agents: 4 }, + { package_policies: [{ id: policies.items[1].id }], agents: 2 }, + { package_policies: [{ id: policies.items[2].id }], agents: 5 }, + { package_policies: [{ id: policies.items[3].id }], agents: 1 }, + { package_policies: [{ id: policies.items[4].id }], agents: 3 }, ], }); render(); await waitFor(() => { expect(sendGetEndpointSpecificPackagePolicies).toHaveBeenCalled(); - expect(sendGetAgentPolicyList).toHaveBeenCalled(); + expect(sendBulkGetAgentPolicyList).toHaveBeenCalled(); }); }); it('should display the policy list table', () => { @@ -164,7 +164,7 @@ describe('When on the policy list page', () => { await waitFor(() => { expect(getPackagePolicies).toHaveBeenCalled(); expect(sendGetEndpointSpecificPackagePolicies).toHaveBeenCalled(); - expect(sendGetAgentPolicyList).toHaveBeenCalled(); + expect(mockedSendBulkGetAgentPolicies).toHaveBeenCalled(); }); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 43a7223260f28c9..33fd25d0d15cf4a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -19,7 +19,6 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { useLocation } from 'react-router-dom'; -import type { AgentPolicy } from '@kbn/fleet-plugin/common'; import type { CreatePackagePolicyRouteState } from '@kbn/fleet-plugin/public'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; import { AdministrationListPage } from '../../../components/administration_list_page'; @@ -58,10 +57,14 @@ export const PolicyList = memo(() => { // endpoint count per policy const policyIds = useMemo(() => data?.items.map((policies) => policies.id) ?? [], [data]); + const agentPolicyIds = useMemo( + () => data?.items.map((policies) => policies.policy_id) ?? [], + [data] + ); const { data: endpointCount = { items: [] } } = useGetAgentCountForPolicy({ - policyIds, + agentPolicyIds, customQueryOptions: { - enabled: policyIds.length > 0, + enabled: agentPolicyIds.length > 0, onError: (err) => { toasts.addDanger( i18n.translate('xpack.securitySolution.policyList.endpointCountError', { @@ -76,7 +79,7 @@ export const PolicyList = memo(() => { const { data: endpointPackageInfo, isFetching: packageIsFetching } = useGetEndpointSecurityPackage({ customQueryOptions: { - enabled: policyIds.length === 0, + enabled: agentPolicyIds.length === 0, onError: (err) => { toasts.addDanger( i18n.translate('xpack.securitySolution.policyList.packageVersionError', { @@ -88,11 +91,14 @@ export const PolicyList = memo(() => { }); const policyIdToEndpointCount = useMemo(() => { - const map = new Map(); - for (const policy of endpointCount?.items) { - for (const packagePolicyId of policy.package_policies) { - if (policyIds.includes(packagePolicyId as string)) { - map.set(packagePolicyId, policy.agents ?? 0); + const map = new Map(); + + for (const agentPolicy of endpointCount?.items) { + if (agentPolicy.package_policies) { + for (const packagePolicy of agentPolicy.package_policies) { + if (policyIds.includes(packagePolicy.id)) { + map.set(packagePolicy.id, agentPolicy.agents ?? 0); + } } } } diff --git a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts index fef45b657d5da14..3c0810b0d551b77 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts @@ -8,10 +8,9 @@ import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query import { useQuery } from '@tanstack/react-query'; import type { IHttpFetchError } from '@kbn/core-http-browser'; import type { GetAgentPoliciesResponse, GetPackagesResponse } from '@kbn/fleet-plugin/common'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import { useHttp } from '../../../common/lib/kibana'; import { MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants'; -import { sendGetAgentPolicyList, sendGetEndpointSecurityPackage } from './ingest'; +import { sendBulkGetAgentPolicyList, sendGetEndpointSecurityPackage } from './ingest'; import type { GetPolicyListResponse } from '../../pages/policy/types'; import { sendGetEndpointSpecificPackagePolicies } from './policies'; import type { ServerApiError } from '../../../common/types'; @@ -53,22 +52,17 @@ export function useGetEndpointSpecificPolicies( * This hook returns the fleet agent policies list filtered by policy id */ export function useGetAgentCountForPolicy({ - policyIds, + agentPolicyIds, customQueryOptions, }: { - policyIds: string[]; + agentPolicyIds: string[]; customQueryOptions?: UseQueryOptions; }): QueryObserverResult { const http = useHttp(); return useQuery( - ['endpointCountForPolicy', policyIds], + ['endpointCountForPolicy', agentPolicyIds], () => { - return sendGetAgentPolicyList(http, { - query: { - perPage: 50, - kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${policyIds.join(' or ')})`, - }, - }); + return sendBulkGetAgentPolicyList(http, agentPolicyIds); }, customQueryOptions ); diff --git a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts index b77ab8a725ecf63..7690348391ad769 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts @@ -12,6 +12,7 @@ import type { GetPackagesResponse, GetAgentPoliciesRequest, GetAgentPoliciesResponse, + GetPackagePoliciesResponse, } from '@kbn/fleet-plugin/common'; import type { NewPolicyData } from '../../../../common/endpoint/types'; import type { GetPolicyResponse, UpdatePolicyResponse } from '../../pages/policy/types'; @@ -37,6 +38,26 @@ export const sendGetPackagePolicy = ( return http.get(`${INGEST_API_PACKAGE_POLICIES}/${packagePolicyId}`, options); }; +/** + * Retrieves multiple package policies by ids + * @param http + * @param packagePolicyIds + * @param options + */ +export const sendBulkGetPackagePolicies = ( + http: HttpStart, + packagePolicyIds: string[], + options?: HttpFetchOptions +) => { + return http.post(`${INGEST_API_PACKAGE_POLICIES}/_bulk_get`, { + ...options, + body: JSON.stringify({ + ids: packagePolicyIds, + ignoreMissing: true, + }), + }); +}; + /** * Retrieve a list of Agent Policies * @param http @@ -49,6 +70,26 @@ export const sendGetAgentPolicyList = ( return http.get(INGEST_API_AGENT_POLICIES, options); }; +/** + * Retrieve a list of Agent Policies + * @param http + * @param options + */ +export const sendBulkGetAgentPolicyList = ( + http: HttpStart, + ids: string[], + options: HttpFetchOptions = {} +) => { + return http.post(`${INGEST_API_AGENT_POLICIES}/_bulk_get`, { + ...options, + body: JSON.stringify({ + ids, + ignoreMissing: true, + full: true, + }), + }); +}; + /** * Updates a package policy * diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 6ecaa56bc12db88..681de8ac4cb0c99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -84,11 +84,8 @@ const GraphOverlayComponent: React.FC = ({ const { timelineFullScreen } = useTimelineFullScreen(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const graphEventId = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId - ); - const sessionViewConfig = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewConfig + const { graphEventId, sessionViewConfig } = useDeepEqualSelector( + (state) => getTimeline(state, timelineId) ?? timelineDefaults ); const fullScreen = useMemo( diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts index fc95c7a025393d9..d99b65943985919 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts @@ -64,14 +64,7 @@ type AgentPolicyWithPackagePolicies = Omit & { const isAgentPolicyWithPackagePolicies = ( agentPolicy: AgentPolicy | AgentPolicyWithPackagePolicies ): agentPolicy is AgentPolicyWithPackagePolicies => { - if ( - agentPolicy.package_policies.length === 0 || - typeof agentPolicy.package_policies[0] !== 'string' - ) { - return true; - } - - return false; + return agentPolicy.package_policies ? true : false; }; export class EndpointMetadataService { diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 5bf0dc53701fc08..538605bb591b0c9 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -43,7 +43,15 @@ export const ALERT_STATUS = { export const LOCAL_STORAGE_DISPLAY_OPTIONS_KEY = 'sessionView:displayOptions'; export const MOUSE_EVENT_PLACEHOLDER = { stopPropagation: () => undefined } as React.MouseEvent; export const DEBOUNCE_TIMEOUT = 500; -export const DEFAULT_TTY_PLAYSPEED_MS = 40; // milli seconds per line of tty output. +export const DEFAULT_TTY_PLAYSPEED_MS = 80; // milli seconds per line of tty output. +export const DEFAULT_TTY_FONT_SIZE = 11; + +// we split terminal output on both newlines and cursor movements. +export const TTY_LINE_SPLITTER_REGEX = /(\r?\n|\x1b\[\d+;\d+[Hf])/gi; + +// used when searching output +export const TTY_STRIP_CONTROL_CODES_REGEX = + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/gi; // when showing the count of alerts in details panel tab, if the number // exceeds ALERT_COUNT_THRESHOLD we put a + next to it, e.g 999+ diff --git a/x-pack/plugins/session_view/common/mocks/responses/session_view_io_events.mock.ts b/x-pack/plugins/session_view/common/mocks/responses/session_view_io_events.mock.ts index 33b9ac56b035df1..c14dccc2bc3d654 100644 --- a/x-pack/plugins/session_view/common/mocks/responses/session_view_io_events.mock.ts +++ b/x-pack/plugins/session_view/common/mocks/responses/session_view_io_events.mock.ts @@ -17,6 +17,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = { message: 'hello world security', event: { action: 'text_output', + id: '1', }, process: { entity_id: '1', @@ -30,7 +31,15 @@ export const sessionViewIOEventsMock: ProcessEventResults = { total_bytes_captured: 1024, total_bytes_skipped: 0, bytes_skipped: [], - text: "256\n,\n Some Companies Puppet instance\n | | | CentOS Stream release 8 on x86_64\n .=/ = = =| =| = === = Load average: 1.23, 1.01, 0.63\n | || || || || || | | | | \n /= = = =' =' =' ' =' Hostname ********\n \\ Type xyz\n o Datacenter ********\n Cluster ********\n\n\n\n\n,0 loaded units listed. Pass --all to see loaded but inactive units, too.\nTo show all installed unit files use 'systemctl list-unit-files'.\n", + text: "256\n,\n Some Companies Puppet instance\n | | | CentOS Stream release 8 on x86_64\n *********************** Load average: 1.23, 1.01, 0.63\n ************************ \n ************************ Hostname ********\n \\ Type xyz\n o Datacenter ********\n Cluster ********\n\n\n\n\n,0 loaded units listed. Pass --all to see loaded but inactive units, too.\nTo show all installed unit files use 'systemctl list-unit-files'.\n", + }, + tty: { + char_device: { + major: 4, + minor: 1, + }, + rows: 59, + columns: 173, }, }, }, @@ -44,6 +53,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = { message: 'hello world security', event: { action: 'text_output', + id: '1', }, process: { entity_id: '2', @@ -57,7 +67,15 @@ export const sessionViewIOEventsMock: ProcessEventResults = { total_bytes_captured: 1024, total_bytes_skipped: 0, bytes_skipped: [], - text: ',\u001b[?2004h\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?2004h\u001b[1;59r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[59;1H"/usr/local/bin/galera_traffic_start.sh" [readonly] 14L, 397C\u001b[1;1H#!/bin/env bash\n# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\n# Script for setting the reject of queries in Galera\n\nmysql -h127.0.0.1 -P6033 -uroot -e "set global wsrep_reject_queries=\'NONE\'" 2>&1\nRC=$?\n\nif [[ $RC != 0 ]]; then\n >&2 echo "Failed to unset the reject of queries on Galera node, exiting."\n exit $RC\nelse\n echo "Successfully unset the reject of queries."\nfi\n\u001b[94m~ \u001b[16;1H~ \u001b[17;1H~ \u001b[18;1H~ \u001b[19;1H~ \u001b[20;1H~ \u001b[21;1H~ \u001b[22;1H~ \u001b[23;1H~ \u001b[24;1H~ \u001b[25;1H~ \u001b[26;1H~ \u001b[27;1H~ \u001b[28;1H~ \u001b[29;1H~ \u001b[30;1H~ \u001b[31;1H~ \u001b[32;1H~ \u001b[33;1H~ \u001b[34;1H~ \u001b[35;1H~ \u001b[36;1H~ \u001b[37;1H~ \u001b[38;1H~ \u001b[39;1H~ \u001b[40;1H~ \u001b[41;1H~ \u001b[42;1H~ \u001b[43;1H~ \u001b[44;1H~ \u001b[45;1H~ \u001b[46;1H~ \u001b[47;1H~ \u001b[48;1H~ \u001b[49;1H~ \u001b[50;1H~ \u001b[51;1H~ \u001b[52;1H~ \u001b[53;1H~ \u001b[54;1H~ \u001b[55;1H~ \u001b[56;1H~ \u001b[57;1H~ \u001b[58;1H~ \u001b[1;1H\u001b[?25h\u0007\u001b[?25l\u001b[m\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hq\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001b[?2004h\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?2004h\u001b[1;59r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[59;1H"/usr/local/bin/galera_traffic_stop.sh" [readonly] 115L, 3570C\u001b[1;1H#!/bin/env bash\n# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\n# Script for rejecting connection on Galera cluster node, either gracefully or not,\n# depending on supplied arguments.\n\nfunction usage() {\n echo "\n This script disables DB connections to Galera node.\n The default is to stop them gracefully.\n\n Usage: $0 [-h] [-w ] [-s ] [-x]\n\n Options:\n -h Prints this help.\n -w Number of seconds for waiting to close the connections.\u001b[17;11HDefault value is to wait for mysql-wait_timeout.\n -s Sleep interval between connections checks.\n -x Kills all connections immediately. Other options are ignored."\n exit\n}\n', + text: ',\u001b[?2004h\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?2004h\u001b[1;59r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[59;1H"/usr/local/bin/script_one.sh" [readonly] 14L, 397C\u001b[1;1H#!/bin/env bash\n# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\n# Script for setting the reject of queries in Mysql\n\nmysql -h127.0.0.1 -P6033 -uroot -e "set global wsrep_reject_queries=\'NONE\'" 2>&1\nRC=$?\n\nif [[ $RC != 0 ]]; then\n >&2 echo "Failed to unset the reject of queries on Mysql node, exiting."\n exit $RC\nelse\n echo "Successfully unset the reject of queries."\nfi\n\u001b[94m~ \u001b[16;1H~ \u001b[17;1H~ \u001b[18;1H~ \u001b[19;1H~ \u001b[20;1H~ \u001b[21;1H~ \u001b[22;1H~ \u001b[23;1H~ \u001b[24;1H~ \u001b[25;1H~ \u001b[26;1H~ \u001b[27;1H~ \u001b[28;1H~ \u001b[29;1H~ \u001b[30;1H~ \u001b[31;1H~ \u001b[32;1H~ \u001b[33;1H~ \u001b[34;1H~ \u001b[35;1H~ \u001b[36;1H~ \u001b[37;1H~ \u001b[38;1H~ \u001b[39;1H~ \u001b[40;1H~ \u001b[41;1H~ \u001b[42;1H~ \u001b[43;1H~ \u001b[44;1H~ \u001b[45;1H~ \u001b[46;1H~ \u001b[47;1H~ \u001b[48;1H~ \u001b[49;1H~ \u001b[50;1H~ \u001b[51;1H~ \u001b[52;1H~ \u001b[53;1H~ \u001b[54;1H~ \u001b[55;1H~ \u001b[56;1H~ \u001b[57;1H~ \u001b[58;1H~ \u001b[1;1H\u001b[?25h\u0007\u001b[?25l\u001b[m\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hq\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001b[?2004h\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?2004h\u001b[1;59r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[59;1H"/usr/local/bin/script_two.sh" [readonly] 115L, 3570C\u001b[1;1H#!/bin/env bash\n# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\n# Script for rejecting connection on Mysql cluster node, either gracefully or not,\n# depending on supplied arguments.\n\nfunction usage() {\n echo "\n This script disables DB connections to Mysql node.\n The default is to stop them gracefully.\n\n Usage: $0 [-h] [-w ] [-s ] [-x]\n\n Options:\n -h Prints this help.\n -w Number of seconds for waiting to close the connections.\u001b[17;11HDefault value is to wait for mysql-wait_timeout.\n -s Sleep interval between connections checks.\n -x Kills all connections immediately. Other options are ignored."\n exit\n}\n', + }, + tty: { + char_device: { + major: 4, + minor: 1, + }, + rows: 59, + columns: 173, }, }, }, @@ -71,6 +89,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = { message: 'hello world security', event: { action: 'text_output', + id: '1', }, process: { entity_id: '2', @@ -84,7 +103,15 @@ export const sessionViewIOEventsMock: ProcessEventResults = { total_bytes_captured: 1024, total_bytes_skipped: 0, bytes_skipped: [], - text: '\nfunction get_number_db_connections() {\n # count current\n DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select count(1) from stats_mysql_processlist where user = \'$DB_USER\' and db like \'db\\_%\' escapee\u001b[26;1H \'\\\'")\n}\n\nfunction set_number_grace_seconds() {\n local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select variable_value from global_variables where variable_name = \'mysql-wait_timeout\'")\n GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n}\n\nfunction wait_for_connections() {\n local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\u001b[37;5Hecho "Waiting for connections to close for up to $GRACE_PERIOD seconds"\u001b[39;5Hfor i in $(seq 0 $number_of_loops); do\u001b[40;9Hget_number_db_connections\u001b[41;9Hif [[ $DB_CONNECTIONS_NUMBER -eq 0 ]]; then\u001b[42;13Hecho "No connection found for user $DB_USER to this node"\u001b[43;13Hbreak\u001b[44;9Helse\u001b[45;13Hecho "$DB_CONNECTIONS_NUMBER connection(s) found, waiting for ${SLEEP_INTERVAL}s, round $i"\u001b[46;13Hsleep $SLEEP_INTERVAL\u001b[47;9Hfi\n done\n}\n\nfunction parse_args() {\n while getopts \'hs:w:x\' opt; do\u001b[53;9Hcase "$opt" in\u001b[54;9Hh)\u001b[55;13Husage\u001b[56;13H;;\u001b[57;9Hs)\u001b[58;13Hif ! [[ $OPTARG =~ ^[0-9]+$ ]]; then\u001b[1;1H\u001b[?25h\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hset number\r\u001b[?25l\u001b[1;1H\u001b[38;5;130m 1 \u001b[m#!/bin/env bash\n\u001b[38;5;130m 2 \u001b[m# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\u001b[38;5;130m 3 \n 4 \u001b[m# Script for rejecting connection on Galera cluster node, either gracefully or not,\n\u001b[38;5;130m 5 \u001b[m# depending on supplied arguments.\n\u001b[38;5;130m 6 \n 7 \u001b[mfunction usage() {\n\u001b[38;5;130m 8 \u001b[m echo "\n\u001b[38;5;130m 9 \u001b[m This script disables DB connections to Galera node.\n\u001b[38;5;130m 10 \u001b[m The default is to stop them gracefully.\n\u001b[38;5;130m 11 \n 12 \u001b[m Usage: $0 [-h] [-w ] [-s ] [-x]\n\u001b[38;5;130m 13 \n 14 \u001b[m Options:\n\u001b[38;5;130m 15 \u001b[m -h Prints this help.\n\u001b[38;5;130m 16 \u001b[m -w Number of seconds for waiting to close the connections.\n\u001b[38;5;130m 17 \u001b[m Default value is to wait for mysql-wait_timeout.\n\u001b[38;5;130m 18 \u001b[m -s Sleep interval between connections checks.\n\u001b[38;5;130m 19 \u001b[m -x Kills all connections immediately. Other options are ignored."\n\u001b[38;5;130m 20 \u001b[m exit\n\u001b[38;5;130m 21 \u001b[m}\n\u001b[38;5;130m 22 \n 23 \u001b[mfunction get_number_db_connections() {\n\u001b[38;5;130m 24 \u001b[m # count current\n\u001b[38;5;130m 25 \u001b[m DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select count(1) from stats_mysql_processlist where user = \'$DB_USER\' and db like \'db\\_%%\u001b[26;1H\u001b[38;5;130m \u001b[m\' escape \'\\\'")\n\u001b[38;5;130m 26 \u001b[m}\n\u001b[38;5;130m 27 \n 28 \u001b[mfunction set_number_grace_seconds() {\n\u001b[38;5;130m 29 \u001b[m local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select variable_value from global_variables where variable_name = \'mysql-wait_timm\u001b[31;1H\u001b[38;5;130m \u001b[meout\'")\u001b[31;16H\u001b[K\u001b[32;1H\u001b[38;5;130m 30 \u001b[m GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n\u001b[38;5;130m 31 \u001b[m}\n\u001b[38;5;130m 32 \u001b[m\u001b[34;10H\u001b[K\u001b[35;1H\u001b[38;5;130m 33 \u001b[mfunction wait_for_connections() {\u001b[35;42H\u001b[K\u001b[36;1H\u001b[38;5;130m 34 \u001b[m local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\n\u001b[38;5;130m 35 \u001b[m\u001b[37;10H\u001b[K\u001b[38;1H\u001b[38;5;130m 36 \u001b[m echo "Waiting for connections to close for up to $GRACE_PERIOD seconds"\n\u001b[38;5;130m 37 \u001b[m\u001b[39;9H\u001b[K\u001b[40;1H\u001b[38;5;130m 38 \u001b[m for i in $(seq 0 $number_of_loops); do\n', + text: '\nfunction get_number_db_connections() {\n # count current\n DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select count(1) from stats_mysql_processlist where user = \'$DB_USER\' and db like \'db\\_%\' escapee\u001b[26;1H \'\\\'")\n}\n\nfunction set_number_grace_seconds() {\n local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select variable_value from global_variables where variable_name = \'mysql-wait_timeout\'")\n GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n}\n\nfunction wait_for_connections() {\n local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\u001b[37;5Hecho "Waiting for connections to close for up to $GRACE_PERIOD seconds"\u001b[39;5Hfor i in $(seq 0 $number_of_loops); do\u001b[40;9Hget_number_db_connections\u001b[41;9Hif [[ $DB_CONNECTIONS_NUMBER -eq 0 ]]; then\u001b[42;13Hecho "No connection found for user $DB_USER to this node"\u001b[43;13Hbreak\u001b[44;9Helse\u001b[45;13Hecho "$DB_CONNECTIONS_NUMBER connection(s) found, waiting for ${SLEEP_INTERVAL}s, round $i"\u001b[46;13Hsleep $SLEEP_INTERVAL\u001b[47;9Hfi\n done\n}\n\nfunction parse_args() {\n while getopts \'hs:w:x\' opt; do\u001b[53;9Hcase "$opt" in\u001b[54;9Hh)\u001b[55;13Husage\u001b[56;13H;;\u001b[57;9Hs)\u001b[58;13Hif ! [[ $OPTARG =~ ^[0-9]+$ ]]; then\u001b[1;1H\u001b[?25h\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hset number\r\u001b[?25l\u001b[1;1H\u001b[38;5;130m 1 \u001b[m#!/bin/env bash\n\u001b[38;5;130m 2 \u001b[m# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\u001b[38;5;130m 3 \n 4 \u001b[m# Script for rejecting connection on Mysql cluster node, either gracefully or not,\n\u001b[38;5;130m 5 \u001b[m# depending on supplied arguments.\n\u001b[38;5;130m 6 \n 7 \u001b[mfunction usage() {\n\u001b[38;5;130m 8 \u001b[m echo "\n\u001b[38;5;130m 9 \u001b[m This script disables DB connections to Mysql node.\n\u001b[38;5;130m 10 \u001b[m The default is to stop them gracefully.\n\u001b[38;5;130m 11 \n 12 \u001b[m Usage: $0 [-h] [-w ] [-s ] [-x]\n\u001b[38;5;130m 13 \n 14 \u001b[m Options:\n\u001b[38;5;130m 15 \u001b[m -h Prints this help.\n\u001b[38;5;130m 16 \u001b[m -w Number of seconds for waiting to close the connections.\n\u001b[38;5;130m 17 \u001b[m Default value is to wait for mysql-wait_timeout.\n\u001b[38;5;130m 18 \u001b[m -s Sleep interval between connections checks.\n\u001b[38;5;130m 19 \u001b[m -x Kills all connections immediately. Other options are ignored."\n\u001b[38;5;130m 20 \u001b[m exit\n\u001b[38;5;130m 21 \u001b[m}\n\u001b[38;5;130m 22 \n 23 \u001b[mfunction get_number_db_connections() {\n\u001b[38;5;130m 24 \u001b[m # count current\n\u001b[38;5;130m 25 \u001b[m DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select count(1) from stats_mysql_processlist where user = \'$DB_USER\' and db like \'db\\_%%\u001b[26;1H\u001b[38;5;130m \u001b[m\' escape \'\\\'")\n\u001b[38;5;130m 26 \u001b[m}\n\u001b[38;5;130m 27 \n 28 \u001b[mfunction set_number_grace_seconds() {\n\u001b[38;5;130m 29 \u001b[m local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select variable_value from global_variables where variable_name = \'mysql-wait_timm\u001b[31;1H\u001b[38;5;130m \u001b[meout\'")\u001b[31;16H\u001b[K\u001b[32;1H\u001b[38;5;130m 30 \u001b[m GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n\u001b[38;5;130m 31 \u001b[m}\n\u001b[38;5;130m 32 \u001b[m\u001b[34;10H\u001b[K\u001b[35;1H\u001b[38;5;130m 33 \u001b[mfunction wait_for_connections() {\u001b[35;42H\u001b[K\u001b[36;1H\u001b[38;5;130m 34 \u001b[m local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\n\u001b[38;5;130m 35 \u001b[m\u001b[37;10H\u001b[K\u001b[38;1H\u001b[38;5;130m 36 \u001b[m echo "Waiting for connections to close for up to $GRACE_PERIOD seconds"\n\u001b[38;5;130m 37 \u001b[m\u001b[39;9H\u001b[K\u001b[40;1H\u001b[38;5;130m 38 \u001b[m for i in $(seq 0 $number_of_loops); do\n', + }, + tty: { + char_device: { + major: 4, + minor: 1, + }, + rows: 59, + columns: 173, }, }, }, @@ -98,6 +125,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = { message: 'hello world security', event: { action: 'text_output', + id: '1', }, process: { entity_id: '2', @@ -113,6 +141,14 @@ export const sessionViewIOEventsMock: ProcessEventResults = { bytes_skipped: [], text: '\u001b[38;5;130m 39 \u001b[m get_number_db_connections\u001b[41;42H\u001b[K\u001b[42;1H\u001b[38;5;130m 40 \u001b[m if [[ $DB_CONNECTIONS_NUMBER -eq 0 ]]; then\u001b[42;60H\u001b[K\u001b[43;1H\u001b[38;5;130m 41 \u001b[m echo "No connection found for user $DB_USER to this node"\n\u001b[38;5;130m 42 \u001b[m \u001b[8Cbreak\n\u001b[38;5;130m 43 \u001b[m else\u001b[45;21H\u001b[K\u001b[46;1H\u001b[38;5;130m 44 \u001b[m echo "$DB_CONNECTIONS_NUMBER connection(s) found, waiting for ${SLEEP_INTERVAL}s, round $i"\n\u001b[38;5;130m 45 \u001b[m \u001b[10Csleep $SLEEP_INTERVAL\n\u001b[38;5;130m 46 \u001b[m\u001b[8Cfi\n\u001b[38;5;130m 47 \u001b[m done\n\u001b[38;5;130m 48 \u001b[m}\n\u001b[38;5;130m 49 \u001b[m\u001b[51;10H\u001b[K\u001b[52;1H\u001b[38;5;130m 50 \u001b[mfunction parse_args() {\u001b[52;33H\u001b[K\u001b[53;1H\u001b[38;5;130m 51 \u001b[m while getopts \'hs:w:x\' opt; do\n\u001b[38;5;130m 52 \u001b[m case "$opt" in\n\u001b[38;5;130m 53 \u001b[m h)\n\u001b[38;5;130m 54 \u001b[m usage\n\u001b[38;5;130m 55 \u001b[m \u001b[10C;;\n\u001b[38;5;130m 56 \u001b[m s)\u001b[58;19H\u001b[K\u001b[1;9H\u001b[?25h\u001b[?25l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[1;1H\u001b[38;5;130m 58 \u001b[m\u001b[16C>&2 echo "Sleep interval (-s) must be a number"\n\u001b[38;5;130m 59 \u001b[m\u001b[16Cexit 1\n\u001b[38;5;130m 60 \u001b[m\u001b[12Cfi\n\u001b[38;5;130m 61 \u001b[m\u001b[12CARG_SLEEP_INTERVAL="$OPTARG"\n\u001b[38;5;130m 62 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 63 \u001b[m\u001b[8Cw)\n\u001b[38;5;130m 64 \u001b[m\u001b[12Cif ! [[ $OPTARG =~ ^[0-9]+$ ]]; then\n\u001b[38;5;130m 65 \u001b[m\u001b[16C>&2 echo "Wait timeout (-w) must be a number"\n\u001b[38;5;130m 66 \u001b[m\u001b[16Cexit 1\n\u001b[38;5;130m 67 \u001b[m\u001b[12Cfi\n\u001b[38;5;130m 68 \u001b[m\u001b[12CARG_GRACE_PERIOD="$OPTARG"\n\u001b[38;5;130m 69 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 70 \u001b[m\u001b[8Cx)\n\u001b[38;5;130m 71 \u001b[m\u001b[12CARG_KILL_IMMEDIATELY=1\n\u001b[38;5;130m 72 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 73 \u001b[m\u001b[8Cesac\n\u001b[38;5;130m 74 \u001b[m done\n\u001b[38;5;130m 75 \n 76 \u001b[m GRACE_PERIOD=${ARG_GRACE_PERIOD:--1}\n\u001b[38;5;130m 77 \u001b[m SLEEP_INTERVAL=${ARG_SLEEP_INTERVAL:-30}\n\u001b[38;5;130m 78 \u001b[m KILL_IMMEDIATELY=${ARG_KILL_IMMEDIATELY:-0}\n\u001b[38;5;130m 79 \u001b[m}\n\u001b[38;5;130m 80 \n 81 \u001b[mDB_USER="rolap01"\n\u001b[38;5;130m 82 \n 83 \u001b[mparse_args $@\n\u001b[38;5;130m 84 \n 85 \u001b[mif [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 86 \u001b[m echo "WARNING: Not waiting for connections to close gracefully"\n\u001b[38;5;130m 87 \u001b[m echo "Press any key to continue... wsrep_reject_queries will be set to \'ALL_KILL\'"\n\u001b[38;5;130m 88 \u001b[m read a\n\u001b[38;5;130m 89 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL_KILL\'"\n\u001b[38;5;130m 90 \u001b[melse\n\u001b[38;5;130m 91 \u001b[m # Stop accepting queries in mariadb, do not kill opened connections\n\u001b[38;5;130m 92 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL\'"\n\u001b[38;5;130m 93 \u001b[mfi\n\u001b[38;5;130m 94 \n 95 \u001b[mexit_code=$?\n', }, + tty: { + char_device: { + major: 4, + minor: 1, + }, + rows: 59, + columns: 173, + }, }, }, }, @@ -125,6 +161,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = { message: 'hello world security', event: { action: 'text_output', + id: '1', }, process: { entity_id: '2', @@ -138,7 +175,15 @@ export const sessionViewIOEventsMock: ProcessEventResults = { total_bytes_captured: 1024, total_bytes_skipped: 0, bytes_skipped: [], - text: '\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo "Failed to set the reject of queries on Galera node, exiting."\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo "Successfully stopped accepting queries."\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo "ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections."\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[1;1H\u001b[38;5;130m 1 \u001b[m#!/bin/env bash\n\u001b[38;5;130m 2 \u001b[m# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\u001b[38;5;130m 3 \n 4 \u001b[m# Script for rejecting connection on Galera cluster node, either gracefully or not,\n\u001b[38;5;130m 5 \u001b[m# depending on supplied arguments.\n\u001b[38;5;130m 6 \n 7 \u001b[mfunction usage() {\n\u001b[38;5;130m 8 \u001b[m echo "\n\u001b[38;5;130m 9 \u001b[m This script disables DB connections to Galera node.\n\u001b[38;5;130m 10 \u001b[m The default is to stop them gracefully.\n\u001b[38;5;130m 11 \n 12 \u001b[m Usage: $0 [-h] [-w ] [-s ] [-x]\n\u001b[38;5;130m 13 \n 14 \u001b[m Options:\n\u001b[38;5;130m 15 \u001b[m -h Prints this help.\n\u001b[38;5;130m 16 \u001b[m -w Number of seconds for waiting to close the connections.\n\u001b[38;5;130m 17 \u001b[m\u001b[10CDefault value is to wait for mysql-wait_timeout.\n\u001b[38;5;130m 18 \u001b[m -s Sleep interval between connections checks.\n\u001b[38;5;130m 19 \u001b[m -x Kills all connections immediately. Other options are ignored."\n\u001b[38;5;130m 20 \u001b[m exit\n\u001b[38;5;130m 21 \u001b[m}\n\u001b[38;5;130m 22 \n 23 \u001b[mfunction get_number_db_connections() {\n\u001b[38;5;130m 24 \u001b[m # count current\n\u001b[38;5;130m 25 \u001b[m DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select count(1) from stats_mysql_processlist where user = \'$DB_USER\' and db like \'db\\_%%\u001b[26;1H\u001b[38;5;130m \u001b[m\' escape \'\\\'")\n\u001b[38;5;130m 26 \u001b[m}\n\u001b[38;5;130m 27 \n 28 \u001b[mfunction set_number_grace_seconds() {\n\u001b[38;5;130m 29 \u001b[m local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select variable_value from global_variables where variable_name = \'mysql-wait_timm\u001b[31;1H\u001b[38;5;130m \u001b[meout\'")\n\u001b[38;5;130m 30 \u001b[m GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n\u001b[38;5;130m 31 \u001b[m}\n\u001b[38;5;130m 32 \n', + text: '\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo "Failed to set the reject of queries on Mysql node, exiting."\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo "Successfully stopped accepting queries."\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo "ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections."\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[1;1H\u001b[38;5;130m 1 \u001b[m#!/bin/env bash\n\u001b[38;5;130m 2 \u001b[m# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\u001b[38;5;130m 3 \n 4 \u001b[m# Script for rejecting connection on Mysql cluster node, either gracefully or not,\n\u001b[38;5;130m 5 \u001b[m# depending on supplied arguments.\n\u001b[38;5;130m 6 \n 7 \u001b[mfunction usage() {\n\u001b[38;5;130m 8 \u001b[m echo "\n\u001b[38;5;130m 9 \u001b[m This script disables DB connections to Mysql node.\n\u001b[38;5;130m 10 \u001b[m The default is to stop them gracefully.\n\u001b[38;5;130m 11 \n 12 \u001b[m Usage: $0 [-h] [-w ] [-s ] [-x]\n\u001b[38;5;130m 13 \n 14 \u001b[m Options:\n\u001b[38;5;130m 15 \u001b[m -h Prints this help.\n\u001b[38;5;130m 16 \u001b[m -w Number of seconds for waiting to close the connections.\n\u001b[38;5;130m 17 \u001b[m\u001b[10CDefault value is to wait for mysql-wait_timeout.\n\u001b[38;5;130m 18 \u001b[m -s Sleep interval between connections checks.\n\u001b[38;5;130m 19 \u001b[m -x Kills all connections immediately. Other options are ignored."\n\u001b[38;5;130m 20 \u001b[m exit\n\u001b[38;5;130m 21 \u001b[m}\n\u001b[38;5;130m 22 \n 23 \u001b[mfunction get_number_db_connections() {\n\u001b[38;5;130m 24 \u001b[m # count current\n\u001b[38;5;130m 25 \u001b[m DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select count(1) from stats_mysql_processlist where user = \'$DB_USER\' and db like \'db\\_%%\u001b[26;1H\u001b[38;5;130m \u001b[m\' escape \'\\\'")\n\u001b[38;5;130m 26 \u001b[m}\n\u001b[38;5;130m 27 \n 28 \u001b[mfunction set_number_grace_seconds() {\n\u001b[38;5;130m 29 \u001b[m local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e "select variable_value from global_variables where variable_name = \'mysql-wait_timm\u001b[31;1H\u001b[38;5;130m \u001b[meout\'")\n\u001b[38;5;130m 30 \u001b[m GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n\u001b[38;5;130m 31 \u001b[m}\n\u001b[38;5;130m 32 \n', + }, + tty: { + char_device: { + major: 4, + minor: 1, + }, + rows: 59, + columns: 173, }, }, }, @@ -152,6 +197,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = { message: 'hello world security', event: { action: 'text_output', + id: '1', }, process: { entity_id: '2', @@ -167,6 +213,14 @@ export const sessionViewIOEventsMock: ProcessEventResults = { bytes_skipped: [], text: ' 33 \u001b[mfunction wait_for_connections() {\n\u001b[38;5;130m 34 \u001b[m local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\n\u001b[38;5;130m 35 \n 36 \u001b[m echo "Waiting for connections to close for up to $GRACE_PERIOD seconds"\n\u001b[38;5;130m 37 \n 38 \u001b[m for i in $(seq 0 $number_of_loops); do\n\u001b[38;5;130m 39 \u001b[m\u001b[8Cget_number_db_connections\n\u001b[38;5;130m 40 \u001b[m\u001b[8Cif [[ $DB_CONNECTIONS_NUMBER -eq 0 ]]; then\n\u001b[38;5;130m 41 \u001b[m\u001b[12Cecho "No connection found for user $DB_USER to this node"\n\u001b[38;5;130m 42 \u001b[m\u001b[12Cbreak\n\u001b[38;5;130m 43 \u001b[m\u001b[8Celse\n\u001b[38;5;130m 44 \u001b[m\u001b[12Cecho "$DB_CONNECTIONS_NUMBER connection(s) found, waiting for ${SLEEP_INTERVAL}s, round $i"\n\u001b[38;5;130m 45 \u001b[m\u001b[12Csleep $SLEEP_INTERVAL\n\u001b[38;5;130m 46 \u001b[m\u001b[8Cfi\n\u001b[38;5;130m 47 \u001b[m done\n\u001b[38;5;130m 48 \u001b[m}\n\u001b[38;5;130m 49 \n 50 \u001b[mfunction parse_args() {\n\u001b[38;5;130m 51 \u001b[m while getopts \'hs:w:x\' opt; do\n\u001b[38;5;130m 52 \u001b[m\u001b[8Ccase "$opt" in\n\u001b[38;5;130m 53 \u001b[m\u001b[8Ch)\n\u001b[38;5;130m 54 \u001b[m\u001b[12Cusage\n\u001b[38;5;130m 55 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 56 \u001b[m\u001b[8Cs)\u001b[1;9H\u001b[?25h\u001b[?25l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[1;1H\u001b[38;5;130m 58 \u001b[m\u001b[16C>&2 echo "Sleep interval (-s) must be a number"\n\u001b[38;5;130m 59 \u001b[m\u001b[16Cexit 1\n\u001b[38;5;130m 60 \u001b[m\u001b[12Cfi\n\u001b[38;5;130m 61 \u001b[m\u001b[12CARG_SLEEP_INTERVAL="$OPTARG"\n\u001b[38;5;130m 62 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 63 \u001b[m\u001b[8Cw)\n\u001b[38;5;130m 64 \u001b[m\u001b[12Cif ! [[ $OPTARG =~ ^[0-9]+$ ]]; then\n\u001b[38;5;130m 65 \u001b[m\u001b[16C>&2 echo "Wait timeout (-w) must be a number"\n\u001b[38;5;130m 66 \u001b[m\u001b[16Cexit 1\n\u001b[38;5;130m 67 \u001b[m\u001b[12Cfi\n\u001b[38;5;130m 68 \u001b[m\u001b[12CARG_GRACE_PERIOD="$OPTARG"\n\u001b[38;5;130m 69 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 70 \u001b[m\u001b[8Cx)\n\u001b[38;5;130m 71 \u001b[m\u001b[12CARG_KILL_IMMEDIATELY=1\n\u001b[38;5;130m 72 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 73 \u001b[m\u001b[8Cesac\n\u001b[38;5;130m 74 \u001b[m done\n\u001b[38;5;130m 75 \n 76 \u001b[m GRACE_PERIOD=${ARG_GRACE_PERIOD:--1}\n\u001b[38;5;130m 77 \u001b[m SLEEP_INTERVAL=${ARG_SLEEP_INTERVAL:-30}\n\u001b[38;5;130m 78 \u001b[m KILL_IMMEDIATELY=${ARG_KILL_IMMEDIATELY:-0}\n\u001b[38;5;130m 79 \u001b[m}\n\u001b[38;5;130m 80 \n 81 \u001b[mDB_USER="rolap01"\n\u001b[38;5;130m 82 \n 83 \u001b[mparse_args $@\n', }, + tty: { + char_device: { + major: 4, + minor: 1, + }, + rows: 59, + columns: 173, + }, }, }, }, @@ -179,6 +233,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = { message: 'hello world security', event: { action: 'text_output', + id: '1', }, process: { entity_id: '2', @@ -192,7 +247,15 @@ export const sessionViewIOEventsMock: ProcessEventResults = { total_bytes_captured: 1024, total_bytes_skipped: 0, bytes_skipped: [], - text: '\u001b[38;5;130m 84 \n 85 \u001b[mif [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 86 \u001b[m echo "WARNING: Not waiting for connections to close gracefully"\n\u001b[38;5;130m 87 \u001b[m echo "Press any key to continue... wsrep_reject_queries will be set to \'ALL_KILL\'"\n\u001b[38;5;130m 88 \u001b[m read a\n\u001b[38;5;130m 89 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL_KILL\'"\n\u001b[38;5;130m 90 \u001b[melse\n\u001b[38;5;130m 91 \u001b[m # Stop accepting queries in mariadb, do not kill opened connections\n\u001b[38;5;130m 92 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL\'"\n\u001b[38;5;130m 93 \u001b[mfi\n\u001b[38;5;130m 94 \n 95 \u001b[mexit_code=$?\n\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo "Failed to set the reject of queries on Galera node, exiting."\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo "Successfully stopped accepting queries."\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo "ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections."\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\nType :qa! and press to abandon all changes and exit Vim\u0007\u001b[58;9H\u001b[?25h\u0007\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hqa!\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001bkroot@staging-host:~\u001b\\\n', + text: '\u001b[38;5;130m 84 \n 85 \u001b[mif [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 86 \u001b[m echo "WARNING: Not waiting for connections to close gracefully"\n\u001b[38;5;130m 87 \u001b[m echo "Press any key to continue... wsrep_reject_queries will be set to \'ALL_KILL\'"\n\u001b[38;5;130m 88 \u001b[m read a\n\u001b[38;5;130m 89 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL_KILL\'"\n\u001b[38;5;130m 90 \u001b[melse\n\u001b[38;5;130m 91 \u001b[m # Stop accepting queries in mariadb, do not kill opened connections\n\u001b[38;5;130m 92 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL\'"\n\u001b[38;5;130m 93 \u001b[mfi\n\u001b[38;5;130m 94 \n 95 \u001b[mexit_code=$?\n\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo "Failed to set the reject of queries on Mysql node, exiting."\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo "Successfully stopped accepting queries."\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo "ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections."\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\nType :qa! and press to abandon all changes and exit Vim\u0007\u001b[58;9H\u001b[?25h\u0007\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hqa!\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001bkroot@staging-host:~\u001b\\\n', + }, + tty: { + char_device: { + major: 4, + minor: 1, + }, + rows: 59, + columns: 173, }, }, }, @@ -206,6 +269,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = { message: 'hello world security', event: { action: 'text_output', + id: '1', }, process: { entity_id: '1', @@ -219,7 +283,15 @@ export const sessionViewIOEventsMock: ProcessEventResults = { total_bytes_captured: 1024, total_bytes_skipped: 0, bytes_skipped: [], - text: '\u001bkroot@staging-host:~\u001b\\\b\b\b\b\u001b[1P\b\b\b\b\u001b[1P\b\b\b\b\u001b[1P\b\b\b\b\b\b\b\b\b\n\u001bkroot@staging-host:~\u001b\\\b\u001b[K\b\u001b[K\b\u001b[K\n,\n22/05/26 09:24:09 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] vi -R /usr/local/bin/galera_traffic_start.sh\u0007\n22/05/26 09:25:32 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] vi -R /usr/local/bin/galera_traffic_start.sh.sh.sh.sho.shp.sh\n22/05/26 09:30:08 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] exi\u0007\u0007\u0007exitlogout\n,\u001bec2-user@staging-host:~\u001b\\\n\u001bec2-user@staging-host:~\u001b\\\n,\n22/05/26 09:24:01 rack-na/cl_md (md), Cluster ********\n[ec2-user@staging-host:~] sudo -i\n22/05/26 10:11:37 rack-na/cl_md (md), Cluster ********\n[ec2-user@staging-host:~] exitlogout\n\n', + text: '\u001bkroot@staging-host:~\u001b\\\b\b\b\b\u001b[1P\b\b\b\b\u001b[1P\b\b\b\b\u001b[1P\b\b\b\b\b\b\b\b\b\n\u001bkroot@staging-host:~\u001b\\\b\u001b[K\b\u001b[K\b\u001b[K\n,\n22/05/26 09:24:09 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] vi -R /usr/local/bin/script_one.sh\u0007\n22/05/26 09:25:32 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] vi -R /usr/local/bin/script_one.sh.sh.sh.sho.shp.sh\n22/05/26 09:30:08 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] exi\u0007\u0007\u0007exitlogout\n,\u001bec2-user@staging-host:~\u001b\\\n\u001bec2-user@staging-host:~\u001b\\\n,\n22/05/26 09:24:01 rack-na/cl_md (md), Cluster ********\n[ec2-user@staging-host:~] sudo -i\n22/05/26 10:11:37 rack-na/cl_md (md), Cluster ********\n[ec2-user@staging-host:~] exitlogout\n\n', + }, + tty: { + char_device: { + major: 4, + minor: 1, + }, + rows: 59, + columns: 173, }, }, }, diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts index e620b654db16ffa..a2b40347f1b5f65 100644 --- a/x-pack/plugins/session_view/common/types/process_tree/index.ts +++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts @@ -60,16 +60,13 @@ export interface Teletype { major?: number; minor?: number; }; + rows?: number; + columns?: number; } -// used by tty_player component to split process.io.text into lines of IO export interface IOLine { - value?: string; - - // the following is only set client side for caching purposes - process_name?: string; - process_entity_id?: string; - process_entity_cursor?: string; + event: ProcessEvent; + value: string; } export interface IOFields { diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index e2d795283c7ce2e..921a3f7ce8b9c4c 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -133,8 +133,8 @@ export const SessionView = ({ } = useFetchSessionViewAlerts(sessionEntityId, investigatedAlertId); const handleRefresh = useCallback(() => { - refetch({ refetchPage: (page, index, allPages) => allPages.length - 1 === index }); - refetchAlerts({ refetchPage: (page, index, allPages) => allPages.length - 1 === index }); + refetch({ refetchPage: (_page, index, allPages) => allPages.length - 1 === index }); + refetchAlerts({ refetchPage: (_page, index, allPages) => allPages.length - 1 === index }); }, [refetch, refetchAlerts]); const alerts = useMemo(() => { diff --git a/x-pack/plugins/session_view/public/components/tty_player/hooks.test.tsx b/x-pack/plugins/session_view/public/components/tty_player/hooks.test.tsx index 23f81d7941439ca..fb9095187438596 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/hooks.test.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player/hooks.test.tsx @@ -8,7 +8,9 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock'; import { useIOLines, useXtermPlayer, XtermPlayerDeps } from './hooks'; import { ProcessEventsPage } from '../../../common/types/process_tree'; -import { DEFAULT_TTY_PLAYSPEED_MS } from '../../../common/constants'; +import { DEFAULT_TTY_FONT_SIZE, DEFAULT_TTY_PLAYSPEED_MS } from '../../../common/constants'; + +const VIM_LINE_START = 19; describe('TTYPlayer/hooks', () => { beforeAll(() => { @@ -66,10 +68,11 @@ describe('TTYPlayer/hooks', () => { initialProps = { ref: mockRef, isPlaying: false, + setIsPlaying: jest.fn(), lines, hasNextPage: false, fetchNextPage: () => null, - isFullscreen: false, + fontSize: DEFAULT_TTY_FONT_SIZE, }; }); @@ -88,7 +91,7 @@ describe('TTYPlayer/hooks', () => { expect(currentLine).toBe(0); act(() => { - seekToLine(17); // line where vim output starts + seekToLine(VIM_LINE_START); // line where vim output starts }); jest.advanceTimersByTime(100); @@ -102,14 +105,14 @@ describe('TTYPlayer/hooks', () => { }); act(() => { - xTermResult.current.seekToLine(17); // line where vim output starts + xTermResult.current.seekToLine(VIM_LINE_START); // line where vim output starts }); jest.advanceTimersByTime(100); const { terminal, currentLine } = xTermResult.current; - expect(currentLine).toBe(17); + expect(currentLine).toBe(VIM_LINE_START); expect(terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('#!/bin/env bash'); }); @@ -152,7 +155,7 @@ describe('TTYPlayer/hooks', () => { act(() => { jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * initialProps.lines.length + 100); }); - expect(result.current.currentLine).toBe(initialProps.lines.length); + expect(result.current.currentLine).toBe(initialProps.lines.length - 1); }); it('will allow a plain text search highlight on the last line printed', async () => { diff --git a/x-pack/plugins/session_view/public/components/tty_player/hooks.ts b/x-pack/plugins/session_view/public/components/tty_player/hooks.ts index e122a2dbd0c112e..0138775199c6d03 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/hooks.ts +++ b/x-pack/plugins/session_view/public/components/tty_player/hooks.ts @@ -6,13 +6,16 @@ */ import { Terminal } from 'xterm'; import 'xterm/css/xterm.css'; -import { FitAddon } from 'xterm-addon-fit'; import { useMemo, useState, useEffect, useCallback } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { CoreStart } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { SearchAddon } from './xterm_search'; import { useEuiTheme } from '../../hooks'; + +// eslint-disable-next-line @kbn/imports/no_boundary_crossing +import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock'; + import { IOLine, ProcessEvent, @@ -24,8 +27,12 @@ import { IO_EVENTS_PER_PAGE, QUERY_KEY_IO_EVENTS, DEFAULT_TTY_PLAYSPEED_MS, + DEFAULT_TTY_FONT_SIZE, + TTY_LINE_SPLITTER_REGEX, } from '../../../common/constants'; +const MOCK_DEBUG = false; // This code will be removed once we have an agent to test with. + export const useFetchIOEvents = (sessionEntityId: string) => { const { http } = useKibana().services; const cachingKeys = useMemo(() => [QUERY_KEY_IO_EVENTS, sessionEntityId], [sessionEntityId]); @@ -41,13 +48,18 @@ export const useFetchIOEvents = (sessionEntityId: string) => { }, }); + if (MOCK_DEBUG) { + res.events = sessionViewIOEventsMock.events; + res.total = res.events?.length || 0; + } + const events = res.events?.map((event: any) => event._source as ProcessEvent) ?? []; return { events, cursor, total: res.total }; }, { getNextPageParam: (lastPage) => { - if (lastPage.events.length >= IO_EVENTS_PER_PAGE) { + if (!MOCK_DEBUG && lastPage.events.length >= IO_EVENTS_PER_PAGE) { return { cursor: lastPage.events[lastPage.events.length - 1]['@timestamp'], }; @@ -77,9 +89,21 @@ export const useIOLines = (pages: ProcessEventsPage[] | undefined) => { return pages.reduce((previous, current) => { if (current.events) { current.events.forEach((event) => { - if (event?.process?.io?.text) { - const data: IOLine[] = event.process.io.text.split(/\n\r?/).map((line) => { + const { process } = event; + if (process?.io?.text !== undefined) { + const splitLines = process.io.text.split(TTY_LINE_SPLITTER_REGEX); + const combinedLines = [splitLines[0]]; + + // delimiters e.g \r\n or cursor movements are merged with their line text + // we start on an odd number so that cursor movements happen at the start of each line + // this is needed for the search to work accurately + for (let i = 1; i < splitLines.length - 1; i = i + 2) { + combinedLines.push(splitLines[i] + splitLines[i + 1]); + } + + const data: IOLine[] = combinedLines.map((line) => { return { + event, // pointer to the event so it's easy to look up other details for the line value: line, }; }); @@ -99,85 +123,94 @@ export const useIOLines = (pages: ProcessEventsPage[] | undefined) => { export interface XtermPlayerDeps { ref: React.RefObject; isPlaying: boolean; + setIsPlaying(value: boolean): void; lines: IOLine[]; + fontSize: number; hasNextPage?: boolean; fetchNextPage?: () => void; - isFullscreen?: boolean; } export const useXtermPlayer = ({ ref, isPlaying, + setIsPlaying, lines, + fontSize, hasNextPage, fetchNextPage, - isFullscreen, }: XtermPlayerDeps) => { const { euiTheme } = useEuiTheme(); const { font, colors } = euiTheme; const [currentLine, setCurrentLine] = useState(0); - const [userSeeked, setUserSeeked] = useState(false); const [playSpeed] = useState(DEFAULT_TTY_PLAYSPEED_MS); // potentially configurable + const tty = lines?.[currentLine]?.event.process?.tty; - const [terminal, fitAddon, searchAddon] = useMemo(() => { + const [terminal, searchAddon] = useMemo(() => { const term = new Terminal({ theme: { - background: 'rgba(0,0,0,0)', selection: colors.warning, }, fontFamily: font.familyCode, - fontSize: 11, - allowTransparency: true, + fontSize: DEFAULT_TTY_FONT_SIZE, + scrollback: 0, + convertEol: true, }); - const fitInstance = new FitAddon(); const searchInstance = new SearchAddon(); - - term.loadAddon(fitInstance); term.loadAddon(searchInstance); - return [term, fitInstance, searchInstance]; - }, [colors, font]); + return [term, searchInstance]; + }, [font, colors]); useEffect(() => { - if (ref.current) { + if (ref.current && !terminal.element) { terminal.open(ref.current); } }, [terminal, ref]); - useEffect(() => { - // isFullscreen check is there just to avoid the necessary "unnecessary" react-hook dep - // When isFullscreen changes, e.g goes from false to true and vice versa, we need to call fit. - if (isFullscreen !== undefined) { - fitAddon.fit(); - } - }, [isFullscreen, fitAddon]); - const render = useCallback( - (lineNumber: number) => { + (lineNumber: number, clear: boolean) => { if (lines.length === 0) { return; } let linesToPrint; - if (userSeeked) { - linesToPrint = lines.slice(0, lineNumber); + if (clear) { + linesToPrint = lines.slice(0, lineNumber + 1); + terminal.reset(); terminal.clear(); - setUserSeeked(false); } else { linesToPrint = [lines[lineNumber]]; } linesToPrint.forEach((line, index) => { if (line?.value !== undefined) { - terminal.writeln(line.value); + terminal.write(line.value); } }); }, - [terminal, lines, userSeeked] + [terminal, lines] ); + useEffect(() => { + const fontChanged = terminal.getOption('fontSize') !== fontSize; + const ttyChanged = tty && (terminal.rows !== tty?.rows || terminal.cols !== tty?.columns); + + if (fontChanged) { + terminal.setOption('fontSize', fontSize); + } + + if (tty?.rows && tty?.columns && ttyChanged) { + terminal.resize(tty.columns, tty.rows); + } + + if (fontChanged || ttyChanged) { + // clear and rerender + render(currentLine, true); + } + }, [currentLine, fontSize, terminal, render, tty]); + useEffect(() => { if (isPlaying) { const timer = setTimeout(() => { @@ -185,29 +218,32 @@ export const useXtermPlayer = ({ return; } - if (currentLine < lines.length) { + if (currentLine < lines.length - 1) { setCurrentLine(currentLine + 1); } + + render(currentLine, false); + + if (hasNextPage && fetchNextPage && currentLine === lines.length - 1) { + fetchNextPage(); + } }, playSpeed); return () => { - clearInterval(timer); + clearTimeout(timer); }; } - }, [lines, currentLine, isPlaying, playSpeed]); + }, [lines, currentLine, isPlaying, playSpeed, render, hasNextPage, fetchNextPage]); - useEffect(() => { - render(currentLine); - - if (hasNextPage && fetchNextPage && currentLine === lines.length - 1) { - fetchNextPage(); - } - }, [fetchNextPage, currentLine, lines, render, hasNextPage]); + const seekToLine = useCallback( + (index) => { + setCurrentLine(index); + setIsPlaying(false); - const seekToLine = useCallback((line) => { - setUserSeeked(true); - setCurrentLine(line); - }, []); + render(index, true); + }, + [setIsPlaying, render] + ); const search = useCallback( (query: string, startCol: number) => { @@ -216,15 +252,10 @@ export const useXtermPlayer = ({ [searchAddon] ); - const fit = useCallback(() => { - fitAddon.fit(); - }, [fitAddon]); - return { terminal, currentLine, seekToLine, search, - fit, }; }; diff --git a/x-pack/plugins/session_view/public/components/tty_player/index.tsx b/x-pack/plugins/session_view/public/components/tty_player/index.tsx index 48dbf82441e427a..7d3656d8ba7b7f3 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player/index.tsx @@ -7,6 +7,7 @@ import React, { useRef, useState, useCallback, ChangeEvent, MouseEvent } from 'react'; import { EuiPanel, EuiRange, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; import { TTYSearchBar } from '../tty_search_bar'; +import { TTYTextSizer } from '../tty_text_sizer'; import { useStyles } from './styles'; import { useFetchIOEvents, useIOLines, useXtermPlayer } from './hooks'; @@ -16,22 +17,31 @@ export interface TTYPlayerDeps { isFullscreen: boolean; } +const DEFAULT_FONT_SIZE = 11; + export const TTYPlayer = ({ sessionEntityId, onClose, isFullscreen }: TTYPlayerDeps) => { - const styles = useStyles(); - const ref = useRef(null); + const ref = useRef(null); + const scrollRef = useRef(null); const { data, fetchNextPage, hasNextPage } = useFetchIOEvents(sessionEntityId); const lines = useIOLines(data?.pages); + + const [fontSize, setFontSize] = useState(DEFAULT_FONT_SIZE); const [isPlaying, setIsPlaying] = useState(false); + const { search, currentLine, seekToLine } = useXtermPlayer({ ref, isPlaying, + setIsPlaying, lines, + fontSize, hasNextPage, fetchNextPage, - isFullscreen, }); + const tty = lines?.[currentLine]?.event?.process?.tty; + const styles = useStyles(tty); + const onLineChange = useCallback( (event: ChangeEvent | MouseEvent) => { const line = parseInt((event?.target as HTMLInputElement).value || '0', 10); @@ -65,7 +75,10 @@ export const TTYPlayer = ({ sessionEntityId, onClose, isFullscreen }: TTYPlayerD -
+ +
+
+
{/* the following will be replaced by a new component */} + + +
diff --git a/x-pack/plugins/session_view/public/components/tty_player/styles.ts b/x-pack/plugins/session_view/public/components/tty_player/styles.ts index c4061c8a64dce65..e1d18e307502734 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/styles.ts +++ b/x-pack/plugins/session_view/public/components/tty_player/styles.ts @@ -7,15 +7,14 @@ import { useMemo } from 'react'; import { CSSObject, css } from '@emotion/react'; -import { transparentize, useEuiScrollBar } from '@elastic/eui'; +import { transparentize } from '@elastic/eui'; import { useEuiTheme } from '../../hooks'; +import { Teletype } from '../../../common/types/process_tree'; -export const useStyles = () => { +export const useStyles = (tty?: Teletype) => { const { euiTheme } = useEuiTheme(); - const euiScrollBar = useEuiScrollBar(); - const cached = useMemo(() => { - const { size, colors, border } = euiTheme; + const { size, font, colors, border } = euiTheme; const container: CSSObject = { position: 'absolute', @@ -23,6 +22,7 @@ export const useStyles = () => { width: '100%', height: '100%', overflow: 'hidden', + zIndex: 10, borderRadius: size.s, backgroundColor: colors.ink, '.euiRangeLevel--warning': { @@ -36,20 +36,47 @@ export const useStyles = () => { }, }; + const windowBoundsColor = transparentize(colors.ghost, 0.6); + const terminal: CSSObject = { + minHeight: '100%', + '.xterm': css` + display: inline-block; + `, + '.xterm-screen': css` + overflow-y: visible; + border: ${border.width.thin} dotted ${windowBoundsColor}; + border-top: 0; + border-left: 0; + box-sizing: content-box; + `, + }; + + if (tty?.rows) { + terminal['.xterm-screen:after'] = css` + position: absolute; + right: ${size.s}; + top: ${size.s}; + content: '${tty?.columns}x${tty?.rows}'; + color: ${windowBoundsColor}; + font-family: ${font.familyCode}; + font-size: ${size.m}; + `; + } + + const scrollPane: CSSObject = { width: '100%', height: 'calc(100% - 142px)', - '.xterm-viewport': css` - ${euiScrollBar} - `, border: border.thin, + overflow: 'auto', }; return { container, terminal, + scrollPane, }; - }, [euiScrollBar, euiTheme]); + }, [tty, euiTheme]); return cached; }; diff --git a/x-pack/plugins/session_view/public/components/tty_player/translations.ts b/x-pack/plugins/session_view/public/components/tty_player/translations.ts new file mode 100644 index 000000000000000..244a5a355b0ec73 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/tty_player/translations.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +export const BETA = i18n.translate('xpack.sessionView.beta', { + defaultMessage: 'Beta', +}); + +export const REFRESH_SESSION = i18n.translate('xpack.sessionView.refreshSession', { + defaultMessage: 'Refresh session', +}); + +export const OPEN_TTY_PLAYER = i18n.translate('xpack.sessionView.openTTYPlayer', { + defaultMessage: 'Open TTY player', +}); diff --git a/x-pack/plugins/session_view/public/components/tty_player/xterm_search.ts b/x-pack/plugins/session_view/public/components/tty_player/xterm_search.ts index 258920319676c9d..3c430d691e3f7c4 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/xterm_search.ts +++ b/x-pack/plugins/session_view/public/components/tty_player/xterm_search.ts @@ -94,7 +94,7 @@ export class SearchAddon implements ITerminalAddon { } if (searchOptions?.lastLineOnly) { - startRow = this._terminal.buffer.active.cursorY - 1; + startRow = this._terminal.buffer.active.cursorY; startCol = searchOptions?.startCol || 0; } @@ -176,10 +176,9 @@ export class SearchAddon implements ITerminalAddon { // Start from selection start if there is a selection startRow = currentSelection.startRow; startCol = currentSelection.startColumn; - } - - if (searchOptions?.lastLineOnly) { + } else if (searchOptions?.lastLineOnly) { startRow = this._terminal.buffer.active.cursorY - 1; + startCol = this._terminal.cols; } this._initLinesCache(); diff --git a/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx b/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx index 4545acf1a457835..77e9c4576c5653e 100644 --- a/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx @@ -56,7 +56,9 @@ describe('TTYSearchBar component', () => { // there is a slight delay in the seek in xtermjs, so we wait 100ms before trying to highlight a result. await new Promise((r) => setTimeout(r, 100)); - expect(props.xTermSearchFn).toHaveBeenCalledTimes(1); + expect(props.xTermSearchFn).toHaveBeenCalledTimes(2); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '', 0); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 6); }); it('calls seekToline and xTermSearchFn when currentMatch changes', async () => { @@ -76,12 +78,13 @@ describe('TTYSearchBar component', () => { // two calls, first instance -h is at line 22, 2nd at line 42 expect(props.seekToLine).toHaveBeenCalledTimes(2); - expect(props.seekToLine).toHaveBeenNthCalledWith(1, 22); - expect(props.seekToLine).toHaveBeenNthCalledWith(2, 42); + expect(props.seekToLine).toHaveBeenNthCalledWith(1, 24); + expect(props.seekToLine).toHaveBeenNthCalledWith(2, 94); - expect(props.xTermSearchFn).toHaveBeenCalledTimes(2); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '-h', 6); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 13); + expect(props.xTermSearchFn).toHaveBeenCalledTimes(3); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '', 0); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 6); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(3, '-h', 13); }); it('calls xTermSearchFn with empty query when search is cleared', async () => { @@ -97,6 +100,6 @@ describe('TTYSearchBar component', () => { userEvent.click(renderResult.getByTestId('clearSearchButton')); await new Promise((r) => setTimeout(r, 100)); - expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '', 0); + expect(props.xTermSearchFn).toHaveBeenNthCalledWith(3, '', 0); }); }); diff --git a/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx b/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx index af60bdf664f9d80..e5081a217dec13b 100644 --- a/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx @@ -4,9 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useMemo, useState, useCallback } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { SessionViewSearchBar } from '../session_view_search_bar'; import { IOLine } from '../../../common/types/process_tree'; +import { TTY_STRIP_CONTROL_CODES_REGEX } from '../../../common/constants'; interface SearchResult { line: IOLine; @@ -24,50 +25,71 @@ export const TTYSearchBar = ({ lines, seekToLine, xTermSearchFn }: TTYSearchBarD const [currentMatch, setCurrentMatch] = useState(null); const [searchQuery, setSearchQuery] = useState(''); - useEffect(() => { - if (currentMatch) { - const goToLine = lines.indexOf(currentMatch.line); - seekToLine(goToLine); - } + const jumpToMatch = useCallback( + (match) => { + if (match) { + const goToLine = lines.indexOf(match.line); + seekToLine(goToLine); + } - const timeout = setTimeout(() => { - return xTermSearchFn(searchQuery, currentMatch?.index || 0); - }, 100); + const timeout = setTimeout(() => { + return xTermSearchFn(searchQuery, match?.index || 0); + }, 100); - return () => { - clearTimeout(timeout); - }; - }, [currentMatch, searchQuery, lines, xTermSearchFn, seekToLine]); + return () => { + clearTimeout(timeout); + }; + }, + [lines, seekToLine, xTermSearchFn, searchQuery] + ); const searchResults = useMemo(() => { - if (searchQuery) { - const matches: SearchResult[] = []; + const matches: SearchResult[] = []; + if (searchQuery) { lines.reduce((previous: SearchResult[], current: IOLine) => { if (current.value) { + // check for cursor movement at the start of the line + const cursorMovement = current.value.match(/^\x1b\[\d+;(\d+)(H|d)/); const regex = new RegExp(searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig'); - const lineMatches = current.value.matchAll(regex); + const lineMatches = current.value + .replace(TTY_STRIP_CONTROL_CODES_REGEX, '') + .replace(/^\r?\n/, '') + .matchAll(regex); + if (lineMatches) { for (const match of lineMatches) { - previous.push({ line: current, match: match[0], index: match.index || 0 }); + let matchOffset = 0; + + if (cursorMovement) { + // the column position 1 based e.g \x1b[39;5H means row 39 column 5 + matchOffset = parseInt(cursorMovement[1], 10) - 3; + } + + previous.push({ + line: current, + match: match[0], + index: matchOffset + (match.index || 0), + }); } } } return previous; }, matches); + } - if (matches.length > 0) { - setCurrentMatch(matches[0]); - } else { - setCurrentMatch(null); - } - - return matches; + if (matches.length > 0) { + const firstMatch = matches[0]; + setCurrentMatch(firstMatch); + jumpToMatch(firstMatch); + } else { + setCurrentMatch(null); + xTermSearchFn('', 0); } - return []; - }, [searchQuery, lines]); + return matches; + }, [searchQuery, lines, jumpToMatch, xTermSearchFn]); const onSearch = useCallback((query) => { setSearchQuery(query); @@ -80,9 +102,10 @@ export const TTYSearchBar = ({ lines, seekToLine, xTermSearchFn }: TTYSearchBarD if (match && currentMatch !== match) { setCurrentMatch(match); + jumpToMatch(match); } }, - [currentMatch, searchResults] + [jumpToMatch, currentMatch, searchResults] ); return ( diff --git a/x-pack/plugins/session_view/public/components/tty_text_sizer/index.test.tsx b/x-pack/plugins/session_view/public/components/tty_text_sizer/index.test.tsx new file mode 100644 index 000000000000000..ec9e8ffd3cab25a --- /dev/null +++ b/x-pack/plugins/session_view/public/components/tty_text_sizer/index.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DEFAULT_TTY_FONT_SIZE } from '../../../common/constants'; +import { TTYTextSizer, TTYTextSizerDeps } from '.'; + +describe('TTYTextSizer component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + let props: TTYTextSizerDeps; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + + props = { + tty: { + rows: 24, + columns: 80, + }, + containerHeight: 200, + fontSize: DEFAULT_TTY_FONT_SIZE, + onFontSizeChanged: jest.fn(), + }; + }); + + it('mounts and renders the text sizer controls', async () => { + renderResult = mockedContext.render(); + expect(renderResult.queryByTestId('sessionView:TTYTextSizer')).toBeTruthy(); + }); + + it('emits a fontSize which will fit the container when ZoomFit clicked', async () => { + renderResult = mockedContext.render(); + + const zoomFitBtn = renderResult.queryByTestId('sessionView:TTYZoomFit'); + + if (zoomFitBtn) { + userEvent.click(zoomFitBtn); + } + + expect(props.onFontSizeChanged).toHaveBeenCalledTimes(1); + expect(props.onFontSizeChanged).toHaveBeenCalledWith(6.41025641025641); + }); + + it('emits a larger fontSize when zoom in clicked', async () => { + renderResult = mockedContext.render(); + + const zoomInBtn = renderResult.queryByTestId('sessionView:TTYZoomIn'); + + if (zoomInBtn) { + userEvent.click(zoomInBtn); + } + + expect(props.onFontSizeChanged).toHaveBeenCalledTimes(1); + expect(props.onFontSizeChanged).toHaveBeenCalledWith(DEFAULT_TTY_FONT_SIZE + 1); + }); + + it('emits a smaller fontSize when zoom out clicked', async () => { + renderResult = mockedContext.render(); + + const zoomOutBtn = renderResult.queryByTestId('sessionView:TTYZoomOut'); + + if (zoomOutBtn) { + userEvent.click(zoomOutBtn); + } + + expect(props.onFontSizeChanged).toHaveBeenCalledTimes(1); + expect(props.onFontSizeChanged).toHaveBeenCalledWith(DEFAULT_TTY_FONT_SIZE - 1); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/tty_text_sizer/index.tsx b/x-pack/plugins/session_view/public/components/tty_text_sizer/index.tsx new file mode 100644 index 000000000000000..463dbbbf80f6984 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/tty_text_sizer/index.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useMemo } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Teletype } from '../../../common/types/process_tree'; +import { DEFAULT_TTY_FONT_SIZE } from '../../../common/constants'; +import { ZOOM_IN, ZOOM_FIT, ZOOM_OUT } from './translations'; + +export interface TTYTextSizerDeps { + tty?: Teletype; + containerHeight: number; + fontSize: number; + onFontSizeChanged(newSize: number): void; +} + +const LINE_HEIGHT_SCALE_RATIO = 1.3; +const MINIMUM_FONT_SIZE = 2; +const MAXIMUM_FONT_SIZE = 20; + +export const TTYTextSizer = ({ + tty, + containerHeight, + fontSize, + onFontSizeChanged, +}: TTYTextSizerDeps) => { + const onFitFontSize = useMemo(() => { + if (tty?.rows && containerHeight) { + const lineHeight = DEFAULT_TTY_FONT_SIZE * LINE_HEIGHT_SCALE_RATIO; + const desiredHeight = tty.rows * lineHeight; + return DEFAULT_TTY_FONT_SIZE * (containerHeight / desiredHeight); + } + + return DEFAULT_TTY_FONT_SIZE; + }, [containerHeight, tty?.rows]); + + const onFit = useCallback(() => { + if (fontSize === onFitFontSize || onFitFontSize > DEFAULT_TTY_FONT_SIZE) { + onFontSizeChanged(DEFAULT_TTY_FONT_SIZE); + } else { + onFontSizeChanged(onFitFontSize); + } + }, [fontSize, onFontSizeChanged, onFitFontSize]); + + const onZoomOut = useCallback(() => { + onFontSizeChanged(Math.max(MINIMUM_FONT_SIZE, fontSize - 1)); + }, [fontSize, onFontSizeChanged]); + + const onZoomIn = useCallback(() => { + onFontSizeChanged(Math.min(MAXIMUM_FONT_SIZE, fontSize + 1)); + }, [fontSize, onFontSizeChanged]); + + return ( + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/tty_text_sizer/translations.ts b/x-pack/plugins/session_view/public/components/tty_text_sizer/translations.ts new file mode 100644 index 000000000000000..0bec7775d43c50b --- /dev/null +++ b/x-pack/plugins/session_view/public/components/tty_text_sizer/translations.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +export const ZOOM_IN = i18n.translate('xpack.sessionView.zoomIn', { + defaultMessage: 'Zoom in', +}); + +export const ZOOM_FIT = i18n.translate('xpack.sessionView.zoomFit', { + defaultMessage: 'Zoom fit', +}); + +export const ZOOM_OUT = i18n.translate('xpack.sessionView.zoomOut', { + defaultMessage: 'Zoom out', +}); diff --git a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts index bf5f56b81bd701c..70b494a42f12103 100644 --- a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts +++ b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts @@ -9,6 +9,7 @@ import { BrowserSimpleFields, CommonFields, DataStream, + FormMonitorType, HTTPAdvancedFields, HTTPMethod, HTTPSimpleFields, @@ -31,6 +32,7 @@ export const DEFAULT_NAMESPACE_STRING = 'default'; export const DEFAULT_COMMON_FIELDS: CommonFields = { [ConfigKey.MONITOR_TYPE]: DataStream.HTTP, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP, [ConfigKey.ENABLED]: true, [ConfigKey.SCHEDULE]: { number: '3', @@ -85,6 +87,7 @@ export const DEFAULT_BROWSER_SIMPLE_FIELDS: BrowserSimpleFields = { [ConfigKey.SOURCE_ZIP_PASSWORD]: '', [ConfigKey.SOURCE_ZIP_FOLDER]: '', [ConfigKey.SOURCE_ZIP_PROXY_URL]: '', + [ConfigKey.TEXT_ASSERTION]: '', [ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: undefined, [ConfigKey.ZIP_URL_TLS_CERTIFICATE]: undefined, [ConfigKey.ZIP_URL_TLS_KEY]: undefined, @@ -92,6 +95,7 @@ export const DEFAULT_BROWSER_SIMPLE_FIELDS: BrowserSimpleFields = { [ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]: undefined, [ConfigKey.ZIP_URL_TLS_VERSION]: undefined, [ConfigKey.URLS]: '', + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP, }; export const DEFAULT_HTTP_SIMPLE_FIELDS: HTTPSimpleFields = { @@ -102,6 +106,7 @@ export const DEFAULT_HTTP_SIMPLE_FIELDS: HTTPSimpleFields = { [ConfigKey.URLS]: '', [ConfigKey.MAX_REDIRECTS]: '0', [ConfigKey.MONITOR_TYPE]: DataStream.HTTP, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP, }; export const DEFAULT_HTTP_ADVANCED_FIELDS: HTTPAdvancedFields = { @@ -127,6 +132,7 @@ export const DEFAULT_ICMP_SIMPLE_FIELDS: ICMPSimpleFields = { [ConfigKey.HOSTS]: '', [ConfigKey.MONITOR_TYPE]: DataStream.ICMP, [ConfigKey.WAIT]: '1', + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP, }; export const DEFAULT_TCP_SIMPLE_FIELDS: TCPSimpleFields = { @@ -136,6 +142,7 @@ export const DEFAULT_TCP_SIMPLE_FIELDS: TCPSimpleFields = { }, [ConfigKey.HOSTS]: '', [ConfigKey.MONITOR_TYPE]: DataStream.TCP, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP, }; export const DEFAULT_TCP_ADVANCED_FIELDS: TCPAdvancedFields = { @@ -165,7 +172,9 @@ export const DEFAULT_FIELDS: MonitorDefaults = { ...DEFAULT_TCP_ADVANCED_FIELDS, ...DEFAULT_TLS_FIELDS, }, - [DataStream.ICMP]: DEFAULT_ICMP_SIMPLE_FIELDS, + [DataStream.ICMP]: { + ...DEFAULT_ICMP_SIMPLE_FIELDS, + }, [DataStream.BROWSER]: { ...DEFAULT_BROWSER_SIMPLE_FIELDS, ...DEFAULT_BROWSER_ADVANCED_FIELDS, diff --git a/x-pack/plugins/synthetics/common/constants/monitor_management.ts b/x-pack/plugins/synthetics/common/constants/monitor_management.ts index 598e985d3c3b066..d613d234fc3c179 100644 --- a/x-pack/plugins/synthetics/common/constants/monitor_management.ts +++ b/x-pack/plugins/synthetics/common/constants/monitor_management.ts @@ -11,6 +11,7 @@ export enum ConfigKey { CUSTOM_HEARTBEAT_ID = 'custom_heartbeat_id', CONFIG_ID = 'config_id', ENABLED = 'enabled', + FORM_MONITOR_TYPE = 'form_monitor_type', HOSTS = 'hosts', IGNORE_HTTPS_ERRORS = 'ignore_https_errors', MONITOR_SOURCE_TYPE = 'origin', @@ -53,6 +54,7 @@ export enum ConfigKey { SOURCE_ZIP_PROXY_URL = 'source.zip_url.proxy_url', PROJECT_ID = 'project_id', SYNTHETICS_ARGS = 'synthetics_args', + TEXT_ASSERTION = 'playwright_text_assertion', TLS_CERTIFICATE_AUTHORITIES = 'ssl.certificate_authorities', TLS_CERTIFICATE = 'ssl.certificate', TLS_KEY = 'ssl.key', diff --git a/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts b/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts index e5b9edf0edf7fb0..1524e646bb50897 100644 --- a/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts @@ -77,5 +77,6 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKey.PLAYWRIGHT_OPTIONS]: null, [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, [ConfigKey.ORIGINAL_SPACE]: null, + [ConfigKey.TEXT_ASSERTION]: null, ...commonFormatters, }; diff --git a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts index 55fe80e3bba8d38..5a5fbb864ecb373 100644 --- a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts @@ -27,6 +27,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.NAMESPACE]: null, [ConfigKey.REVISION]: null, [ConfigKey.MONITOR_SOURCE_TYPE]: null, + [ConfigKey.FORM_MONITOR_TYPE]: null, }; export const arrayToJsonFormatter = (value: string[] = []) => diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_configs.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_configs.ts index 4d1e22ffaea3b09..934c53479530162 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_configs.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_configs.ts @@ -126,3 +126,13 @@ export enum SourceType { } export const SourceTypeCodec = tEnum('SourceType', SourceType); + +export enum FormMonitorType { + SINGLE = 'single', + MULTISTEP = 'multistep', + HTTP = 'http', + TCP = 'tcp', + ICMP = 'icmp', +} + +export const FormMonitorTypeCodec = tEnum('FormMonitorType', FormMonitorType); diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 2405fc1f9667565..ebb5376792d7047 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -16,6 +16,7 @@ import { import { DataStream, DataStreamCodec, + FormMonitorTypeCodec, ModeCodec, ResponseBodyIndexPolicyCodec, ScheduleUnitCodec, @@ -79,6 +80,7 @@ export const CommonFieldsCodec = t.intersection([ [ConfigKey.LOCATIONS]: MonitorServiceLocationsCodec, }), t.partial({ + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorTypeCodec, [ConfigKey.TIMEOUT]: t.union([t.string, t.null]), [ConfigKey.REVISION]: t.number, [ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec, @@ -220,6 +222,7 @@ export const EncryptedBrowserSimpleFieldsCodec = t.intersection([ [ConfigKey.PROJECT_ID]: t.string, [ConfigKey.ORIGINAL_SPACE]: t.string, [ConfigKey.CUSTOM_HEARTBEAT_ID]: t.string, + [ConfigKey.TEXT_ASSERTION]: t.string, }), ]), ZipUrlTLSFieldsCodec, diff --git a/x-pack/plugins/synthetics/e2e/journeys/index.ts b/x-pack/plugins/synthetics/e2e/journeys/index.ts index a73ec34ef1d2900..a33a5185fcac2a1 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/index.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export * from './alerts'; + export * from './synthetics'; +export * from './alerts'; export * from './data_view_permissions'; export * from './uptime.journey'; export * from './step_duration.journey'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts new file mode 100644 index 000000000000000..881e562d56dd8e0 --- /dev/null +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import uuid from 'uuid'; +import { journey, step, expect, Page } from '@elastic/synthetics'; +import { FormMonitorType } from '../../../common/runtime_types/monitor_management'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; + +const customLocation = process.env.SYNTHETICS_TEST_LOCATION; + +const basicMonitorDetails = { + location: customLocation || 'US Central', + schedule: '3', +}; +const httpName = `http monitor ${uuid.v4()}`; +const icmpName = `icmp monitor ${uuid.v4()}`; +const tcpName = `tcp monitor ${uuid.v4()}`; +const browserName = `browser monitor ${uuid.v4()}`; +const browserRecorderName = `browser monitor recorder ${uuid.v4()}`; +const apmServiceName = 'apmServiceName'; + +const configuration = { + [FormMonitorType.HTTP]: { + monitorType: FormMonitorType.HTTP, + monitorConfig: { + ...basicMonitorDetails, + name: httpName, + url: 'https://elastic.co', + locations: [basicMonitorDetails.location], + apmServiceName, + }, + monitorListDetails: { + ...basicMonitorDetails, + name: httpName, + }, + monitorEditDetails: [ + ['[data-test-subj=syntheticsMonitorConfigSchedule]', '3'], + ['[data-test-subj=syntheticsMonitorConfigName]', httpName], + ['[data-test-subj=syntheticsMonitorConfigURL]', 'https://elastic.co'], + ['[data-test-subj=syntheticsMonitorConfigAPMServiceName]', apmServiceName], + ], + }, + [FormMonitorType.TCP]: { + monitorType: FormMonitorType.TCP, + monitorConfig: { + ...basicMonitorDetails, + name: tcpName, + host: 'smtp.gmail.com:587', + locations: [basicMonitorDetails.location], + apmServiceName, + }, + monitorListDetails: { + ...basicMonitorDetails, + name: tcpName, + }, + monitorEditDetails: [ + ['[data-test-subj=syntheticsMonitorConfigSchedule]', '3'], + ['[data-test-subj=syntheticsMonitorConfigName]', tcpName], + ['[data-test-subj=syntheticsMonitorConfigHost]', 'smtp.gmail.com:587'], + ['[data-test-subj=syntheticsMonitorConfigAPMServiceName]', apmServiceName], + ], + }, + [FormMonitorType.ICMP]: { + monitorType: FormMonitorType.ICMP, + monitorConfig: { + ...basicMonitorDetails, + name: icmpName, + host: '1.1.1.1', + locations: [basicMonitorDetails.location], + apmServiceName, + }, + monitorListDetails: { + ...basicMonitorDetails, + name: icmpName, + }, + monitorEditDetails: [ + ['[data-test-subj=syntheticsMonitorConfigSchedule]', '3'], + ['[data-test-subj=syntheticsMonitorConfigName]', icmpName], + ['[data-test-subj=syntheticsMonitorConfigHost]', '1.1.1.1'], + ['[data-test-subj=syntheticsMonitorConfigAPMServiceName]', apmServiceName], + // name: httpName, + ], + }, + [FormMonitorType.MULTISTEP]: { + monitorType: FormMonitorType.MULTISTEP, + monitorConfig: { + ...basicMonitorDetails, + schedule: '10', + name: browserName, + inlineScript: 'step("test step", () => {})', + locations: [basicMonitorDetails.location], + apmServiceName, + }, + monitorListDetails: { + ...basicMonitorDetails, + schedule: '10', + name: browserName, + }, + monitorEditDetails: [ + ['[data-test-subj=syntheticsMonitorConfigSchedule]', '10'], + ['[data-test-subj=syntheticsMonitorConfigName]', browserName], + ['[data-test-subj=codeEditorContainer] textarea', 'step("test step", () => {})'], + ['[data-test-subj=syntheticsMonitorConfigAPMServiceName]', apmServiceName], + ], + }, + [`${FormMonitorType.MULTISTEP}__recorder`]: { + monitorType: FormMonitorType.MULTISTEP, + monitorConfig: { + ...basicMonitorDetails, + schedule: '10', + name: browserRecorderName, + recorderScript: 'step("test step", () => {})', + locations: [basicMonitorDetails.location], + apmServiceName: 'Sample APM Service', + }, + monitorListDetails: { + ...basicMonitorDetails, + schedule: '10', + name: browserRecorderName, + }, + monitorEditDetails: [ + ['[data-test-subj=syntheticsMonitorConfigSchedule]', '10'], + ['[data-test-subj=syntheticsMonitorConfigName]', browserRecorderName], + ['[data-test-subj=codeEditorContainer] textarea', 'step("test step", () => {})'], + // name: httpName, + ], + }, +}; + +const createMonitorJourney = ({ + monitorName, + monitorType, + monitorConfig, + monitorListDetails, + monitorEditDetails, +}: { + monitorName: string; + monitorType: FormMonitorType; + monitorConfig: Record; + monitorListDetails: Record; + monitorEditDetails: Array<[string, string]>; +}) => { + journey( + `Synthetics - add monitor - ${monitorName}`, + async ({ page, params }: { page: Page; params: any }) => { + const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + step('Go to monitor management', async () => { + await syntheticsApp.navigateToMonitorManagement(); + }); + + step('login to Kibana', async () => { + await syntheticsApp.loginToKibana(); + const invalid = await page.locator( + `text=Username or password is incorrect. Please try again.` + ); + expect(await invalid.isVisible()).toBeFalsy(); + }); + + step('Ensure all montiors are deleted', async () => { + await syntheticsApp.navigateToMonitorManagement(); + await syntheticsApp.waitForLoadingToFinish(); + const isSuccessful = await syntheticsApp.deleteMonitors(); + expect(isSuccessful).toBeTruthy(); + }); + + step(`create ${monitorName}`, async () => { + await syntheticsApp.navigateToAddMonitor(); + await syntheticsApp.createMonitor({ monitorConfig, monitorType }); + const isSuccessful = await syntheticsApp.confirmAndSave(); + expect(isSuccessful).toBeTruthy(); + }); + + step(`view ${monitorName} details in Monitor Management UI`, async () => { + await syntheticsApp.navigateToMonitorManagement(); + const hasFailure = await syntheticsApp.findMonitorConfiguration(monitorListDetails); + expect(hasFailure).toBeFalsy(); + }); + + step(`edit ${monitorName}`, async () => { + await syntheticsApp.navigateToEditMonitor(); + await syntheticsApp.findByText(monitorListDetails.location); + const hasFailure = await syntheticsApp.findEditMonitorConfiguration( + monitorEditDetails, + monitorType + ); + expect(hasFailure).toBeFalsy(); + }); + + step('delete monitor', async () => { + await syntheticsApp.navigateToMonitorManagement(); + await syntheticsApp.findByText('Monitor name'); + const isSuccessful = await syntheticsApp.deleteMonitors(); + expect(isSuccessful).toBeTruthy(); + }); + } + ); +}; + +Object.values(configuration).forEach((config) => { + createMonitorJourney({ + monitorType: config.monitorType, + monitorName: `${config.monitorConfig.name} monitor`, + monitorConfig: config.monitorConfig, + monitorListDetails: config.monitorListDetails, + monitorEditDetails: config.monitorEditDetails as Array<[string, string]>, + }); +}); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts index 1783ced950ca171..3c8869fab85a8e4 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts @@ -6,3 +6,4 @@ */ export * from './getting_started.journey'; +export * from './add_monitor.journey'; diff --git a/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx index ba5d245a45588f3..0046326bad612c8 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Page } from '@elastic/synthetics'; +import { expect, Page } from '@elastic/synthetics'; import { getQuerystring } from '@kbn/observability-plugin/e2e/utils'; import { DataStream } from '../../common/runtime_types/monitor_management'; import { loginPageProvider } from './login'; @@ -107,6 +107,8 @@ export function monitorManagementPageProvider({ }, async clickAddMonitor() { + const isEnabled = await this.checkIsEnabled(); + expect(isEnabled).toBe(true); await page.click('text=Add monitor'); }, diff --git a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx b/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx index 13ef33a7261630f..fc365abd823b92c 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Page } from '@elastic/synthetics'; +import { expect, Page } from '@elastic/synthetics'; +import { FormMonitorType } from '../../common/runtime_types/monitor_management'; import { loginPageProvider } from './login'; import { utilsPageProvider } from './utils'; @@ -19,8 +20,7 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib const isRemote = Boolean(process.env.SYNTHETICS_REMOTE_ENABLED); const basePath = isRemote ? remoteKibanaUrl : kibanaUrl; const monitorManagement = `${basePath}/app/synthetics/monitors`; - const addMonitor = `${basePath}/app/uptime/add-monitor`; - + const addMonitor = `${basePath}/app/synthetics/add-monitor`; return { ...loginPageProvider({ page, @@ -49,25 +49,43 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib }, async navigateToAddMonitor() { - await page.goto(addMonitor, { - waitUntil: 'networkidle', - }); + await page.goto(addMonitor); }, async ensureIsOnMonitorConfigPage() { await page.isVisible('[data-test-subj=monitorSettingsSection]'); }, - async confirmAndSave(isEditPage?: boolean) { + async confirmAndSave() { await this.ensureIsOnMonitorConfigPage(); - if (isEditPage) { - await page.click('text=Update monitor'); - } else { - await page.click('text=Create monitor'); - } + await this.clickByTestSubj('syntheticsMonitorConfigSubmitButton'); return await this.findByText('Monitor added successfully.'); }, + async deleteMonitors() { + let isSuccessful: boolean = false; + while (true) { + if ((await page.$(this.byTestId('syntheticsMonitorListActions'))) === null) { + isSuccessful = true; + break; + } + await page.click(this.byTestId('syntheticsMonitorListActions'), { delay: 800 }); + await page.click('text=delete', { delay: 800 }); + await page.waitForSelector('[data-test-subj="confirmModalTitleText"]'); + await this.clickByTestSubj('confirmModalConfirmButton'); + isSuccessful = Boolean(await this.findByTestSubj('uptimeDeleteMonitorSuccess')); + await this.navigateToMonitorManagement(); + await page.waitForTimeout(5 * 1000); + } + return isSuccessful; + }, + + async navigateToEditMonitor() { + await this.clickByTestSubj('syntheticsMonitorListActions'); + await page.click('text=Edit'); + await this.findByText('Edit monitor'); + }, + async selectLocations({ locations }: { locations: string[] }) { for (let i = 0; i < locations.length; i++) { await page.click( @@ -77,6 +95,13 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib } }, + async selectLocationsAddEdit({ locations }: { locations: string[] }) { + for (let i = 0; i < locations.length; i++) { + await page.click(this.byTestId('syntheticsMonitorConfigLocations')); + await page.click(`text=${locations[i]}`); + } + }, + async fillFirstMonitorDetails({ url, locations }: { url: string; locations: string[] }) { await this.fillByTestSubj('urls-input', url); await page.click(this.byTestId('comboBoxInput')); @@ -84,6 +109,162 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib await page.click(this.byTestId('urls-input')); }, + async selectMonitorType(monitorType: string) { + await this.clickByTestSubj(monitorType); + }, + + async findMonitorConfiguration(monitorConfig: Record) { + const values = Object.values(monitorConfig); + + for (let i = 0; i < values.length; i++) { + await this.findByText(values[i]); + } + }, + + async findEditMonitorConfiguration( + monitorConfig: Array<[string, string]>, + monitorType: FormMonitorType + ) { + await page.click('text="Advanced options"'); + + for (let i = 0; i < monitorConfig.length; i++) { + const [selector, expected] = monitorConfig[i]; + const actual = await page.inputValue(selector); + expect(actual).toEqual(expected); + } + }, + + async fillCodeEditor(value: string) { + await page.fill('[data-test-subj=codeEditorContainer] textarea', value); + }, + + async createBasicHTTPMonitorDetails({ + name, + url, + apmServiceName, + locations, + }: { + name: string; + url: string; + apmServiceName: string; + locations: string[]; + }) { + await this.selectMonitorType('syntheticsMonitorTypeHTTP'); + await this.createBasicMonitorDetails({ name, apmServiceName, locations }); + await this.fillByTestSubj('syntheticsMonitorConfigURL', url); + }, + + async createBasicTCPMonitorDetails({ + name, + host, + apmServiceName, + locations, + }: { + name: string; + host: string; + apmServiceName: string; + locations: string[]; + }) { + await this.selectMonitorType('syntheticsMonitorTypeTCP'); + await this.createBasicMonitorDetails({ name, apmServiceName, locations }); + await this.fillByTestSubj('syntheticsMonitorConfigHost', host); + }, + + async createBasicICMPMonitorDetails({ + name, + host, + apmServiceName, + locations, + }: { + name: string; + host: string; + apmServiceName: string; + locations: string[]; + }) { + await this.selectMonitorType('syntheticsMonitorTypeICMP'); + await this.createBasicMonitorDetails({ name, apmServiceName, locations }); + await this.fillByTestSubj('syntheticsMonitorConfigHost', host); + }, + + async createBasicBrowserMonitorDetails({ + name, + inlineScript, + recorderScript, + params, + username, + password, + apmServiceName, + locations, + }: { + name: string; + inlineScript?: string; + recorderScript?: string; + params?: string; + username?: string; + password?: string; + apmServiceName: string; + locations: string[]; + }) { + await this.createBasicMonitorDetails({ name, apmServiceName, locations }); + if (inlineScript) { + await this.clickByTestSubj('syntheticsSourceTab__inline'); + await this.fillCodeEditor(inlineScript); + return; + } + if (recorderScript) { + // Upload buffer from memory + await page.setInputFiles('input[data-test-subj=syntheticsFleetScriptRecorderUploader]', { + name: 'file.js', + mimeType: 'text/javascript', + buffer: Buffer.from(recorderScript), + }); + } + }, + + async createBasicMonitorDetails({ + name, + apmServiceName, + locations, + }: { + name: string; + apmServiceName: string; + locations: string[]; + }) { + await page.click('text="Advanced options"'); + await this.fillByTestSubj('syntheticsMonitorConfigName', name); + await this.fillByTestSubj('syntheticsMonitorConfigAPMServiceName', apmServiceName); + await this.selectLocationsAddEdit({ locations }); + }, + + async createMonitor({ + monitorConfig, + monitorType, + }: { + monitorConfig: Record; + monitorType: FormMonitorType; + }) { + switch (monitorType) { + case FormMonitorType.HTTP: + // @ts-ignore + await this.createBasicHTTPMonitorDetails(monitorConfig); + break; + case FormMonitorType.TCP: + // @ts-ignore + await this.createBasicTCPMonitorDetails(monitorConfig); + break; + case FormMonitorType.ICMP: + // @ts-ignore + await this.createBasicICMPMonitorDetails(monitorConfig); + break; + case FormMonitorType.MULTISTEP: + // @ts-ignore + await this.createBasicBrowserMonitorDetails(monitorConfig); + break; + default: + break; + } + }, + async enableMonitorManagement(shouldEnable: boolean = true) { const isEnabled = await this.checkIsEnabled(); if (isEnabled === shouldEnable) { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx index 704b5d350b9e040..a334d6ec1a514f8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx @@ -8,13 +8,17 @@ import React, { useEffect } from 'react'; import { EuiEmptyPrompt, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { useBreadcrumbs } from '../../hooks'; import { getServiceLocations } from '../../state'; +import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui'; import { SimpleMonitorForm } from './simple_monitor_form'; + export const GettingStartedPage = () => { const dispatch = useDispatch(); + const history = useHistory(); useEffect(() => { dispatch(getServiceLocations()); @@ -32,7 +36,13 @@ export const GettingStartedPage = () => { <> {OR_LABEL}{' '} - {SELECT_DIFFERENT_MONITOR} + + {SELECT_DIFFERENT_MONITOR} + {i18n.translate('xpack.synthetics.gettingStarted.createSingle.description', { defaultMessage: ' to get started with Elastic Synthetics Monitoring', })} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.tsx index 14ceff545e240bf..ce849f586a93c04 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.tsx @@ -77,7 +77,13 @@ export const SimpleMonitorForm = () => { - + {CREATE_MONITOR_LABEL} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/advanced/index.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/advanced/index.tsx new file mode 100644 index 000000000000000..d5accba8d73999e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/advanced/index.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescribedFormGroup, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { useFormContext, FieldError } from 'react-hook-form'; +import { FORM_CONFIG } from '../form/form_config'; +import { Field } from '../form/field'; +import { ConfigKey, FormMonitorType } from '../types'; + +export const AdvancedConfig = () => { + const { + watch, + formState: { errors }, + } = useFormContext(); + const [type]: [FormMonitorType] = watch([ConfigKey.FORM_MONITOR_TYPE]); + + return FORM_CONFIG[type]?.advanced ? ( + + + + {FORM_CONFIG[type].advanced?.map((configGroup) => { + return ( + {configGroup.title}} + fullWidth + key={configGroup.title} + > + {configGroup.components.map((field) => { + return ( + + ); + })} + + ); + })} + + + ) : null; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/constants.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/constants.ts new file mode 100644 index 000000000000000..a5f56f375aba17b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from '../../../../../common/constants/monitor_management'; +export * from '../../../../../common/constants/monitor_defaults'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx new file mode 100644 index 000000000000000..6d7fa2cbc441f5c --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import styled from 'styled-components'; + +import { EuiPanel } from '@elastic/eui'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { CodeEditor as MonacoCodeEditor } from '@kbn/kibana-react-plugin/public'; + +import { MonacoEditorLangId } from '../types'; + +const CodeEditorContainer = styled(EuiPanel)` + padding: 0; +`; + +interface Props { + ariaLabel: string; + id: string; + languageId: MonacoEditorLangId; + onChange: (value: string) => void; + value: string; + placeholder?: string; +} + +export const CodeEditor = ({ ariaLabel, id, languageId, onChange, value, placeholder }: Props) => { + return ( + + + + + + ); +}; + +const MonacoCodeContainer = euiStyled.div` + & > .kibanaCodeEditor { + z-index: 0; + } +`; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/combo_box.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/combo_box.test.tsx new file mode 100644 index 000000000000000..3788b3ca7f2c3bd --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/combo_box.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { render } from '../../../utils/testing/rtl_helpers'; +import { ComboBox } from './combo_box'; + +describe('', () => { + const onChange = jest.fn(); + const selectedOptions: string[] = []; + + it('renders ComboBox', () => { + const { getByTestId } = render( + + ); + + expect(getByTestId('syntheticsFleetComboBox')).toBeInTheDocument(); + }); + + it('calls onBlur', () => { + const onBlur = jest.fn(); + const { getByTestId } = render( + + ); + + const combobox = getByTestId('syntheticsFleetComboBox'); + fireEvent.focus(combobox); + fireEvent.blur(combobox); + + expect(onBlur).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/combo_box.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/combo_box.tsx new file mode 100644 index 000000000000000..16a31e8e5d623d3 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/combo_box.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +export interface Props { + onChange: (value: string[]) => void; + onBlur?: () => void; + selectedOptions: string[]; +} + +export const ComboBox = ({ onChange, onBlur, selectedOptions, ...props }: Props) => { + const [formattedSelectedOptions, setSelectedOptions] = useState< + Array> + >(selectedOptions.map((option) => ({ label: option, key: option }))); + const [isInvalid, setInvalid] = useState(false); + + const onOptionsChange = useCallback( + (options: Array>) => { + setSelectedOptions(options); + const formattedTags = options.map((option) => option.label); + onChange(formattedTags); + setInvalid(false); + }, + [onChange, setSelectedOptions, setInvalid] + ); + + const onCreateOption = useCallback( + (tag: string) => { + const formattedTag = tag.trim(); + const newOption = { + label: formattedTag, + }; + + onChange([...selectedOptions, formattedTag]); + + // Select the option. + setSelectedOptions([...formattedSelectedOptions, newOption]); + }, + [onChange, formattedSelectedOptions, selectedOptions, setSelectedOptions] + ); + + const onSearchChange = useCallback( + (searchValue: string) => { + if (!searchValue) { + setInvalid(false); + + return; + } + + setInvalid(!isValid(searchValue)); + }, + [setInvalid] + ); + + return ( + + data-test-subj="syntheticsFleetComboBox" + noSuggestions + selectedOptions={formattedSelectedOptions} + onCreateOption={onCreateOption} + onChange={onOptionsChange} + onBlur={() => onBlur?.()} + onSearchChange={onSearchChange} + isInvalid={isInvalid} + {...props} + /> + ); +}; + +const isValid = (value: string) => { + // Ensure that the tag is more than whitespace + return value.match(/\S+/) !== null; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/header_field.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/header_field.test.tsx new file mode 100644 index 000000000000000..6f920bf10d84a93 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/header_field.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../../../utils/testing/rtl_helpers'; +import { HeaderField, contentTypes } from './header_field'; +import { Mode } from '../types'; + +describe('', () => { + const onChange = jest.fn(); + const onBlur = jest.fn(); + const defaultValue = {}; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('renders HeaderField', () => { + const { getByText, getByTestId } = render( + + ); + + expect(getByText('Key')).toBeInTheDocument(); + expect(getByText('Value')).toBeInTheDocument(); + const key = getByTestId('keyValuePairsKey0') as HTMLInputElement; + const value = getByTestId('keyValuePairsValue0') as HTMLInputElement; + expect(key.value).toEqual('sample'); + expect(value.value).toEqual('header'); + }); + + it('calls onBlur', () => { + const { getByTestId } = render( + + ); + + const key = getByTestId('keyValuePairsKey0') as HTMLInputElement; + const value = getByTestId('keyValuePairsValue0') as HTMLInputElement; + + fireEvent.blur(key); + fireEvent.blur(value); + + expect(onBlur).toHaveBeenCalledTimes(2); + }); + + it('formats headers and handles onChange', async () => { + const { getByTestId, getByText } = render( + + ); + const addHeader = getByText('Add header'); + fireEvent.click(addHeader); + const key = getByTestId('keyValuePairsKey0') as HTMLInputElement; + const value = getByTestId('keyValuePairsValue0') as HTMLInputElement; + const newKey = 'sampleKey'; + const newValue = 'sampleValue'; + fireEvent.change(key, { target: { value: newKey } }); + fireEvent.change(value, { target: { value: newValue } }); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + [newKey]: newValue, + }); + }); + }); + + it('handles deleting headers', async () => { + const { getByTestId, getByText, getByLabelText } = render( + + ); + const addHeader = getByText('Add header'); + + fireEvent.click(addHeader); + + const key = getByTestId('keyValuePairsKey0') as HTMLInputElement; + const value = getByTestId('keyValuePairsValue0') as HTMLInputElement; + const newKey = 'sampleKey'; + const newValue = 'sampleValue'; + fireEvent.change(key, { target: { value: newKey } }); + fireEvent.change(value, { target: { value: newValue } }); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + [newKey]: newValue, + }); + }); + + const deleteBtn = getByLabelText('Delete item number 2, sampleKey:sampleValue'); + + // uncheck + fireEvent.click(deleteBtn); + }); + + it('handles content mode', async () => { + const contentMode: Mode = Mode.PLAINTEXT; + render( + + ); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + 'Content-Type': contentTypes[Mode.PLAINTEXT], + }); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/header_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/header_field.tsx new file mode 100644 index 000000000000000..d2a3c4968191aae --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/header_field.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { ContentType, Mode } from '../types'; + +import { KeyValuePairsField, Pair } from './key_value_field'; + +interface Props { + contentMode?: Mode; + defaultValue: Record; + onChange: (value: Record) => void; + onBlur?: () => void; + 'data-test-subj'?: string; +} + +export const HeaderField = ({ + contentMode, + defaultValue, + onChange, + onBlur, + 'data-test-subj': dataTestSubj, +}: Props) => { + const defaultValueKeys = Object.keys(defaultValue).filter((key) => key !== 'Content-Type'); // Content-Type is a secret header we hide from the user + const formattedDefaultValues: Pair[] = [ + ...defaultValueKeys.map((key) => { + return [key || '', defaultValue[key] || '']; // key, value + }), + ]; + const [headers, setHeaders] = useState(formattedDefaultValues); + + useEffect(() => { + const formattedHeaders = headers.reduce((acc: Record, header) => { + const [key, value] = header; + if (key) { + return { + ...acc, + [key]: value, + }; + } + return acc; + }, {}); + + if (contentMode) { + onChange({ 'Content-Type': contentTypes[contentMode], ...formattedHeaders }); + } else { + onChange(formattedHeaders); + } + }, [contentMode, headers, onChange]); + + return ( + + } + defaultPairs={headers} + onChange={setHeaders} + onBlur={() => onBlur?.()} + data-test-subj={dataTestSubj} + /> + ); +}; + +export const contentTypes: Record = { + [Mode.JSON]: ContentType.JSON, + [Mode.PLAINTEXT]: ContentType.TEXT, + [Mode.XML]: ContentType.XML, + [Mode.FORM]: ContentType.FORM, +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/index_response_body_field.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/index_response_body_field.test.tsx new file mode 100644 index 000000000000000..da2e1ef1f93e75b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/index_response_body_field.test.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../../../utils/testing/rtl_helpers'; +import { ResponseBodyIndexField } from './index_response_body_field'; +import { ResponseBodyIndexPolicy } from '../types'; + +describe('', () => { + const defaultDefaultValue = ResponseBodyIndexPolicy.ON_ERROR; + const onChange = jest.fn(); + const onBlur = jest.fn(); + const WrappedComponent = ({ defaultValue = defaultDefaultValue }) => { + return ( + + ); + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('renders ResponseBodyIndexField', () => { + const { getByText, getByTestId } = render(); + const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement; + expect(select.value).toEqual(defaultDefaultValue); + expect(getByText('On error')).toBeInTheDocument(); + expect(getByText('Index response body')).toBeInTheDocument(); + }); + + it('handles select change', async () => { + const { getByText, getByTestId } = render(); + const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement; + const newPolicy = ResponseBodyIndexPolicy.ALWAYS; + expect(select.value).toEqual(defaultDefaultValue); + + fireEvent.change(select, { target: { value: newPolicy } }); + + await waitFor(() => { + expect(select.value).toBe(newPolicy); + expect(getByText('Always')).toBeInTheDocument(); + expect(onChange).toBeCalledWith(newPolicy); + }); + }); + + it('calls onBlur', async () => { + const { getByTestId } = render(); + const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement; + const newPolicy = ResponseBodyIndexPolicy.ALWAYS; + + fireEvent.change(select, { target: { value: newPolicy } }); + fireEvent.blur(select); + + expect(onBlur).toHaveBeenCalledTimes(1); + }); + + it('handles checkbox change', async () => { + const { getByTestId, getByLabelText } = render(); + const checkbox = getByLabelText('Index response body') as HTMLInputElement; + const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement; + const newPolicy = ResponseBodyIndexPolicy.NEVER; + expect(checkbox.checked).toBe(true); + + fireEvent.click(checkbox); + + await waitFor(() => { + expect(checkbox.checked).toBe(false); + expect(select).not.toBeInTheDocument(); + expect(onChange).toBeCalledWith(newPolicy); + }); + + fireEvent.click(checkbox); + + await waitFor(() => { + expect(checkbox.checked).toBe(true); + expect(select).not.toBeInTheDocument(); + expect(onChange).toBeCalledWith(defaultDefaultValue); + }); + }); + + it('handles ResponseBodyIndexPolicy.NEVER as a default value', async () => { + const { queryByTestId, getByTestId, getByLabelText } = render( + + ); + const checkbox = getByLabelText('Index response body') as HTMLInputElement; + expect(checkbox.checked).toBe(false); + expect( + queryByTestId('indexResponseBodyFieldSelect') as HTMLInputElement + ).not.toBeInTheDocument(); + + fireEvent.click(checkbox); + const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement; + + await waitFor(() => { + expect(checkbox.checked).toBe(true); + expect(select).toBeInTheDocument(); + expect(select.value).toEqual(ResponseBodyIndexPolicy.ON_ERROR); + // switches back to on error policy when checkbox is checked + expect(onChange).toBeCalledWith(ResponseBodyIndexPolicy.ON_ERROR); + }); + + const newPolicy = ResponseBodyIndexPolicy.ALWAYS; + fireEvent.change(select, { target: { value: newPolicy } }); + + await waitFor(() => { + expect(select.value).toEqual(newPolicy); + expect(onChange).toBeCalledWith(newPolicy); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/index_response_body_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/index_response_body_field.tsx new file mode 100644 index 000000000000000..d7de7adcf1aa976 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/index_response_body_field.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { ResponseBodyIndexPolicy } from '../types'; + +interface Props { + defaultValue: ResponseBodyIndexPolicy; + onChange: (responseBodyIndexPolicy: ResponseBodyIndexPolicy) => void; + onBlur?: () => void; +} + +export const ResponseBodyIndexField = ({ defaultValue, onChange, onBlur }: Props) => { + const [policy, setPolicy] = useState( + defaultValue !== ResponseBodyIndexPolicy.NEVER ? defaultValue : ResponseBodyIndexPolicy.ON_ERROR + ); + const [checked, setChecked] = useState(defaultValue !== ResponseBodyIndexPolicy.NEVER); + + useEffect(() => { + if (checked) { + setPolicy(policy); + onChange(policy); + } else { + onChange(ResponseBodyIndexPolicy.NEVER); + } + }, [checked, policy, setPolicy, onChange]); + + useEffect(() => { + onChange(policy); + }, [onChange, policy]); + + return ( + + + + } + onChange={(event) => { + const checkedEvent = event.target.checked; + setChecked(checkedEvent); + }} + onBlur={() => onBlur?.()} + /> + + {checked && ( + + { + setPolicy(event.target.value as ResponseBodyIndexPolicy); + }} + onBlur={() => onBlur?.()} + /> + + )} + + ); +}; + +const responseBodyIndexPolicyOptions = [ + { + value: ResponseBodyIndexPolicy.ALWAYS, + text: i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.responseBodyIndex.always', + { + defaultMessage: 'Always', + } + ), + }, + { + value: ResponseBodyIndexPolicy.ON_ERROR, + text: i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.responseBodyIndex.onError', + { + defaultMessage: 'On error', + } + ), + }, +]; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/key_value_field.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/key_value_field.test.tsx new file mode 100644 index 000000000000000..670c414e37bbb42 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/key_value_field.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../../../utils/testing/rtl_helpers'; +import { KeyValuePairsField, Pair } from './key_value_field'; + +describe('', () => { + const onChange = jest.fn(); + const onBlur = jest.fn(); + const defaultDefaultValue = [['', '']] as Pair[]; + const WrappedComponent = ({ + defaultValue = defaultDefaultValue, + addPairControlLabel = 'Add pair', + }) => { + return ( + + ); + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('renders KeyValuePairsField', () => { + const { getByText } = render(); + expect(getByText('Key')).toBeInTheDocument(); + expect(getByText('Value')).toBeInTheDocument(); + + expect(getByText('Add pair')).toBeInTheDocument(); + }); + + it('calls onBlur', () => { + const { getByText, getByTestId } = render(); + const addPair = getByText('Add pair'); + fireEvent.click(addPair); + + const keyInput = getByTestId('keyValuePairsKey0') as HTMLInputElement; + const valueInput = getByTestId('keyValuePairsValue0') as HTMLInputElement; + + userEvent.type(keyInput, 'some-key'); + userEvent.type(valueInput, 'some-value'); + fireEvent.blur(valueInput); + + expect(onBlur).toHaveBeenCalledTimes(2); + }); + + it('handles adding and editing a new row', async () => { + const { getByTestId, queryByTestId, getByText } = render( + + ); + + expect(queryByTestId('keyValuePairsKey0')).not.toBeInTheDocument(); + expect(queryByTestId('keyValuePairsValue0')).not.toBeInTheDocument(); // check that only one row exists + + const addPair = getByText('Add pair'); + + fireEvent.click(addPair); + + const newRowKey = getByTestId('keyValuePairsKey0') as HTMLInputElement; + const newRowValue = getByTestId('keyValuePairsValue0') as HTMLInputElement; + + await waitFor(() => { + expect(newRowKey.value).toEqual(''); + expect(newRowValue.value).toEqual(''); + expect(onChange).toBeCalledWith([[newRowKey.value, newRowValue.value]]); + }); + + fireEvent.change(newRowKey, { target: { value: 'newKey' } }); + fireEvent.change(newRowValue, { target: { value: 'newValue' } }); + + await waitFor(() => { + expect(newRowKey.value).toEqual('newKey'); + expect(newRowValue.value).toEqual('newValue'); + expect(onChange).toBeCalledWith([[newRowKey.value, newRowValue.value]]); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/key_value_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/key_value_field.tsx new file mode 100644 index 000000000000000..153d537ebec0ea4 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/key_value_field.tsx @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment, useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiButton, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormControlLayoutDelimited, + EuiFormLabel, + EuiFormFieldset, + EuiSpacer, +} from '@elastic/eui'; + +const StyledFieldset = styled(EuiFormFieldset)` + &&& { + legend { + width: calc(100% - 52px); // right margin + flex item padding + margin-right: 40px; + } + .euiFlexGroup { + margin-left: 0; + } + .euiFlexItem { + margin-left: 0; + padding-left: 12px; + } + } +`; + +const StyledField = styled(EuiFieldText)` + text-align: left; +`; + +export type Pair = [ + string, // key + string // value +]; + +interface Props { + addPairControlLabel: string | React.ReactElement; + defaultPairs: Pair[]; + onChange: (pairs: Pair[]) => void; + onBlur?: () => void; + 'data-test-subj'?: string; +} + +export const KeyValuePairsField = ({ + addPairControlLabel, + defaultPairs, + onChange, + onBlur, + 'data-test-subj': dataTestSubj, +}: Props) => { + const [pairs, setPairs] = useState(defaultPairs); + + const handleOnChange = useCallback( + (event: React.ChangeEvent, index: number, isKey: boolean) => { + const targetValue = event.target.value; + + setPairs((prevPairs) => { + const newPairs = [...prevPairs]; + const [prevKey, prevValue] = prevPairs[index]; + newPairs[index] = isKey ? [targetValue, prevValue] : [prevKey, targetValue]; + return newPairs; + }); + }, + [setPairs] + ); + + const handleAddPair = useCallback(() => { + setPairs((prevPairs) => [['', ''], ...prevPairs]); + }, [setPairs]); + + const handleDeletePair = useCallback( + (index: number) => { + setPairs((prevPairs) => { + const newPairs = [...prevPairs]; + newPairs.splice(index, 1); + return [...newPairs]; + }); + }, + [setPairs] + ); + + useEffect(() => { + onChange(pairs); + }, [onChange, pairs]); + + return ( +
+ + + + + {addPairControlLabel} + + + + + + + { + + } + + + { + + } + + + ), + } + : undefined + } + > + {pairs.map((pair, index) => { + const [key, value] = pair; + return ( + + + + handleDeletePair(index)} + /> + + } + startControl={ + handleOnChange(event, index, true)} + onBlur={() => onBlur?.()} + /> + } + endControl={ + handleOnChange(event, index, false)} + onBlur={() => onBlur?.()} + /> + } + delimiter=":" + /> + + + ); + })} + +
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_type_radio_group.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_type_radio_group.tsx new file mode 100644 index 000000000000000..a803f01770d4e8e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_type_radio_group.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPanel, + EuiText, + EuiLink, + EuiSpacer, + EuiKeyPadMenu, + EuiKeyPadMenuItem, + EuiIcon, + EuiKeyPadMenuItemProps, +} from '@elastic/eui'; + +export type MonitorTypeRadioOption = EuiKeyPadMenuItemProps & { + icon: string; + description: string; + descriptionTitle: string; + link: string; + value: string; + label: React.ReactNode; + onChange: (id: string, value: string) => void; + name: string; + 'data-test-subj': string; +}; + +export const MonitorType = ({ + id, + value, + label, + icon, + onChange, + name, + isSelected, + 'data-test-subj': dataTestSubj, +}: MonitorTypeRadioOption) => { + return ( + + + + ); +}; + +export const MonitorTypeRadioGroup = ({ + options, + value, + name, + onChange, + ariaLegend, + ...props +}: EuiKeyPadMenuItemProps & { + options: MonitorTypeRadioOption[]; + onChange: React.ChangeEvent; + name: string; + value: string; + ariaLegend: string; +}) => { + const selectedOption = options.find((radio) => radio.value === value); + return ( + <> + + {options.map((radio) => { + return ( + + ); + })} + + + {selectedOption && ( + + +

{selectedOption.descriptionTitle}

+
+ + {`${selectedOption.description} `} + + {i18n.translate('xpack.synthetics.monitorConfig.monitorType.learnMoreLink', { + defaultMessage: 'Learn more', + })} + + + +
+ )} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/optional_label.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/optional_label.tsx new file mode 100644 index 000000000000000..a9db178a84bb3a3 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/optional_label.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiText } from '@elastic/eui'; + +export const OptionalLabel = () => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.test.tsx new file mode 100644 index 000000000000000..7f3442027da8720 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import 'jest-canvas-mock'; + +import React, { useState, useCallback } from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../../../utils/testing/rtl_helpers'; +import { RequestBodyField } from './request_body_field'; +import { Mode } from '../types'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( + { + props.onChange(e.jsonContent); + }} + /> + ), + }; +}); + +describe('', () => { + const defaultMode = Mode.PLAINTEXT; + const defaultValue = 'sample value'; + const WrappedComponent = () => { + const [config, setConfig] = useState({ + type: defaultMode, + value: defaultValue, + }); + + return ( + setConfig({ type: code.type as Mode, value: code.value }), + [setConfig] + )} + /> + ); + }; + + it('renders RequestBodyField', () => { + const { getByText, getByLabelText } = render(); + + expect(getByText('Form')).toBeInTheDocument(); + expect(getByText('Text')).toBeInTheDocument(); + expect(getByText('XML')).toBeInTheDocument(); + expect(getByText('JSON')).toBeInTheDocument(); + expect(getByLabelText('Text code editor')).toBeInTheDocument(); + }); + + it('handles changing code editor mode', async () => { + const { getByText, getByLabelText, queryByText, queryByLabelText } = render( + + ); + + // currently text code editor is displayed + expect(getByLabelText('Text code editor')).toBeInTheDocument(); + expect(queryByText('Key')).not.toBeInTheDocument(); + + const formButton = getByText('Form').closest('button'); + if (formButton) { + fireEvent.click(formButton); + } + await waitFor(() => { + expect(getByText('Add form field')).toBeInTheDocument(); + expect(queryByLabelText('Text code editor')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.tsx new file mode 100644 index 000000000000000..2d38c65e9a2fe0f --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.tsx @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { stringify, parse } from 'query-string'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { EuiTabbedContent } from '@elastic/eui'; +import { Mode, MonacoEditorLangId } from '../types'; +import { KeyValuePairsField, Pair } from './key_value_field'; +import { CodeEditor } from './code_editor'; + +interface Props { + onChange: (requestBody: { type: Mode; value: string }) => void; + onBlur?: () => void; + value: { + type: Mode; + value: string; + }; +} + +enum ResponseBodyType { + CODE = 'code', + FORM = 'form', +} + +// TO DO: Look into whether or not code editor reports errors, in order to prevent form submission on an error +export const RequestBodyField = ({ onChange, onBlur, value: { type, value } }: Props) => { + const [values, setValues] = useState>({ + [ResponseBodyType.FORM]: type === Mode.FORM ? value : '', + [ResponseBodyType.CODE]: type !== Mode.FORM ? value : '', + }); + useEffect(() => { + onChange({ + type, + value: type === Mode.FORM ? values[ResponseBodyType.FORM] : values[ResponseBodyType.CODE], + }); + }, [onChange, type, values]); + + const handleSetMode = useCallback( + (currentMode: Mode) => { + onChange({ + type: currentMode, + value: + currentMode === Mode.FORM ? values[ResponseBodyType.FORM] : values[ResponseBodyType.CODE], + }); + }, + [onChange, values] + ); + + const onChangeFormFields = useCallback( + (pairs: Pair[]) => { + const formattedPairs = pairs.reduce((acc: Record, header) => { + const [key, pairValue] = header; + if (key) { + return { + ...acc, + [key]: pairValue, + }; + } + return acc; + }, {}); + return setValues((prevValues) => ({ + ...prevValues, + [Mode.FORM]: stringify(formattedPairs), + })); + }, + [setValues] + ); + + const defaultFormPairs: Pair[] = useMemo(() => { + const pairs = parse(values[Mode.FORM]); + const keys = Object.keys(pairs); + const formattedPairs: Pair[] = keys.map((key: string) => { + // key, value, checked; + return [key, `${pairs[key]}`]; + }); + return formattedPairs; + }, [values]); + + const tabs = [ + { + id: Mode.PLAINTEXT, + name: modeLabels[Mode.PLAINTEXT], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.PLAINTEXT}`, + content: ( + { + setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code })); + onBlur?.(); + }} + value={values[ResponseBodyType.CODE]} + /> + ), + }, + { + id: Mode.JSON, + name: modeLabels[Mode.JSON], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.JSON}`, + content: ( + { + setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code })); + onBlur?.(); + }} + value={values[ResponseBodyType.CODE]} + /> + ), + }, + { + id: Mode.XML, + name: modeLabels[Mode.XML], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.XML}`, + content: ( + { + setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code })); + onBlur?.(); + }} + value={values[ResponseBodyType.CODE]} + /> + ), + }, + { + id: Mode.FORM, + name: modeLabels[Mode.FORM], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.FORM}`, + content: ( + + } + defaultPairs={defaultFormPairs} + onChange={onChangeFormFields} + onBlur={() => onBlur?.()} + /> + ), + }, + ]; + + return ( + tab.id === type)} + autoFocus="selected" + onTabClick={(tab) => { + handleSetMode(tab.id as Mode); + }} + /> + ); +}; + +const modeLabels = { + [Mode.FORM]: i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.requestBodyType.form', + { + defaultMessage: 'Form', + } + ), + [Mode.PLAINTEXT]: i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.requestBodyType.text', + { + defaultMessage: 'Text', + } + ), + [Mode.JSON]: i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.requestBodyType.JSON', + { + defaultMessage: 'JSON', + } + ), + [Mode.XML]: i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.requestBodyType.XML', + { + defaultMessage: 'XML', + } + ), +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/script_recorder_fields.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/script_recorder_fields.test.tsx new file mode 100644 index 000000000000000..f9fa393bab5dcf9 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/script_recorder_fields.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../../../utils/testing/rtl_helpers'; +import { ScriptRecorderFields } from './script_recorder_fields'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'), + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +const onChange = jest.fn(); + +describe('', () => { + let file: File; + const testScript = 'step(() => {})'; + const WrappedComponent = ({ + isEditable = true, + script = '', + fileName = '', + }: { + isEditable?: boolean; + script?: string; + fileName?: string; + }) => { + return ( + + ); + }; + + beforeEach(() => { + jest.clearAllMocks(); + file = new File([testScript], 'samplescript.js', { type: 'text/javascript' }); + }); + + it('renders ScriptRecorderFields', () => { + const { queryByText } = render(); + + expect(queryByText('Show script')).not.toBeInTheDocument(); + expect(queryByText('Remove script')).not.toBeInTheDocument(); + }); + + it('handles uploading files', async () => { + const { getByTestId } = render(); + + const uploader = getByTestId('syntheticsFleetScriptRecorderUploader'); + + fireEvent.change(uploader, { + target: { files: [file] }, + }); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ scriptText: testScript, fileName: 'samplescript.js' }); + }); + }); + + it('shows user errors for invalid file types', async () => { + const { getByTestId, getByText } = render(); + file = new File(['journey(() => {})'], 'samplescript.js', { type: 'text/javascript' }); + + let uploader = getByTestId('syntheticsFleetScriptRecorderUploader') as HTMLInputElement; + + fireEvent.change(uploader, { + target: { files: [file] }, + }); + + uploader = getByTestId('syntheticsFleetScriptRecorderUploader') as HTMLInputElement; + + await waitFor(() => { + expect(onChange).not.toBeCalled(); + expect( + getByText( + 'Error uploading file. Please upload a .js file generated by the Elastic Synthetics Recorder in inline script format.' + ) + ).toBeInTheDocument(); + }); + }); + + it('shows show script button when script is available', () => { + const { getByText, queryByText } = render(); + + const showScriptBtn = getByText('Show script'); + + expect(queryByText(testScript)).not.toBeInTheDocument(); + + fireEvent.click(showScriptBtn); + + expect(getByText(testScript)).toBeInTheDocument(); + }); + + it('shows show remove script button when script is available and isEditable is true', async () => { + const { getByText, getByTestId } = render( + + ); + + const showScriptBtn = getByText('Show script'); + fireEvent.click(showScriptBtn); + + expect(getByText(testScript)).toBeInTheDocument(); + + fireEvent.click(getByTestId('euiFlyoutCloseButton')); + + const removeScriptBtn = getByText('Remove script'); + + fireEvent.click(removeScriptBtn); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ scriptText: '', fileName: '' }); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/script_recorder_fields.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/script_recorder_fields.tsx new file mode 100644 index 000000000000000..3704fd4f4ec110c --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/script_recorder_fields.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutHeader, + EuiFormRow, + EuiCodeBlock, + EuiTitle, + EuiButton, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { Uploader } from './uploader'; + +interface Props { + onChange: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void; + script: string; + fileName?: string; + isEditable?: boolean; +} + +export function ScriptRecorderFields({ onChange, script, fileName, isEditable }: Props) { + const [showScript, setShowScript] = useState(false); + + const handleUpload = useCallback( + ({ scriptText, fileName: fileNameT }: { scriptText: string; fileName: string }) => { + onChange({ scriptText, fileName: fileNameT }); + }, + [onChange] + ); + + return ( + <> + + {isEditable && script ? ( + + + {fileName} + + + ) : ( + + )} + {script && ( + <> + + + + setShowScript(true)} + iconType="editorCodeBlock" + iconSide="right" + > + + + + + {isEditable && ( + onChange({ scriptText: '', fileName: '' })} + iconType="trash" + iconSide="right" + color="danger" + > + + + )} + + + + )} + {showScript && ( + setShowScript(false)} + aria-labelledby="syntheticsBrowserScriptBlockHeader" + closeButtonAriaLabel={CLOSE_BUTTON_LABEL} + > + + + + {fileName || PLACEHOLDER_FILE_NAME} + + + +
+ + {script} + +
+
+ )} + + ); +} + +const PLACEHOLDER_FILE_NAME = i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.mockFileName', + { + defaultMessage: 'test_script.js', + } +); + +const CLOSE_BUTTON_LABEL = i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.closeButtonLabel', + { + defaultMessage: 'Close script flyout', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.test.tsx new file mode 100644 index 000000000000000..b52cfe346a80d6e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.test.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../utils/testing/rtl_helpers'; +import { SourceField } from './source_field'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'), + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( + { + props.onChange(e.jsonContent); + }} + /> + ), + }; +}); + +const onChange = jest.fn(); +const onBlur = jest.fn(); + +describe('', () => { + const WrappedComponent = ({ + script = '', + fileName = '', + type = 'recorder', + isEdit = false, + }: { + isEditable?: boolean; + script?: string; + fileName?: string; + type?: 'recorder' | 'inline'; + isEdit?: boolean; + }) => { + return ( + + ); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders ScriptRecorderFields as the default tab', () => { + const { getByText } = render(); + + expect(getByText('Select or drag and drop a .js file')).toBeInTheDocument(); + }); + + it('changes to code editor when selected', async () => { + const script = 'test script'; + const { getByTestId } = render(); + expect(getByTestId('codeEditorContainer')).toBeInTheDocument(); + }); + + it('displays code editor by default in edit flow', async () => { + const fileName = 'fileName'; + const script = 'test script'; + const { getByTestId } = render( + + ); + expect(getByTestId('codeEditorContainer')).toBeInTheDocument(); + }); + + it('displays filename of existing script in edit flow', async () => { + const fileName = 'fileName'; + const script = 'test script'; + const { getByText } = render( + + ); + userEvent.click(getByText(/Upload new script/)); + expect(getByText(fileName)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx new file mode 100644 index 000000000000000..6ddb0b27a8f6b7d --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { + EuiTabbedContent, + EuiFormRow, + EuiBetaBadge, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { CodeEditor } from './code_editor'; +import { ScriptRecorderFields } from './script_recorder_fields'; +import { ConfigKey, MonacoEditorLangId } from '../types'; + +enum SourceType { + INLINE = 'syntheticsBrowserInlineConfig', + SCRIPT_RECORDER = 'syntheticsBrowserScriptRecorderConfig', + ZIP = 'syntheticsBrowserZipURLConfig', +} + +interface SourceConfig { + script: string; + type: 'recorder' | 'inline'; + fileName?: string; +} + +interface Props { + onChange: (sourceConfig: SourceConfig) => void; + onBlur: (field: ConfigKey) => void; + value: SourceConfig; + isEditFlow?: boolean; +} + +export const SourceField = ({ onChange, onBlur, value, isEditFlow = false }: Props) => { + const [sourceType, setSourceType] = useState( + value.type === 'inline' ? SourceType.INLINE : SourceType.SCRIPT_RECORDER + ); + const [config, setConfig] = useState(value); + + useEffect(() => { + onChange(config); + }, [config, onChange]); + + const allTabs = [ + { + id: 'syntheticsBrowserScriptRecorderConfig', + name: ( + + + {isEditFlow ? ( + + ) : ( + + )} + + + + + + ), + 'data-test-subj': 'syntheticsSourceTab__scriptRecorder', + content: ( + { + setConfig((prevConfig) => ({ + ...prevConfig, + script: scriptText, + type: 'recorder', + fileName, + })); + }} + script={config.script} + isEditable={isEditFlow} + fileName={config.fileName} + /> + ), + }, + { + id: 'syntheticsBrowserInlineConfig', + name: ( + + ), + 'data-test-subj': `syntheticsSourceTab__inline`, + content: ( + + } + fullWidth + > + { + setConfig((prevConfig) => ({ ...prevConfig, script: code })); + onBlur(ConfigKey.SOURCE_INLINE); + }} + value={config.script} + placeholder={i18n.translate( + 'xpack.synthetics.addEditMonitor.scriptEditor.placeholder', + { + defaultMessage: '// Paste your Playwright script here...', + } + )} + /> + + ), + }, + ]; + + if (isEditFlow) { + allTabs.reverse(); + } + + return ( + tab.id === SourceType.INLINE) + : allTabs.find((tab) => tab.id === sourceType) + } + autoFocus="selected" + onTabClick={(tab) => { + if (tab.id !== sourceType) { + setConfig({ + script: '', + type: tab.id === SourceType.INLINE ? 'inline' : 'recorder', + fileName: '', + }); + } + setSourceType(tab.id as SourceType); + }} + /> + ); +}; + +const StyledBetaBadgeWrapper = styled(EuiFlexItem)` + .euiToolTipAnchor { + display: flex; + } +`; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/uploader.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/uploader.tsx new file mode 100644 index 000000000000000..e028e24504f0311 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/uploader.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useRef } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiFormRow, EuiFilePicker } from '@elastic/eui'; + +interface Props { + onUpload: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void; +} + +export function Uploader({ onUpload }: Props) { + const fileReader = useRef(null); + const [error, setError] = useState(null); + const filePickerRef = useRef(null); + + const handleFileRead = (fileName: string) => { + const content = fileReader?.current?.result as string; + + if (content?.trim().slice(0, 4) !== 'step') { + setError(PARSING_ERROR); + filePickerRef.current?.removeFiles(); + return; + } + + onUpload({ scriptText: content, fileName }); + setError(null); + }; + + const handleFileChosen = (files: FileList | null) => { + if (!files || !files.length) { + onUpload({ scriptText: '', fileName: '' }); + return; + } + if (files.length && !files[0].type.includes('javascript')) { + setError(INVALID_FILE_ERROR); + filePickerRef.current?.removeFiles(); + return; + } + fileReader.current = new FileReader(); + fileReader.current.onloadend = () => handleFileRead(files[0].name); + fileReader.current.readAsText(files[0]); + }; + + return ( + + + + ); +} + +const TESTING_SCRIPT_LABEL = i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.fieldLabel', + { + defaultMessage: 'Testing script', + } +); + +const PROMPT_TEXT = i18n.translate('xpack.synthetics.monitorConfig.uploader.label', { + defaultMessage: 'Select or drag and drop a .js file', +}); + +const INVALID_FILE_ERROR = i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.invalidFileError', + { + defaultMessage: + 'Invalid file type. Please upload a .js file generated by the Elastic Synthetics Recorder.', + } +); + +const PARSING_ERROR = i18n.translate( + 'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.parsingError', + { + defaultMessage: + 'Error uploading file. Please upload a .js file generated by the Elastic Synthetics Recorder in inline script format.', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/controlled_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/controlled_field.tsx new file mode 100644 index 000000000000000..8248500d351c505 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/controlled_field.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFormRow, EuiFormRowProps } from '@elastic/eui'; +import { useSelector } from 'react-redux'; +import { + UseFormReturn, + ControllerRenderProps, + ControllerFieldState, + useFormContext, +} from 'react-hook-form'; +import { useKibanaSpace, useIsEditFlow } from '../hooks'; +import { selectServiceLocationsState } from '../../../state'; +import { FieldMeta } from '../types'; + +type Props = FieldMeta & { + component: React.ComponentType; + field: ControllerRenderProps; + fieldState: ControllerFieldState; + formRowProps: Partial; + error: React.ReactNode; + dependenciesValues: unknown[]; + dependenciesFieldMeta: Record; +}; + +const setFieldValue = (key: string, setValue: UseFormReturn['setValue']) => (value: any) => { + setValue(key, value); +}; + +export const ControlledField = ({ + component: FieldComponent, + props, + fieldKey, + shouldUseSetValue, + field, + formRowProps, + fieldState, + customHook, + error, + dependenciesValues, + dependenciesFieldMeta, +}: Props) => { + const { setValue, reset, formState } = useFormContext(); + const noop = () => {}; + let hook: Function = noop; + let hookProps; + const { locations } = useSelector(selectServiceLocationsState); + const { space } = useKibanaSpace(); + const isEdit = useIsEditFlow(); + if (customHook) { + hookProps = customHook(field.value); + hook = hookProps.func; + } + const { [hookProps?.fieldKey as string]: hookResult } = hook(hookProps?.params) || {}; + const onChange = shouldUseSetValue ? setFieldValue(fieldKey, setValue) : field.onChange; + const generatedProps = props + ? props({ + field, + setValue, + reset, + locations, + dependencies: dependenciesValues, + dependenciesFieldMeta, + space: space?.id, + isEdit, + formState, + }) + : {}; + const isInvalid = hookResult || Boolean(fieldState.error); + const hookError = hookResult ? hookProps?.error : undefined; + return ( + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.test.tsx new file mode 100644 index 000000000000000..192c76e414b07a5 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.test.tsx @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ConfigKey, DataStream, FormMonitorType, SyntheticsMonitor } from '../types'; +import { DEFAULT_FIELDS } from '../constants'; +import { formatDefaultFormValues } from './defaults'; + +describe('defaults', () => { + const testScript = 'testScript'; + const monitorValues = { + __ui: { + script_source: { + file_name: '', + is_generated_script: false, + }, + }, + enabled: true, + 'filter_journeys.match': '', + 'filter_journeys.tags': [], + form_monitor_type: 'multistep', + ignore_https_errors: false, + journey_id: '', + locations: [ + { + id: 'us_central', + isServiceManaged: true, + }, + ], + name: 'Browser monitor', + namespace: 'default', + origin: 'ui', + params: '', + playwright_options: '', + playwright_text_assertion: '', + project_id: '', + schedule: { + number: '10', + unit: 'm', + }, + screenshots: 'on', + 'service.name': '', + 'source.inline.script': testScript, + 'source.project.content': '', + 'source.zip_url.folder': '', + 'source.zip_url.password': '', + 'source.zip_url.proxy_url': '', + 'source.zip_url.ssl.certificate': undefined, + 'source.zip_url.ssl.certificate_authorities': undefined, + 'source.zip_url.ssl.key': undefined, + 'source.zip_url.ssl.key_passphrase': undefined, + 'source.zip_url.ssl.supported_protocols': undefined, + 'source.zip_url.ssl.verification_mode': undefined, + 'source.zip_url.url': '', + 'source.zip_url.username': '', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + synthetics_args: [], + tags: [], + 'throttling.config': '5d/3u/20l', + 'throttling.download_speed': '5', + 'throttling.is_enabled': true, + 'throttling.latency': '20', + 'throttling.upload_speed': '3', + timeout: '16', + type: 'browser', + 'url.port': null, + urls: '', + } as SyntheticsMonitor; + + it('correctly formats monitor type to form type', () => { + expect(formatDefaultFormValues(monitorValues)).toEqual({ + __ui: { + script_source: { + file_name: '', + is_generated_script: false, + }, + }, + enabled: true, + 'filter_journeys.match': '', + 'filter_journeys.tags': [], + form_monitor_type: 'multistep', + ignore_https_errors: false, + journey_id: '', + locations: [ + { + id: 'us_central', + isServiceManaged: true, + }, + ], + name: 'Browser monitor', + namespace: 'default', + origin: 'ui', + params: '', + playwright_options: '', + playwright_text_assertion: '', + project_id: '', + schedule: { + number: '10', + unit: 'm', + }, + screenshots: 'on', + 'service.name': '', + 'source.inline': { + fileName: '', + script: 'testScript', + type: 'inline', + }, + 'source.inline.script': 'testScript', + 'source.project.content': '', + 'source.zip_url.folder': '', + 'source.zip_url.password': '', + 'source.zip_url.proxy_url': '', + 'source.zip_url.ssl.certificate': undefined, + 'source.zip_url.ssl.certificate_authorities': undefined, + 'source.zip_url.ssl.key': undefined, + 'source.zip_url.ssl.key_passphrase': undefined, + 'source.zip_url.ssl.supported_protocols': undefined, + 'source.zip_url.ssl.verification_mode': undefined, + 'source.zip_url.url': '', + 'source.zip_url.username': '', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + synthetics_args: [], + tags: [], + 'throttling.config': '5d/3u/20l', + 'throttling.download_speed': '5', + 'throttling.is_enabled': true, + 'throttling.latency': '20', + 'throttling.upload_speed': '3', + timeout: '16', + type: 'browser', + 'url.port': null, + urls: '', + }); + }); + + it.each([ + [DataStream.HTTP, 'testCA'], + [DataStream.HTTP, ''], + [DataStream.TCP, 'testCA'], + [DataStream.TCP, ''], + ])('correctly formats isTLSEnabled', (formType, testCA) => { + const monitor = { + ...DEFAULT_FIELDS[formType as DataStream], + [ConfigKey.FORM_MONITOR_TYPE]: formType as unknown as FormMonitorType, + [ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: testCA, + }; + expect(formatDefaultFormValues(monitor)).toEqual({ + ...monitor, + isTLSEnabled: Boolean(testCA), + [ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: testCA, + }); + }); + + it.each([ + [DataStream.HTTP, FormMonitorType.HTTP], + [DataStream.TCP, FormMonitorType.TCP], + [DataStream.ICMP, FormMonitorType.ICMP], + [DataStream.BROWSER, FormMonitorType.MULTISTEP], + ])( + 'correctly formats legacy uptime monitors to include ConfigKey.FORM_MONITOR_TYPE', + (dataStream, formType) => { + const monitor = { + ...DEFAULT_FIELDS[dataStream], + [ConfigKey.FORM_MONITOR_TYPE]: undefined, + }; + expect(formatDefaultFormValues(monitor as unknown as SyntheticsMonitor)).toEqual( + expect.objectContaining({ + [ConfigKey.FORM_MONITOR_TYPE]: formType, + }) + ); + } + ); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.tsx new file mode 100644 index 000000000000000..c8412a866dbb7ec --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { isEqual } from 'lodash'; +import { DEFAULT_FIELDS, DEFAULT_TLS_FIELDS } from '../constants'; +import { + ConfigKey, + DataStream, + FormMonitorType, + SyntheticsMonitor, + BrowserFields, + TLSFields, +} from '../types'; + +export const getDefaultFormFields = ( + spaceId: string = 'default' +): Record> => { + return { + [FormMonitorType.MULTISTEP]: { + ...DEFAULT_FIELDS[DataStream.BROWSER], + 'source.inline': { + type: 'recorder', + script: '', + fileName: '', + }, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP, + [ConfigKey.NAMESPACE]: spaceId, + }, + [FormMonitorType.SINGLE]: { + ...DEFAULT_FIELDS[DataStream.BROWSER], + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.SINGLE, + [ConfigKey.NAMESPACE]: spaceId, + }, + [FormMonitorType.HTTP]: { + ...DEFAULT_FIELDS[DataStream.HTTP], + isTLSEnabled: false, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP, + [ConfigKey.NAMESPACE]: spaceId, + }, + [FormMonitorType.TCP]: { + ...DEFAULT_FIELDS[DataStream.TCP], + isTLSEnabled: false, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP, + [ConfigKey.NAMESPACE]: spaceId, + }, + [FormMonitorType.ICMP]: { + ...DEFAULT_FIELDS[DataStream.ICMP], + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP, + [ConfigKey.NAMESPACE]: spaceId, + }, + }; +}; + +export const formatDefaultFormValues = (monitor?: SyntheticsMonitor) => { + if (!monitor) return undefined; + + let formMonitorType = monitor[ConfigKey.FORM_MONITOR_TYPE]; + const monitorType = monitor[ConfigKey.MONITOR_TYPE]; + const monitorWithFormMonitorType = { + ...monitor, + }; + + // handle default monitor types from Uptime, which don't contain `ConfigKey.FORM_MONITOR_TYPE` + if (!formMonitorType) { + formMonitorType = + monitorType === DataStream.BROWSER + ? FormMonitorType.MULTISTEP + : (monitorType as Omit as FormMonitorType); + monitorWithFormMonitorType[ConfigKey.FORM_MONITOR_TYPE] = formMonitorType; + } + + switch (formMonitorType) { + case FormMonitorType.MULTISTEP: + const browserMonitor = monitor as BrowserFields; + return { + ...monitorWithFormMonitorType, + 'source.inline': { + type: browserMonitor[ConfigKey.METADATA]?.script_source?.is_generated_script + ? 'recorder' + : 'inline', + script: browserMonitor[ConfigKey.SOURCE_INLINE], + fileName: browserMonitor[ConfigKey.METADATA]?.script_source?.file_name, + }, + }; + case FormMonitorType.SINGLE: + case FormMonitorType.ICMP: + return { + ...monitorWithFormMonitorType, + }; + case FormMonitorType.HTTP: + case FormMonitorType.TCP: + return { + ...monitorWithFormMonitorType, + isTLSEnabled: isCustomTLSEnabled(monitor), + }; + } +}; + +const isCustomTLSEnabled = (monitor: SyntheticsMonitor) => { + const sslKeys = Object.keys(monitor).filter((key) => key.includes('ssl')) as unknown as Array< + keyof TLSFields + >; + const sslValues: Record = {}; + sslKeys.map((key) => (sslValues[key] = (monitor as TLSFields)[key])); + return !isEqual(sslValues, DEFAULT_TLS_FIELDS); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/disclaimer.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/disclaimer.test.tsx new file mode 100644 index 000000000000000..2265b035876f734 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/disclaimer.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '../../../utils/testing/rtl_helpers'; +import * as formContext from 'react-hook-form'; +import { Disclaimer } from './disclaimer'; +import { ServiceLocations } from '../types'; + +export const mockLocation = { + label: 'US Central', + id: 'us_central', + geo: { + lat: 1, + lon: 1, + }, + url: 'url', + isServiceManaged: true, +}; +describe('', () => { + beforeEach(() => { + jest.spyOn(formContext, 'useFormContext').mockReturnValue({ + watch: () => [[mockLocation] as ServiceLocations], + } as unknown as formContext.UseFormReturn); + }); + + it('shows disclaimer when ', () => { + const { getByText } = render(); + + expect(getByText(/You consent/)).toBeInTheDocument(); + }); + + it('does not show disclaimer when locations are not service managed', () => { + jest.spyOn(formContext, 'useFormContext').mockReturnValue({ + watch: () => [[{ ...mockLocation, isServiceManaged: false }] as ServiceLocations], + } as unknown as formContext.UseFormReturn); + const { queryByText } = render(); + + expect(queryByText(/You consent/)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/disclaimer.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/disclaimer.tsx new file mode 100644 index 000000000000000..58e182be0b05377 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/disclaimer.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiText, EuiSpacer } from '@elastic/eui'; +import { useFormContext } from 'react-hook-form'; +import { ConfigKey, MonitorServiceLocation } from '../types'; + +export const Disclaimer: React.FC = () => { + const { watch } = useFormContext(); + const [locations]: [locations: MonitorServiceLocation[]] = watch([ConfigKey.LOCATIONS]); + + const includesServiceLocation = locations.find((location) => location.isServiceManaged === true); + + return includesServiceLocation ? ( + <> + + +

+ {i18n.translate('xpack.synthetics.monitorConfig.locations.disclaimer', { + defaultMessage: + 'You consent to the transfer of testing instructions and the output of such instructions (including any data shown therein) to your selected testing location, on infrastructure provided by a cloud service provider chosen by Elastic.', + })} +

+
+ + ) : null; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field.tsx new file mode 100644 index 000000000000000..fbb0943b5f77322 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { memo, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Controller, useFormContext, FieldError, ControllerFieldState } from 'react-hook-form'; +import { EuiFormRow } from '@elastic/eui'; +import { selectServiceLocationsState } from '../../../state'; +import { useKibanaSpace, useIsEditFlow } from '../hooks'; +import { ControlledField } from './controlled_field'; +import { FieldMeta } from '../types'; + +type Props = FieldMeta & { fieldError?: FieldError }; + +export const Field = memo( + ({ + component: Component, + helpText, + label, + ariaLabel, + props, + fieldKey, + controlled, + showWhen, + shouldUseSetValue, + required, + validation, + error, + fieldError, + dependencies, + customHook, + }: Props) => { + const { register, watch, control, setValue, reset, getFieldState, formState } = + useFormContext(); + const { locations } = useSelector(selectServiceLocationsState); + const { space } = useKibanaSpace(); + const isEdit = useIsEditFlow(); + const [dependenciesFieldMeta, setDependenciesFieldMeta] = useState< + Record + >({}); + let show = true; + let dependenciesValues: unknown[] = []; + if (showWhen) { + const [showKey, expectedValue] = showWhen; + const [actualValue] = watch([showKey]); + show = actualValue === expectedValue; + } + if (dependencies) { + dependenciesValues = watch(dependencies); + } + useEffect(() => { + if (dependencies) { + dependencies.forEach((dependency) => { + setDependenciesFieldMeta((prevState) => ({ + ...prevState, + [dependency]: getFieldState(dependency), + })); + }); + } + // run effect when dependencies values change, to get the most up to date meta state + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(dependenciesValues || []), dependencies, getFieldState]); + + if (!show) { + return null; + } + + const formRowProps = { + label, + 'aria-label': ariaLabel, + helpText, + fullWidth: true, + }; + + return controlled ? ( + { + return ( + + ); + }} + /> + ) : ( + + + + ); + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx new file mode 100644 index 000000000000000..b3d7f10c2b7b53c --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx @@ -0,0 +1,980 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { UseFormReturn, ControllerRenderProps, FormState } from 'react-hook-form'; +import { + EuiButtonGroup, + EuiCheckbox, + EuiCode, + EuiComboBox, + EuiComboBoxOptionOption, + EuiComboBoxProps, + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiFieldNumber, + EuiFieldPassword, + EuiSelect, + EuiSuperSelect, + EuiSwitch, + EuiText, + EuiLink, + EuiTextArea, +} from '@elastic/eui'; +import { useMonitorName } from '../hooks/use_monitor_name'; +import { MonitorTypeRadioGroup } from '../fields/monitor_type_radio_group'; +import { + ConfigKey, + DataStream, + FormMonitorType, + HTTPMethod, + MonitorFields, + MonitorServiceLocations, + ScreenshotOption, + ServiceLocations, + SyntheticsMonitor, + TLSVersion, + VerificationMode, + FieldMeta, +} from '../types'; +import { DEFAULT_BROWSER_ADVANCED_FIELDS } from '../constants'; +import { HeaderField } from '../fields/header_field'; +import { RequestBodyField } from '../fields/request_body_field'; +import { ResponseBodyIndexField } from '../fields/index_response_body_field'; +import { ComboBox } from '../fields/combo_box'; +import { SourceField } from '../fields/source_field'; +import { getDefaultFormFields } from './defaults'; +import { validate, validateHeaders, WHOLE_NUMBERS_ONLY, FLOATS_ONLY } from './validation'; + +const getScheduleContent = (value: number) => { + if (value > 60) { + return i18n.translate('xpack.synthetics.monitorConfig.schedule.label', { + defaultMessage: 'Every {value, number} {value, plural, one {hour} other {hours}}', + values: { + value: value / 60, + }, + }); + } else { + return i18n.translate('xpack.synthetics.monitorConfig.schedule.minutes.label', { + defaultMessage: 'Every {value, number} {value, plural, one {minute} other {minutes}}', + values: { + value, + }, + }); + } +}; + +const getScheduleConfig = (schedules: number[]) => { + return schedules.map((value) => ({ + value: `${value}`, + text: getScheduleContent(value), + })); +}; + +const BROWSER_SCHEDULES = getScheduleConfig([3, 5, 10, 15, 30, 60, 120, 240]); + +const LIGHTWEIGHT_SCHEDULES = getScheduleConfig([1, 3, 5, 10, 15, 30, 60]); + +export const MONITOR_TYPE_CONFIG = { + [FormMonitorType.MULTISTEP]: { + id: 'syntheticsMonitorTypeMultistep', + 'data-test-subj': 'syntheticsMonitorTypeMultistep', + label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.multiStep.label', { + defaultMessage: 'Multistep', + }), + value: FormMonitorType.MULTISTEP, + descriptionTitle: i18n.translate('xpack.synthetics.monitorConfig.monitorType.multiStep.title', { + defaultMessage: 'Multistep Browser Journey', + }), + description: i18n.translate( + 'xpack.synthetics.monitorConfig.monitorType.multiStep.description', + { + defaultMessage: + 'Navigate through multiple steps or pages to test key user flows from a real browser.', + } + ), + link: '#', + icon: 'videoPlayer', + beta: true, + }, + [FormMonitorType.SINGLE]: { + id: 'syntheticsMonitorTypeSingle', + 'data-test-subj': 'syntheticsMonitorTypeSingle', + label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.singlePage.label', { + defaultMessage: 'Single Page', + }), + value: FormMonitorType.SINGLE, + descriptionTitle: i18n.translate( + 'xpack.synthetics.monitorConfig.monitorType.singlePage.title', + { + defaultMessage: 'Single Page Browser Test', + } + ), + description: i18n.translate( + 'xpack.synthetics.monitorConfig.monitorType.singlePage.description', + { + defaultMessage: + 'Test a single page load including all objects on the page from a real web browser.', + } + ), + link: '#', + icon: 'videoPlayer', + beta: true, + }, + [FormMonitorType.HTTP]: { + id: 'syntheticsMonitorTypeHTTP', + 'data-test-subj': 'syntheticsMonitorTypeHTTP', + label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.http.label', { + defaultMessage: 'HTTP Ping', + }), + value: FormMonitorType.HTTP, + descriptionTitle: i18n.translate('xpack.synthetics.monitorConfig.monitorType.http.title', { + defaultMessage: 'HTTP Ping', + }), + description: i18n.translate('xpack.synthetics.monitorConfig.monitorType.http.description', { + defaultMessage: + 'A lightweight API check to validate the availability of a web service or endpoint.', + }), + link: '#', + icon: 'online', + beta: false, + }, + [FormMonitorType.TCP]: { + id: 'syntheticsMonitorTypeTCP', + 'data-test-subj': 'syntheticsMonitorTypeTCP', + label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.tcp.label', { + defaultMessage: 'TCP Ping', + }), + value: FormMonitorType.TCP, + descriptionTitle: i18n.translate('xpack.synthetics.monitorConfig.monitorType.tcp.title', { + defaultMessage: 'TCP Ping', + }), + description: i18n.translate('xpack.synthetics.monitorConfig.monitorType.tcp.description', { + defaultMessage: + 'A lightweight API check to validate the availability of a web service or endpoint.', + }), + link: '#', + icon: 'online', + beta: false, + }, + [FormMonitorType.ICMP]: { + id: 'syntheticsMonitorTypeICMP', + 'data-test-subj': 'syntheticsMonitorTypeICMP', + label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.icmp.label', { + defaultMessage: 'ICMP Ping', + }), + value: FormMonitorType.ICMP, + descriptionTitle: i18n.translate('xpack.synthetics.monitorConfig.monitorType.icmp.title', { + defaultMessage: 'ICMP Ping', + }), + description: i18n.translate('xpack.synthetics.monitorConfig.monitorType.icmp.description', { + defaultMessage: + 'A lightweight API check to validate the availability of a web service or endpoint.', + }), + link: '#', + icon: 'online', + beta: false, + }, +}; + +export const FIELD: Record = { + [ConfigKey.FORM_MONITOR_TYPE]: { + fieldKey: ConfigKey.FORM_MONITOR_TYPE, + required: true, + component: MonitorTypeRadioGroup, + ariaLabel: i18n.translate('xpack.synthetics.monitorConfig.monitorType.label', { + defaultMessage: 'Monitor type', + }), + controlled: true, + props: ({ field, reset, space }) => ({ + onChange: (_: string, monitorType: FormMonitorType) => { + const defaultFields = getDefaultFormFields(space)[monitorType]; + reset(defaultFields); + }, + selectedOption: field?.value, + options: Object.values(MONITOR_TYPE_CONFIG), + }), + validation: () => ({ + required: true, + }), + }, + [`${ConfigKey.URLS}__single`]: { + fieldKey: ConfigKey.URLS, + required: true, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.urlsSingle.label', { + defaultMessage: 'Website URL', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.urlsSingle.helpText', { + defaultMessage: 'For example, https://www.elastic.co.', + }), + controlled: true, + dependencies: [ConfigKey.NAME], + props: ({ setValue, dependenciesFieldMeta, isEdit, formState }) => { + return { + 'data-test-subj': 'syntheticsMonitorConfigURL', + onChange: (event: React.ChangeEvent) => { + setValue(ConfigKey.URLS, event.target.value, { + shouldValidate: Boolean(formState.submitCount > 0), + }); + if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) { + setValue(ConfigKey.NAME, event.target.value, { + shouldValidate: Boolean(formState.submitCount > 0), + }); + } + }, + }; + }, + }, + [`${ConfigKey.URLS}__http`]: { + fieldKey: ConfigKey.URLS, + required: true, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.urls.label', { + defaultMessage: 'URL', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.urls.helpText', { + defaultMessage: 'For example, your service endpoint.', + }), + controlled: true, + dependencies: [ConfigKey.NAME], + props: ({ setValue, dependenciesFieldMeta, isEdit, formState }) => { + return { + onChange: (event: React.ChangeEvent) => { + setValue(ConfigKey.URLS, event.target.value, { + shouldValidate: Boolean(formState.submitCount > 0), + }); + if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) { + setValue(ConfigKey.NAME, event.target.value, { + shouldValidate: Boolean(formState.submitCount > 0), + }); + } + }, + 'data-test-subj': 'syntheticsMonitorConfigURL', + }; + }, + }, + [`${ConfigKey.HOSTS}__tcp`]: { + fieldKey: ConfigKey.HOSTS, + required: true, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.hostsTCP.label', { + defaultMessage: 'Host:Port', + }), + controlled: true, + dependencies: [ConfigKey.NAME], + props: ({ setValue, dependenciesFieldMeta, isEdit, formState }) => { + return { + onChange: (event: React.ChangeEvent) => { + setValue(ConfigKey.HOSTS, event.target.value, { + shouldValidate: Boolean(formState.submitCount > 0), + }); + if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) { + setValue(ConfigKey.NAME, event.target.value, { + shouldValidate: Boolean(formState.submitCount > 0), + }); + } + }, + 'data-test-subj': 'syntheticsMonitorConfigHost', + }; + }, + }, + [`${ConfigKey.HOSTS}__icmp`]: { + fieldKey: ConfigKey.HOSTS, + required: true, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.hostsICMP.label', { + defaultMessage: 'Host', + }), + controlled: true, + dependencies: [ConfigKey.NAME], + props: ({ setValue, dependenciesFieldMeta, isEdit, formState }) => { + return { + onChange: (event: React.ChangeEvent) => { + setValue(ConfigKey.HOSTS, event.target.value, { + shouldValidate: Boolean(formState.submitCount > 0), + }); + if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) { + setValue(ConfigKey.NAME, event.target.value, { + shouldValidate: Boolean(formState.submitCount > 0), + }); + } + }, + 'data-test-subj': 'syntheticsMonitorConfigHost', + }; + }, + }, + [ConfigKey.NAME]: { + fieldKey: ConfigKey.NAME, + required: true, + component: EuiFieldText, + controlled: true, + label: i18n.translate('xpack.synthetics.monitorConfig.name.label', { + defaultMessage: 'Monitor name', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.name.helpText', { + defaultMessage: 'Choose a name to help identify this monitor in the future.', + }), + dependencies: [ConfigKey.URLS, ConfigKey.HOSTS], + customHook: (value: unknown) => ({ + fieldKey: 'nameAlreadyExists', + func: useMonitorName, + params: { search: value as string }, + error: i18n.translate('xpack.synthetics.monitorConfig.name.existsError', { + defaultMessage: 'Monitor name already exists', + }), + }), + validation: () => ({ + validate: { + notEmpty: (value) => Boolean(value.trim()), + }, + }), + error: i18n.translate('xpack.synthetics.monitorConfig.name.error', { + defaultMessage: 'Monitor name is required', + }), + props: () => ({ + 'data-test-subj': 'syntheticsMonitorConfigName', + }), + }, + [ConfigKey.SCHEDULE]: { + fieldKey: `${ConfigKey.SCHEDULE}.number`, + required: true, + component: EuiSelect, + label: i18n.translate('xpack.synthetics.monitorConfig.frequency.label', { + defaultMessage: 'Frequency', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.frequency.helpText', { + defaultMessage: + 'How often do you want to run this test? Higher frequencies will increase your total cost.', + }), + dependencies: [ConfigKey.MONITOR_TYPE], + props: ({ dependencies }) => { + const [monitorType] = dependencies; + return { + 'data-test-subj': 'syntheticsMonitorConfigSchedule', + options: monitorType === DataStream.BROWSER ? BROWSER_SCHEDULES : LIGHTWEIGHT_SCHEDULES, + }; + }, + }, + [ConfigKey.LOCATIONS]: { + fieldKey: ConfigKey.LOCATIONS, + required: true, + controlled: true, + component: EuiComboBox as React.ComponentType>, + label: i18n.translate('xpack.synthetics.monitorConfig.locations.label', { + defaultMessage: 'Locations', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.locations.helpText', { + defaultMessage: + 'Where do you want to run this test from? Additional locations will increase your total cost.', + }), + props: ({ + field, + setValue, + locations, + formState, + }: { + field?: ControllerRenderProps; + setValue: UseFormReturn['setValue']; + locations: ServiceLocations; + formState: FormState; + }) => { + return { + options: Object.values(locations).map((location) => ({ + label: locations?.find((loc) => location.id === loc.id)?.label, + id: location.id, + isServiceManaged: location.isServiceManaged, + })), + selectedOptions: Object.values(field?.value as ServiceLocations).map((location) => ({ + label: locations?.find((loc) => location.id === loc.id)?.label, + id: location.id, + isServiceManaged: location.isServiceManaged, + })), + 'data-test-subj': 'syntheticsMonitorConfigLocations', + onChange: (updatedValues: ServiceLocations) => { + setValue( + ConfigKey.LOCATIONS, + updatedValues.map((location) => ({ + id: location.id, + isServiceManaged: location.isServiceManaged, + })) as MonitorServiceLocations, + { shouldValidate: Boolean(formState.submitCount > 0) } + ); + }, + }; + }, + }, + [ConfigKey.TAGS]: { + fieldKey: ConfigKey.TAGS, + component: ComboBox, + label: i18n.translate('xpack.synthetics.monitorConfig.tags.label', { + defaultMessage: 'Tags', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.tags.helpText', { + defaultMessage: + 'A list of tags that will be sent with each monitor event. Useful for searching and segmenting data.', + }), + controlled: true, + props: ({ field }) => ({ + selectedOptions: field?.value, + }), + }, + [ConfigKey.TIMEOUT]: { + fieldKey: ConfigKey.TIMEOUT, + component: EuiFieldNumber, + label: i18n.translate('xpack.synthetics.monitorConfig.timeout.label', { + defaultMessage: 'Timeout in seconds', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.timeout.helpText', { + defaultMessage: 'The total time allowed for testing the connection and exchanging data.', + }), + props: () => ({ + min: 1, + step: 'any', + }), + dependencies: [ConfigKey.SCHEDULE], + validation: ([schedule]) => { + return { + validate: (value) => { + switch (true) { + case value < 0: + return i18n.translate('xpack.synthetics.monitorConfig.timeout.greaterThan0Error', { + defaultMessage: 'Timeout must be greater than or equal to 0.', + }); + case value > parseFloat((schedule as MonitorFields[ConfigKey.SCHEDULE]).number) * 60: + return i18n.translate('xpack.synthetics.monitorConfig.timeout.scheduleError', { + defaultMessage: 'Timemout must be less than the monitor frequency.', + }); + case !Boolean(`${value}`.match(FLOATS_ONLY)): + return i18n.translate('xpack.synthetics.monitorConfig.timeout.formatError', { + defaultMessage: 'Timeout is invalid.', + }); + default: + return true; + } + }, + }; + }, + }, + [ConfigKey.APM_SERVICE_NAME]: { + fieldKey: ConfigKey.APM_SERVICE_NAME, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.apmServiceName.label', { + defaultMessage: 'APM service name', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.apmServiceName.helpText', { + defaultMessage: + 'Corrseponds to the service.name ECS field from APM. Set this to enable integrations between APM and Synthetics data.', + }), + controlled: true, + props: ({ field }) => ({ + selectedOptions: field?.value, + 'data-test-subj': 'syntheticsMonitorConfigAPMServiceName', + }), + }, + [ConfigKey.NAMESPACE]: { + fieldKey: ConfigKey.NAMESPACE, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.namespace.label', { + defaultMessage: 'Data stream namespace', + }), + helpText: ( + + {i18n.translate('xpack.synthetics.monitorConfig.namespace.helpText', { + defaultMessage: + "Change the default namespace. This setting changes the name of the monitor's data stream. ", + })} + + {i18n.translate('xpack.synthetics.monitorConfig.namespace.learnMore', { + defaultMessage: 'Learn more', + })} + + + ), + controlled: true, + props: ({ field }) => ({ + selectedOptions: field, + }), + }, + [ConfigKey.MAX_REDIRECTS]: { + fieldKey: ConfigKey.MAX_REDIRECTS, + component: EuiFieldNumber, + label: i18n.translate('xpack.synthetics.monitorConfig.maxRedirects.label', { + defaultMessage: 'Max redirects', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.maxRedirects.helpText', { + defaultMessage: 'The total number of redirects to follow.', + }), + props: () => ({ + min: 0, + max: 10, + step: 1, + }), + validation: () => ({ + min: 0, + pattern: WHOLE_NUMBERS_ONLY, + }), + error: i18n.translate('xpack.synthetics.monitorConfig.maxRedirects.error', { + defaultMessage: 'Max redirects is invalid.', + }), + }, + [ConfigKey.WAIT]: { + fieldKey: ConfigKey.WAIT, + component: EuiFieldNumber, + label: i18n.translate('xpack.synthetics.monitorConfig.wait.label', { + defaultMessage: 'Wait', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.wait.helpText', { + defaultMessage: + 'The duration to wait before emitting another ICMP Echo Request if no response is received.', + }), + props: () => ({ + min: 1, + step: 1, + }), + validation: () => ({ + min: 1, + pattern: WHOLE_NUMBERS_ONLY, + }), + error: i18n.translate('xpack.synthetics.monitorConfig.wait.error', { + defaultMessage: 'Wait duration is invalid.', + }), + }, + [ConfigKey.USERNAME]: { + fieldKey: ConfigKey.USERNAME, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.username.label', { + defaultMessage: 'Username', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.username.helpText', { + defaultMessage: 'Username for authenticating with the server.', + }), + }, + [ConfigKey.PASSWORD]: { + fieldKey: ConfigKey.PASSWORD, + component: EuiFieldPassword, + label: i18n.translate('xpack.synthetics.monitorConfig.password.label', { + defaultMessage: 'Password', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.password.helpText', { + defaultMessage: 'Password for authenticating with the server.', + }), + }, + [ConfigKey.PROXY_URL]: { + fieldKey: ConfigKey.PROXY_URL, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.proxyUrl.label', { + defaultMessage: 'Proxy URL', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.proxyUrl.helpText', { + defaultMessage: 'HTTP proxy URL', + }), + }, + [ConfigKey.REQUEST_METHOD_CHECK]: { + fieldKey: ConfigKey.REQUEST_METHOD_CHECK, + component: EuiSelect, + label: i18n.translate('xpack.synthetics.monitorConfig.requestMethod.label', { + defaultMessage: 'Request method', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.requestMethod.helpText', { + defaultMessage: 'The HTTP method to use.', + }), + props: () => ({ + options: Object.keys(HTTPMethod).map((method) => ({ + value: method, + text: method, + })), + }), + }, + [ConfigKey.REQUEST_HEADERS_CHECK]: { + fieldKey: ConfigKey.REQUEST_HEADERS_CHECK, + component: HeaderField, + label: i18n.translate('xpack.synthetics.monitorConfig.requestHeaders.label', { + defaultMessage: 'Request headers', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.requestHeaders.helpText', { + defaultMessage: + 'A dictionary of additional HTTP headers to send. By default the client will set the User-Agent header to identify itself.', + }), + controlled: true, + validation: () => ({ + validate: (headers) => !validateHeaders(headers), + }), + error: i18n.translate('xpack.synthetics.monitorConfig.requestHeaders.error', { + defaultMessage: 'Header key must be a valid HTTP token.', + }), + }, + [ConfigKey.REQUEST_BODY_CHECK]: { + fieldKey: ConfigKey.REQUEST_BODY_CHECK, + component: RequestBodyField, + label: i18n.translate('xpack.synthetics.monitorConfig.requestBody.label', { + defaultMessage: 'Request body', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.requestBody.helpText', { + defaultMessage: 'Request body content.', + }), + controlled: true, + }, + [ConfigKey.RESPONSE_HEADERS_INDEX]: { + fieldKey: ConfigKey.RESPONSE_HEADERS_INDEX, + component: EuiCheckbox, + helpText: ( + <> + + http.response.body.headers + + ), + props: () => ({ + label: i18n.translate('xpack.synthetics.monitorConfig.indexResponseHeaders.label', { + defaultMessage: 'Index response headers', + }), + id: 'syntheticsMonitorConfigResponseHeadersIndex', // checkbox needs an id or it won't work + }), + controlled: true, + }, + [ConfigKey.RESPONSE_BODY_INDEX]: { + fieldKey: ConfigKey.RESPONSE_BODY_INDEX, + component: ResponseBodyIndexField, + helpText: ( + <> + + http.response.body.contents + + ), + props: () => ({ + label: i18n.translate('xpack.synthetics.monitorConfig.indexResponseBody.label', { + defaultMessage: 'Index response body', + }), + }), + controlled: true, + }, + [ConfigKey.RESPONSE_STATUS_CHECK]: { + fieldKey: ConfigKey.RESPONSE_STATUS_CHECK, + component: ComboBox, + label: i18n.translate('xpack.synthetics.monitorConfig.responseStatusCheck.label', { + defaultMessage: 'Check response status equals', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.responseStatusCheck.helpText', { + defaultMessage: + 'A list of expected status codes. Press enter to add a new code. 4xx and 5xx codes are considered down by default. Other codes are considered up.', + }), + controlled: true, + props: ({ field }) => ({ + selectedOptions: field?.value, + }), + validation: () => ({ + validate: (value) => { + const validateFn = validate[DataStream.HTTP][ConfigKey.RESPONSE_STATUS_CHECK]; + if (validateFn) { + return !validateFn({ + [ConfigKey.RESPONSE_STATUS_CHECK]: value, + }); + } + }, + }), + error: i18n.translate('xpack.synthetics.monitorConfig.responseStatusCheck.error', { + defaultMessage: 'Status code must contain digits only.', + }), + }, + [ConfigKey.RESPONSE_HEADERS_CHECK]: { + fieldKey: ConfigKey.RESPONSE_HEADERS_CHECK, + component: HeaderField, + label: i18n.translate('xpack.synthetics.monitorConfig.responseHeadersCheck.label', { + defaultMessage: 'Check response headers contain', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.responseHeadersCheck.helpText', { + defaultMessage: 'A list of expected response headers.', + }), + controlled: true, + validation: () => ({ + validate: (headers) => !validateHeaders(headers), + }), + error: i18n.translate('xpack.synthetics.monitorConfig.responseHeadersCheck.error', { + defaultMessage: 'Header key must be a valid HTTP token.', + }), + }, + [ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]: { + fieldKey: ConfigKey.RESPONSE_BODY_CHECK_POSITIVE, + component: ComboBox, + label: i18n.translate('xpack.synthetics.monitorConfig.responseBodyCheck.label', { + defaultMessage: 'Check response body contains', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.responseBodyCheck.helpText', { + defaultMessage: + 'A list of regular expressions to match the body output. Press enter to add a new expression. Only a single expression needs to match.', + }), + controlled: true, + props: ({ field }) => ({ + selectedOptions: field?.value, + }), + }, + [ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]: { + fieldKey: ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE, + component: ComboBox, + label: i18n.translate('xpack.synthetics.monitorConfig.responseBodyCheckNegative.label', { + defaultMessage: 'Check response body does not contain', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.responseBodyCheckNegative.helpText', { + defaultMessage: + 'A list of regular expressions to match the the body output negatively. Press enter to add a new expression. Return match failed if single expression matches.', + }), + controlled: true, + props: ({ field }) => ({ + selectedOptions: field?.value, + }), + }, + [ConfigKey.RESPONSE_RECEIVE_CHECK]: { + fieldKey: ConfigKey.RESPONSE_RECEIVE_CHECK, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.responseReceiveCheck.label', { + defaultMessage: 'Check response contains', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.responseReceiveCheck.helpText', { + defaultMessage: 'The expected remote host response.', + }), + }, + [`${ConfigKey.PROXY_URL}__tcp`]: { + fieldKey: ConfigKey.PROXY_URL, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.proxyURLTCP.label', { + defaultMessage: 'Proxy URL', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.proxyURLTCP.helpText', { + defaultMessage: + 'The URL of the SOCKS5 proxy to use when connecting to the server. The value must be a URL with a scheme of socks5://.', + }), + }, + [ConfigKey.REQUEST_SEND_CHECK]: { + fieldKey: ConfigKey.REQUEST_SEND_CHECK, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.requestSendCheck.label', { + defaultMessage: 'Request payload', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.requestSendCheck.helpText', { + defaultMessage: 'A payload string to send to the remote host.', + }), + }, + [ConfigKey.SOURCE_INLINE]: { + fieldKey: 'source.inline', + required: true, + component: SourceField, + ariaLabel: i18n.translate('xpack.synthetics.monitorConfig.monitorScript.label', { + defaultMessage: 'Monitor script', + }), + controlled: true, + props: ({ isEdit }) => ({ + isEditFlow: isEdit, + }), + validation: () => ({ + validate: (value) => Boolean(value.script), + }), + error: i18n.translate('xpack.synthetics.monitorConfig.monitorScript.error', { + defaultMessage: 'Monitor script is required', + }), + }, + isTLSEnabled: { + fieldKey: 'isTLSEnabled', + component: EuiSwitch, + controlled: true, + props: ({ setValue }) => { + return { + id: 'syntheticsMontiorConfigIsTLSEnabledSwitch', + label: i18n.translate('xpack.synthetics.monitorConfig.customTLS.label', { + defaultMessage: 'Use custom TLS configuration', + }), + onChange: (event: React.ChangeEvent) => { + setValue('isTLSEnabled', event.target.checked); + }, + }; + }, + }, + [ConfigKey.TLS_VERIFICATION_MODE]: { + fieldKey: ConfigKey.TLS_VERIFICATION_MODE, + component: EuiSelect, + label: i18n.translate('xpack.synthetics.monitorConfig.verificationMode.label', { + defaultMessage: 'Verification mode', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.verificationMode.helpText', { + defaultMessage: + 'Verifies that the provided certificate is signed by a trusted authority (CA) and also verifies that the server’s hostname (or IP address) matches the names identified within the certificate. If the Subject Alternative Name is empty, it returns an error.', + }), + showWhen: ['isTLSEnabled', true], + props: () => ({ + options: Object.values(VerificationMode).map((method) => ({ + value: method, + text: method.toUpperCase(), + })), + }), + }, + [ConfigKey.TLS_VERSION]: { + fieldKey: ConfigKey.TLS_VERSION, + component: EuiComboBox as React.ComponentType>, + label: i18n.translate('xpack.synthetics.monitorConfig.tlsVersion.label', { + defaultMessage: 'Supported TLS protocols', + }), + controlled: true, + showWhen: ['isTLSEnabled', true], + props: ({ + field, + setValue, + }: { + field?: ControllerRenderProps; + setValue: UseFormReturn['setValue']; + }) => { + return { + options: Object.values(TLSVersion).map((version) => ({ + label: version, + })), + selectedOptions: Object.values(field?.value).map((version) => ({ + label: version, + })), + onChange: (updatedValues: Array>) => { + setValue( + ConfigKey.TLS_VERSION, + updatedValues.map((option) => option.label as TLSVersion) + ); + }, + }; + }, + }, + [ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: { + fieldKey: ConfigKey.TLS_CERTIFICATE_AUTHORITIES, + component: EuiTextArea, + label: i18n.translate('xpack.synthetics.monitorConfig.certificateAuthorities.label', { + defaultMessage: 'Certificate authorities', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.certificateAuthorities.helpText', { + defaultMessage: 'PEM-formatted custom certificate authorities.', + }), + showWhen: ['isTLSEnabled', true], + }, + [ConfigKey.TLS_CERTIFICATE]: { + fieldKey: ConfigKey.TLS_CERTIFICATE, + component: EuiTextArea, + label: i18n.translate('xpack.synthetics.monitorConfig.clientCertificate.label', { + defaultMessage: 'Client certificate', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.clientCertificate.helpText', { + defaultMessage: 'PEM-formatted certificate for TLS client authentication.', + }), + showWhen: ['isTLSEnabled', true], + }, + [ConfigKey.TLS_KEY]: { + fieldKey: ConfigKey.TLS_KEY, + component: EuiTextArea, + label: i18n.translate('xpack.synthetics.monitorConfig.clientKey.label', { + defaultMessage: 'Client key', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.clientKey.helpText', { + defaultMessage: 'PEM-formatted certificate key for TLS client authentication.', + }), + showWhen: ['isTLSEnabled', true], + }, + [ConfigKey.TLS_KEY_PASSPHRASE]: { + fieldKey: ConfigKey.TLS_KEY_PASSPHRASE, + component: EuiFieldPassword, + label: i18n.translate('xpack.synthetics.monitorConfig.clientKeyPassphrase.label', { + defaultMessage: 'Client key passphrase', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.clientKeyPassphrase.helpText', { + defaultMessage: 'Certificate key passphrase for TLS client authentication.', + }), + showWhen: ['isTLSEnabled', true], + }, + [ConfigKey.SCREENSHOTS]: { + fieldKey: ConfigKey.SCREENSHOTS, + component: EuiButtonGroup, + label: i18n.translate('xpack.synthetics.monitorConfig.screenshotOptions.label', { + defaultMessage: 'Screenshot options', + }), + helpText: i18n.translate('xpack.synthetics.monitorConfig.screenshotOptions.helpText', { + defaultMessage: 'Set this option to manage the screenshots captured by the synthetics agent.', + }), + controlled: true, + props: ({ + field, + setValue, + }: { + field?: ControllerRenderProps; + setValue: UseFormReturn['setValue']; + }) => ({ + type: 'single', + idSelected: field?.value, + onChange: (option: ScreenshotOption) => setValue(ConfigKey.SCREENSHOTS, option), + options: Object.values(ScreenshotOption).map((option) => ({ + id: option, + label: option.replace(/-/g, ' '), + })), + css: { + 'text-transform': 'capitalize', + }, + }), + }, + [ConfigKey.TEXT_ASSERTION]: { + fieldKey: ConfigKey.TEXT_ASSERTION, + component: EuiFieldText, + label: i18n.translate('xpack.synthetics.monitorConfig.textAssertion.label', { + defaultMessage: 'Text assertion', + }), + required: true, + helpText: i18n.translate('xpack.synthetics.monitorConfig.textAssertion.helpText', { + defaultMessage: 'Consider the page loaded when the specified text is rendered.', + }), + validation: () => ({ + required: true, + }), + }, + [ConfigKey.THROTTLING_CONFIG]: { + fieldKey: ConfigKey.THROTTLING_CONFIG, + component: EuiSuperSelect, + label: i18n.translate('xpack.synthetics.monitorConfig.throttling.label', { + defaultMessage: 'Connection profile', + }), + required: true, + controlled: true, + helpText: i18n.translate('xpack.synthetics.monitorConfig.throttling.helpText', { + defaultMessage: + 'Simulate network throttling (download, upload, latency). More options will be added in a future version.', + }), + props: () => ({ + options: [ + { + value: DEFAULT_BROWSER_ADVANCED_FIELDS[ConfigKey.THROTTLING_CONFIG], + inputDisplay: ( + + + + {i18n.translate('xpack.synthetics.monitorConfig.throttling.options.default', { + defaultMessage: 'Default', + })} + + + + + {'(5 Mbps, 3 Mbps, 20 ms)'} + + + + ), + }, + ], + disabled: true, // currently disabled through 1.0 until we define connection profiles + }), + validation: () => ({ + required: true, + }), + }, +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx new file mode 100644 index 000000000000000..1c49665af081eb5 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ConfigKey, FormMonitorType, FieldMeta } from '../types'; + +import { FIELD } from './field_config'; + +const DEFAULT_DATA_OPTIONS = { + title: i18n.translate('xpack.synthetics.monitorConfig.section.dataOptions.title', { + defaultMessage: 'Data options', + }), + description: i18n.translate('xpack.synthetics.monitorConfig.section.dataOptions.description', { + defaultMessage: 'Configure data options to add context to the data coming from your monitors.', + }), + components: [ + FIELD[ConfigKey.TAGS], + FIELD[ConfigKey.APM_SERVICE_NAME], + FIELD[ConfigKey.NAMESPACE], + ], +}; + +const HTTP_ADVANCED = { + requestConfig: { + title: i18n.translate('xpack.synthetics.monitorConfig.section.requestConfiguration.title', { + defaultMessage: 'Request configuration', + }), + description: i18n.translate( + 'xpack.synthetics.monitorConfig.section.requestConfiguration.description', + { + defaultMessage: + 'Configure an optional request to send to the remote host including method, body, and headers.', + } + ), + components: [ + FIELD[ConfigKey.USERNAME], + FIELD[ConfigKey.PASSWORD], + FIELD[ConfigKey.PROXY_URL], + FIELD[ConfigKey.REQUEST_METHOD_CHECK], + FIELD[ConfigKey.REQUEST_HEADERS_CHECK], + FIELD[ConfigKey.REQUEST_BODY_CHECK], + ], + }, + responseConfig: { + title: i18n.translate('xpack.synthetics.monitorConfig.section.responseConfiguration.title', { + defaultMessage: 'Response configuration', + }), + description: i18n.translate( + 'xpack.synthetics.monitorConfig.section.responseConfiguration.description', + { + defaultMessage: 'Control the indexing of the HTTP response contents.', + } + ), + components: [FIELD[ConfigKey.RESPONSE_HEADERS_INDEX], FIELD[ConfigKey.RESPONSE_BODY_INDEX]], + }, + responseChecks: { + title: i18n.translate('xpack.synthetics.monitorConfig.section.responseChecks.title', { + defaultMessage: 'Response checks', + }), + description: i18n.translate( + 'xpack.synthetics.monitorConfig.section.responseChecks.description', + { + defaultMessage: 'Configure the expected HTTP response.', + } + ), + components: [ + FIELD[ConfigKey.RESPONSE_STATUS_CHECK], + FIELD[ConfigKey.RESPONSE_HEADERS_CHECK], + FIELD[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE], + FIELD[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE], + ], + }, +}; + +export const TCP_ADVANCED = { + requestConfig: { + title: i18n.translate('xpack.synthetics.monitorConfig.section.requestConfigTCP.title', { + defaultMessage: 'Request configuration', + }), + description: i18n.translate( + 'xpack.synthetics.monitorConfig.section.requestConfigTCP.description', + { + defaultMessage: 'Configure the payload sent to the remote host.', + } + ), + components: [FIELD[`${ConfigKey.PROXY_URL}__tcp`], FIELD[ConfigKey.REQUEST_SEND_CHECK]], + }, + responseChecks: { + title: i18n.translate('xpack.synthetics.monitorConfig.section.responseChecksTCP.title', { + defaultMessage: 'Response checks', + }), + description: i18n.translate( + 'xpack.synthetics.monitorConfig.section.responseChecksTCP.description', + { + defaultMessage: 'Configure the expected response from the remote host.', + } + ), + components: [FIELD[ConfigKey.RESPONSE_RECEIVE_CHECK]], + }, +}; + +interface AdvancedFieldGroup { + title: string; + description: string; + components: FieldMeta[]; +} + +type FieldConfig = Record< + FormMonitorType, + { + step1: FieldMeta[]; + step2: FieldMeta[]; + step3?: FieldMeta[]; + scriptEdit?: FieldMeta[]; + advanced?: AdvancedFieldGroup[]; + } +>; + +const TLS_OPTIONS = { + title: i18n.translate('xpack.synthetics.monitorConfig.section.tlsOptions.title', { + defaultMessage: 'TLS options', + }), + description: i18n.translate('xpack.synthetics.monitorConfig.section.tlsOptions.description', { + defaultMessage: + 'Configure TLS options, including verification mode, certificate authorities, and client certificates.', + }), + components: [ + FIELD.isTLSEnabled, + FIELD[ConfigKey.TLS_VERIFICATION_MODE], + FIELD[ConfigKey.TLS_VERSION], + FIELD[ConfigKey.TLS_CERTIFICATE_AUTHORITIES], + FIELD[ConfigKey.TLS_CERTIFICATE], + FIELD[ConfigKey.TLS_KEY], + FIELD[ConfigKey.TLS_KEY_PASSPHRASE], + ], +}; + +export const FORM_CONFIG: FieldConfig = { + [FormMonitorType.HTTP]: { + step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]], + step2: [ + FIELD[`${ConfigKey.URLS}__http`], + FIELD[ConfigKey.NAME], + FIELD[ConfigKey.LOCATIONS], + FIELD[ConfigKey.SCHEDULE], + FIELD[ConfigKey.MAX_REDIRECTS], + FIELD[ConfigKey.TIMEOUT], + ], + advanced: [ + DEFAULT_DATA_OPTIONS, + HTTP_ADVANCED.requestConfig, + HTTP_ADVANCED.responseConfig, + HTTP_ADVANCED.responseChecks, + TLS_OPTIONS, + ], + }, + [FormMonitorType.TCP]: { + step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]], + step2: [ + FIELD[`${ConfigKey.HOSTS}__tcp`], + FIELD[ConfigKey.NAME], + FIELD[ConfigKey.LOCATIONS], + FIELD[ConfigKey.SCHEDULE], + FIELD[ConfigKey.TIMEOUT], + ], + advanced: [ + DEFAULT_DATA_OPTIONS, + TCP_ADVANCED.requestConfig, + TCP_ADVANCED.responseChecks, + TLS_OPTIONS, + ], + }, + [FormMonitorType.MULTISTEP]: { + step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]], + step2: [ + FIELD[ConfigKey.NAME], + FIELD[ConfigKey.LOCATIONS], + FIELD[ConfigKey.SCHEDULE], + FIELD[ConfigKey.THROTTLING_CONFIG], + ], + step3: [FIELD[ConfigKey.SOURCE_INLINE]], + scriptEdit: [FIELD[ConfigKey.SOURCE_INLINE]], + advanced: [ + { + ...DEFAULT_DATA_OPTIONS, + components: [ + FIELD[ConfigKey.TAGS], + FIELD[ConfigKey.APM_SERVICE_NAME], + FIELD[ConfigKey.SCREENSHOTS], + FIELD[ConfigKey.NAMESPACE], + ], + }, + ], + }, + [FormMonitorType.SINGLE]: { + step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]], + step2: [ + FIELD[`${ConfigKey.URLS}__single`], + FIELD[ConfigKey.NAME], + FIELD[ConfigKey.TEXT_ASSERTION], + FIELD[ConfigKey.LOCATIONS], + FIELD[ConfigKey.SCHEDULE], + FIELD[ConfigKey.THROTTLING_CONFIG], + ], + advanced: [ + { + ...DEFAULT_DATA_OPTIONS, + components: [ + FIELD[ConfigKey.TAGS], + FIELD[ConfigKey.APM_SERVICE_NAME], + FIELD[ConfigKey.SCREENSHOTS], + FIELD[ConfigKey.NAMESPACE], + ], + }, + ], + }, + [FormMonitorType.ICMP]: { + step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]], + step2: [ + FIELD[`${ConfigKey.HOSTS}__icmp`], + FIELD[ConfigKey.NAME], + FIELD[ConfigKey.LOCATIONS], + FIELD[ConfigKey.SCHEDULE], + FIELD[ConfigKey.WAIT], + FIELD[ConfigKey.TIMEOUT], + ], + advanced: [DEFAULT_DATA_OPTIONS], + }, +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx new file mode 100644 index 000000000000000..80b34b47fdc9f10 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx @@ -0,0 +1,347 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { format } from './formatter'; + +describe('format', () => { + const formValues = { + type: 'http', + form_monitor_type: 'http', + enabled: true, + schedule: { + number: '3', + unit: 'm', + }, + 'service.name': '', + tags: [], + timeout: '16', + name: 'Sample name', + locations: [ + { + id: 'us_central', + isServiceManaged: true, + }, + ], + namespace: 'default', + origin: 'ui', + __ui: { + is_tls_enabled: false, + }, + urls: 'sample url', + max_redirects: '0', + password: '', + proxy_url: '', + 'check.response.body.negative': [], + 'check.response.body.positive': [], + 'response.include_body': 'on_error', + 'check.response.headers': {}, + 'response.include_headers': true, + 'check.response.status': [], + 'check.request.body': { + value: '', + type: 'text', + }, + 'check.request.headers': {}, + 'check.request.method': 'GET', + username: '', + 'ssl.certificate_authorities': '', + 'ssl.certificate': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.verification_mode': 'full', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + isTLSEnabled: false, + service: { + name: '', + }, + check: { + request: { + method: 'GET', + headers: {}, + body: { + type: 'text', + value: '', + }, + }, + response: { + status: [], + headers: {}, + body: { + positive: [], + negative: [], + }, + }, + }, + response: { + include_headers: true, + include_body: 'on_error', + }, + }; + + it('correctly formats form fields to monitor type', () => { + expect(format(formValues)).toEqual({ + __ui: { + is_tls_enabled: false, + }, + config_id: '', + 'check.request.body': { + type: 'text', + value: '', + }, + 'check.request.headers': {}, + 'check.request.method': 'GET', + 'check.response.body.negative': [], + 'check.response.body.positive': [], + 'check.response.headers': {}, + 'check.response.status': [], + enabled: true, + form_monitor_type: 'http', + locations: [ + { + id: 'us_central', + isServiceManaged: true, + }, + ], + max_redirects: '0', + name: 'Sample name', + namespace: 'default', + origin: 'ui', + password: '', + proxy_url: '', + 'response.include_body': 'on_error', + 'response.include_headers': true, + schedule: { + number: '3', + unit: 'm', + }, + 'service.name': '', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + tags: [], + timeout: '16', + type: 'http', + urls: 'sample url', + username: '', + }); + }); + + it.each([ + ['recorder', true, 'testScriptRecorder', 'fileName'], + ['inline', false, 'testScript', ''], + ])( + 'correctly formats form fields to monitor type', + (scriptType, isGeneratedScript, script, fileName) => { + const browserFormFields = { + type: 'browser', + form_monitor_type: 'multistep', + config_id: '', + enabled: true, + schedule: { + unit: 'm', + number: '10', + }, + 'service.name': '', + tags: [], + timeout: '16', + name: 'Browser monitor', + locations: [ + { + id: 'us_central', + isServiceManaged: true, + }, + ], + namespace: 'default', + origin: 'ui', + journey_id: '', + project_id: '', + playwright_options: '', + __ui: { + script_source: { + is_generated_script: false, + file_name: '', + }, + is_zip_url_tls_enabled: false, + }, + params: '', + 'source.inline.script': '', + 'source.project.content': '', + 'source.zip_url.url': '', + 'source.zip_url.username': '', + 'source.zip_url.password': '', + 'source.zip_url.folder': '', + 'source.zip_url.proxy_url': '', + playwright_text_assertion: '', + urls: '', + screenshots: 'on', + synthetics_args: [], + 'filter_journeys.match': '', + 'filter_journeys.tags': [], + ignore_https_errors: false, + 'throttling.is_enabled': true, + 'throttling.download_speed': '5', + 'throttling.upload_speed': '3', + 'throttling.latency': '20', + 'throttling.config': '5d/3u/20l', + 'ssl.certificate_authorities': '', + 'ssl.certificate': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.verification_mode': 'full', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'source.inline': { + type: 'recorder', + script: '', + fileName: '', + }, + throttling: { + config: '5d/3u/20l', + }, + source: { + inline: { + type: scriptType, + script, + fileName, + }, + }, + service: { + name: '', + }, + }; + expect(format(browserFormFields)).toEqual({ + __ui: { + script_source: { + file_name: fileName, + is_generated_script: isGeneratedScript, + }, + }, + config_id: '', + enabled: true, + 'filter_journeys.match': '', + 'filter_journeys.tags': [], + form_monitor_type: 'multistep', + ignore_https_errors: false, + journey_id: '', + locations: [ + { + id: 'us_central', + isServiceManaged: true, + }, + ], + name: 'Browser monitor', + namespace: 'default', + origin: 'ui', + params: '', + playwright_options: '', + playwright_text_assertion: '', + project_id: '', + schedule: { + number: '10', + unit: 'm', + }, + screenshots: 'on', + 'service.name': '', + 'source.inline.script': script, + 'source.project.content': '', + 'source.zip_url.folder': '', + 'source.zip_url.password': '', + 'source.zip_url.proxy_url': '', + 'source.zip_url.ssl.certificate': undefined, + 'source.zip_url.ssl.certificate_authorities': undefined, + 'source.zip_url.ssl.key': undefined, + 'source.zip_url.ssl.key_passphrase': undefined, + 'source.zip_url.ssl.supported_protocols': undefined, + 'source.zip_url.ssl.verification_mode': undefined, + 'source.zip_url.url': '', + 'source.zip_url.username': '', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + synthetics_args: [], + tags: [], + 'throttling.config': '5d/3u/20l', + 'throttling.download_speed': '5', + 'throttling.is_enabled': true, + 'throttling.latency': '20', + 'throttling.upload_speed': '3', + timeout: '16', + type: 'browser', + 'url.port': null, + urls: '', + }); + } + ); + + it.each([ + ['testCA', true], + ['', false], + ])('correctly formats form fields to monitor type', (certificateAuthorities, isTLSEnabled) => { + expect( + format({ + ...formValues, + ssl: { + // @ts-ignore next + ...formValues.ssl, + certificate_authorities: certificateAuthorities, + }, + }) + ).toEqual({ + __ui: { + is_tls_enabled: isTLSEnabled, + }, + config_id: '', + 'check.request.body': { + type: 'text', + value: '', + }, + 'check.request.headers': {}, + 'check.request.method': 'GET', + 'check.response.body.negative': [], + 'check.response.body.positive': [], + 'check.response.headers': {}, + 'check.response.status': [], + enabled: true, + form_monitor_type: 'http', + locations: [ + { + id: 'us_central', + isServiceManaged: true, + }, + ], + max_redirects: '0', + name: 'Sample name', + namespace: 'default', + origin: 'ui', + password: '', + proxy_url: '', + 'response.include_body': 'on_error', + 'response.include_headers': true, + schedule: { + number: '3', + unit: 'm', + }, + 'service.name': '', + 'ssl.certificate': '', + 'ssl.certificate_authorities': certificateAuthorities, + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + tags: [], + timeout: '16', + type: 'http', + urls: 'sample url', + username: '', + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts new file mode 100644 index 000000000000000..8da53f6cbb2806a --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { get, isEqual } from 'lodash'; +import { ConfigKey, DataStream, FormMonitorType, MonitorFields } from '../types'; +import { DEFAULT_FIELDS, DEFAULT_TLS_FIELDS } from '../constants'; + +export const formatter = (fields: Record) => { + const monitorType = fields[ConfigKey.MONITOR_TYPE] as DataStream; + const monitorFields: Record = {}; + const defaults = DEFAULT_FIELDS[monitorType] as MonitorFields; + Object.keys(defaults).map((key) => { + /* split key names on dot to handle dot notation fields, + * which are changed to nested fields by react-hook-form */ + monitorFields[key] = get(fields, key.split('.')) || defaults[key as ConfigKey]; + }); + return monitorFields as MonitorFields; +}; + +export const format = (fields: Record) => { + const formattedFields = formatter(fields) as MonitorFields; + const formattedMap = { + [FormMonitorType.SINGLE]: { + ...formattedFields, + [ConfigKey.SOURCE_INLINE]: `step('Go to ${formattedFields[ConfigKey.URLS]}', async () => { + await page.goto('${formattedFields[ConfigKey.URLS]}'); + expect(await page.isVisible('text=${ + formattedFields[ConfigKey.TEXT_ASSERTION] + }')).toBeTruthy(); + });`, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.SINGLE, + }, + [FormMonitorType.MULTISTEP]: { + ...formattedFields, + [ConfigKey.METADATA]: { + script_source: { + is_generated_script: get(fields, 'source.inline.type') === 'recorder' ? true : false, + file_name: get(fields, 'source.inline.fileName'), + }, + }, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP, + }, + [FormMonitorType.HTTP]: { + ...formattedFields, + [ConfigKey.METADATA]: { + is_tls_enabled: isCustomTLSEnabled(formattedFields), + }, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP, + }, + [FormMonitorType.TCP]: { + ...formattedFields, + [ConfigKey.METADATA]: { + is_tls_enabled: isCustomTLSEnabled(formattedFields), + }, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP, + }, + [FormMonitorType.ICMP]: { + ...formattedFields, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP, + }, + }; + return formattedMap[fields[ConfigKey.FORM_MONITOR_TYPE] as FormMonitorType]; +}; + +const isCustomTLSEnabled = (fields: MonitorFields) => { + const tlsFields: Record = {}; + Object.keys(DEFAULT_TLS_FIELDS).map((key) => { + tlsFields[key] = fields[key as keyof MonitorFields]; + }); + return !isEqual(tlsFields, DEFAULT_TLS_FIELDS); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx new file mode 100644 index 000000000000000..18e1b78a3c59bb4 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiForm, EuiSpacer } from '@elastic/eui'; +import { FormProvider } from 'react-hook-form'; +import { useFormWrapped } from '../hooks/use_form_wrapped'; +import { FormMonitorType, SyntheticsMonitor } from '../types'; +import { getDefaultFormFields, formatDefaultFormValues } from './defaults'; +import { ActionBar } from './submit'; +import { Disclaimer } from './disclaimer'; + +export const MonitorForm: React.FC<{ defaultValues?: SyntheticsMonitor; space?: string }> = ({ + children, + defaultValues, + space, +}) => { + const methods = useFormWrapped({ + mode: 'onSubmit', + reValidateMode: 'onChange', + defaultValues: + formatDefaultFormValues(defaultValues as SyntheticsMonitor) || + getDefaultFormFields(space)[FormMonitorType.MULTISTEP], + shouldFocusError: true, + }); + + /* React hook form doesn't seem to register a field + * as dirty until validation unless dirtyFields is subscribed to */ + const { + formState: { isSubmitted, errors, dirtyFields: _ }, + } = methods; + + return ( + + + {children} + + + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx new file mode 100644 index 000000000000000..46dba61339a7bd4 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { Redirect, useParams, useHistory, useRouteMatch } from 'react-router-dom'; +import { EuiButton, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useFormContext } from 'react-hook-form'; +import { useFetcher, FETCH_STATUS } from '@kbn/observability-plugin/public'; +import { SyntheticsMonitor } from '../types'; +import { format } from './formatter'; +import { createMonitorAPI, updateMonitorAPI } from '../../../state/monitor_management/api'; +import { kibanaService } from '../../../../../utils/kibana_service'; + +import { MONITORS_ROUTE, MONITOR_EDIT_ROUTE } from '../../../../../../common/constants'; + +export const ActionBar = () => { + const { monitorId } = useParams<{ monitorId: string }>(); + const history = useHistory(); + const editRouteMatch = useRouteMatch({ path: MONITOR_EDIT_ROUTE }); + const isEdit = editRouteMatch?.isExact; + const { handleSubmit } = useFormContext(); + + const [monitorData, setMonitorData] = useState(undefined); + + const { data, status } = useFetcher(() => { + if (!monitorData) { + return null; + } + if (isEdit) { + return updateMonitorAPI({ + id: monitorId, + monitor: monitorData, + }); + } else { + return createMonitorAPI({ + monitor: monitorData, + }); + } + }, [monitorData]); + + const loading = status === FETCH_STATUS.LOADING; + + useEffect(() => { + if (status === FETCH_STATUS.FAILURE) { + kibanaService.toasts.addDanger({ + title: MONITOR_FAILURE_LABEL, + toastLifeTimeMs: 3000, + }); + } else if (status === FETCH_STATUS.SUCCESS && !loading) { + kibanaService.toasts.addSuccess({ + title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, + toastLifeTimeMs: 3000, + }); + } + }, [data, status, monitorId, loading]); + + const formSubmitter = (formData: Record) => { + setMonitorData(format(formData) as SyntheticsMonitor); + }; + + return status === FETCH_STATUS.SUCCESS ? ( + + ) : ( + + + {CANCEL_LABEL} + + + + + + {isEdit ? UPDATE_MONITOR_LABEL : CREATE_MONITOR_LABEL} + + + + + + ); +}; + +const CANCEL_LABEL = i18n.translate('xpack.synthetics.monitorManagement.discardLabel', { + defaultMessage: 'Cancel', +}); + +const CREATE_MONITOR_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.addEdit.createMonitorLabel', + { + defaultMessage: 'Create monitor', + } +); + +const UPDATE_MONITOR_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.updateMonitorLabel', + { + defaultMessage: 'Update monitor', + } +); + +const MONITOR_SUCCESS_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage', + { + defaultMessage: 'Monitor added successfully.', + } +); + +const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorEditedSuccessMessage', + { + defaultMessage: 'Monitor updated successfully.', + } +); + +const MONITOR_FAILURE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorFailureMessage', + { + defaultMessage: 'Monitor was unable to be saved. Please try again later.', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.test.ts new file mode 100644 index 000000000000000..fa91bd457671dc6 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ConfigKey, + DataStream, + HTTPFields, + BrowserFields, + MonitorFields, + ScheduleUnit, +} from '../types'; +import { validate } from './validation'; + +describe('[Monitor Management] validation', () => { + const commonPropsValid: Partial = { + [ConfigKey.SCHEDULE]: { number: '5', unit: ScheduleUnit.MINUTES }, + [ConfigKey.TIMEOUT]: '3m', + }; + + describe('HTTP', () => { + const httpPropsValid: Partial = { + ...commonPropsValid, + [ConfigKey.RESPONSE_STATUS_CHECK]: ['200', '204'], + [ConfigKey.RESPONSE_HEADERS_CHECK]: { 'Content-Type': 'application/json' }, + [ConfigKey.REQUEST_HEADERS_CHECK]: { 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8' }, + [ConfigKey.MAX_REDIRECTS]: '3', + [ConfigKey.URLS]: 'https:// example-url.com', + }; + + it('should return false for all valid props', () => { + const validators = validate[DataStream.HTTP]; + const keysToValidate = [ + ConfigKey.SCHEDULE, + ConfigKey.TIMEOUT, + ConfigKey.RESPONSE_STATUS_CHECK, + ConfigKey.RESPONSE_HEADERS_CHECK, + ConfigKey.REQUEST_HEADERS_CHECK, + ConfigKey.MAX_REDIRECTS, + ConfigKey.URLS, + ]; + const validatorFns = keysToValidate.map((key) => validators[key]); + const result = validatorFns.map((fn) => fn?.(httpPropsValid) ?? true); + + expect(result).not.toEqual(expect.arrayContaining([true])); + }); + }); + + describe.each([ + [ConfigKey.SOURCE_INLINE, 'step(() => {});'], + [ConfigKey.SOURCE_ZIP_URL, 'https://test.zip'], + ])('Browser', (configKey, value) => { + const browserProps: Partial = { + ...commonPropsValid, + [ConfigKey.MONITOR_TYPE]: DataStream.BROWSER, + [ConfigKey.TIMEOUT]: null, + [ConfigKey.URLS]: null, + [ConfigKey.PORT]: null, + [configKey]: value, + }; + + it('should return false for all valid props', () => { + const validators = validate[DataStream.BROWSER]; + const keysToValidate = [ConfigKey.SCHEDULE, ConfigKey.TIMEOUT, configKey]; + const validatorFns = keysToValidate.map((key) => validators[key]); + const result = validatorFns.map((fn) => fn?.(browserProps as Partial) ?? true); + + expect(result).not.toEqual(expect.arrayContaining([true])); + }); + }); + + // TODO: Add test for other monitor types if needed +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx new file mode 100644 index 000000000000000..de3ce4bc327fdb6 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + ConfigKey, + DataStream, + ScheduleUnit, + MonitorFields, + Validator, + Validation, +} from '../types'; + +export const DIGITS_ONLY = /^[0-9]*$/g; +export const INCLUDES_VALID_PORT = /[^\:]+:[0-9]{1,5}$/g; +export const WHOLE_NUMBERS_ONLY = /^[0-9]+(.?[0]+)?$/; +export const FLOATS_ONLY = /^[0-9]+(.?[0-9]+)?$/; + +type ValidationLibrary = Record; + +// returns true if invalid +export function validateHeaders(headers: T): boolean { + return Object.keys(headers).some((key) => { + if (key) { + const whiteSpaceRegEx = /[\s]/g; + return whiteSpaceRegEx.test(key); + } else { + return false; + } + }); +} + +// returns true if invalid +export const validateTimeout = ({ + scheduleNumber, + scheduleUnit, + timeout, +}: { + scheduleNumber: string; + scheduleUnit: ScheduleUnit; + timeout: string; +}): boolean => { + let schedule: number; + switch (scheduleUnit) { + case ScheduleUnit.SECONDS: + schedule = parseFloat(scheduleNumber); + break; + case ScheduleUnit.MINUTES: + schedule = parseFloat(scheduleNumber) * 60; + break; + default: + schedule = parseFloat(scheduleNumber); + } + + return parseFloat(timeout) > schedule; +}; + +// validation functions return true when invalid +const validateCommon: ValidationLibrary = { + [ConfigKey.SCHEDULE]: ({ [ConfigKey.SCHEDULE]: value }) => { + const { number, unit } = value as MonitorFields[ConfigKey.SCHEDULE]; + const parsedFloat = parseFloat(number); + return !parsedFloat || !unit || parsedFloat < 1; + }, + [ConfigKey.TIMEOUT]: ({ + [ConfigKey.MONITOR_TYPE]: monitorType, + [ConfigKey.TIMEOUT]: timeout, + [ConfigKey.SCHEDULE]: schedule, + }) => { + const { number, unit } = schedule as MonitorFields[ConfigKey.SCHEDULE]; + + // Timeout is not currently supported by browser monitors + if (monitorType === DataStream.BROWSER) { + return false; + } + + return ( + !timeout || + parseFloat(timeout) < 0 || + validateTimeout({ + timeout, + scheduleNumber: number, + scheduleUnit: unit, + }) + ); + }, +}; + +const validateHTTP: ValidationLibrary = { + [ConfigKey.RESPONSE_STATUS_CHECK]: ({ [ConfigKey.RESPONSE_STATUS_CHECK]: value }) => { + const statusCodes = value as MonitorFields[ConfigKey.RESPONSE_STATUS_CHECK]; + return statusCodes.length ? statusCodes.some((code) => !`${code}`.match(DIGITS_ONLY)) : false; + }, + [ConfigKey.RESPONSE_HEADERS_CHECK]: ({ [ConfigKey.RESPONSE_HEADERS_CHECK]: value }) => { + const headers = value as MonitorFields[ConfigKey.RESPONSE_HEADERS_CHECK]; + return validateHeaders(headers); + }, + [ConfigKey.REQUEST_HEADERS_CHECK]: ({ [ConfigKey.REQUEST_HEADERS_CHECK]: value }) => { + const headers = value as MonitorFields[ConfigKey.REQUEST_HEADERS_CHECK]; + return validateHeaders(headers); + }, + [ConfigKey.MAX_REDIRECTS]: ({ [ConfigKey.MAX_REDIRECTS]: value }) => + (!!value && !`${value}`.match(DIGITS_ONLY)) || + parseFloat(value as MonitorFields[ConfigKey.MAX_REDIRECTS]) < 0, + [ConfigKey.URLS]: ({ [ConfigKey.URLS]: value }) => !value, + ...validateCommon, +}; + +const validateTCP: Record = { + [ConfigKey.HOSTS]: ({ [ConfigKey.HOSTS]: value }) => { + return !value || !`${value}`.match(INCLUDES_VALID_PORT); + }, + ...validateCommon, +}; + +const validateICMP: ValidationLibrary = { + [ConfigKey.HOSTS]: ({ [ConfigKey.HOSTS]: value }) => !value, + [ConfigKey.WAIT]: ({ [ConfigKey.WAIT]: value }) => + !!value && + !DIGITS_ONLY.test(`${value}`) && + parseFloat(value as MonitorFields[ConfigKey.WAIT]) < 0, + ...validateCommon, +}; + +const validateThrottleValue = (speed: string | undefined, allowZero?: boolean) => { + if (speed === undefined || speed === '') return false; + const throttleValue = parseFloat(speed); + return isNaN(throttleValue) || (allowZero ? throttleValue < 0 : throttleValue <= 0); +}; + +const validateBrowser: ValidationLibrary = { + ...validateCommon, + [ConfigKey.SOURCE_ZIP_URL]: ({ + [ConfigKey.SOURCE_ZIP_URL]: zipUrl, + [ConfigKey.SOURCE_INLINE]: inlineScript, + }) => !zipUrl && !inlineScript, + [ConfigKey.SOURCE_INLINE]: ({ + [ConfigKey.SOURCE_ZIP_URL]: zipUrl, + [ConfigKey.SOURCE_INLINE]: inlineScript, + }) => !zipUrl && !inlineScript, + [ConfigKey.DOWNLOAD_SPEED]: ({ [ConfigKey.DOWNLOAD_SPEED]: downloadSpeed }) => + validateThrottleValue(downloadSpeed), + [ConfigKey.UPLOAD_SPEED]: ({ [ConfigKey.UPLOAD_SPEED]: uploadSpeed }) => + validateThrottleValue(uploadSpeed), + [ConfigKey.LATENCY]: ({ [ConfigKey.LATENCY]: latency }) => validateThrottleValue(latency, true), +}; + +export type ValidateDictionary = Record; + +export const validate: ValidateDictionary = { + [DataStream.HTTP]: validateHTTP, + [DataStream.TCP]: validateTCP, + [DataStream.ICMP]: validateICMP, + [DataStream.BROWSER]: validateBrowser, +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/index.ts new file mode 100644 index 000000000000000..c69bb9421ab7caa --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './use_is_edit_flow'; +export * from './use_kibana_space'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_form_wrapped.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_form_wrapped.tsx new file mode 100644 index 000000000000000..0f2c75ac5598023 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_form_wrapped.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { FieldValues, useForm, UseFormProps } from 'react-hook-form'; + +export function useFormWrapped( + props?: UseFormProps +) { + const { register, ...restOfForm } = useForm(props); + + const euiRegister = useCallback( + (name, ...registerArgs) => { + const { ref, ...restOfRegister } = register(name, ...registerArgs); + + return { + inputRef: ref, + ref, + ...restOfRegister, + }; + }, + [register] + ); + + return { + register: euiRegister, + ...restOfForm, + }; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_is_edit_flow.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_is_edit_flow.tsx new file mode 100644 index 000000000000000..5f0ef77be6a94cb --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_is_edit_flow.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useRouteMatch } from 'react-router-dom'; +import { MONITOR_EDIT_ROUTE } from '../../../../../../common/constants'; + +export const useIsEditFlow = () => { + const editRouteMatch = useRouteMatch({ path: MONITOR_EDIT_ROUTE }); + return editRouteMatch?.isExact || false; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_kibana_space.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_kibana_space.tsx new file mode 100644 index 000000000000000..e7e7a6f1375f5e0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_kibana_space.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useFetcher } from '@kbn/observability-plugin/public'; +import { ClientPluginsStart } from '../../../../../plugin'; + +export const useKibanaSpace = () => { + const { services } = useKibana(); + + const { + data: space, + loading, + error, + } = useFetcher(() => { + return services.spaces?.getActiveSpace(); + }, [services.spaces]); + + return { + space, + loading, + error, + }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.test.tsx new file mode 100644 index 000000000000000..709d61bc7dc2ffe --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { defaultCore, WrappedHelper } from '../../../utils/testing/rtl_helpers'; +import { renderHook } from '@testing-library/react-hooks'; +import { useMonitorName } from './use_monitor_name'; + +import * as reactRouter from 'react-router-dom'; + +const mockRouter = { + ...reactRouter, +}; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn().mockReturnValue({}), +})); + +describe('useMonitorName', () => { + it('returns expected results', () => { + const { result } = renderHook(() => useMonitorName({}), { wrapper: WrappedHelper }); + + expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: '' }); + expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({ + aggs: { + monitorNames: { + terms: { field: 'synthetics-monitor.attributes.name.keyword', size: 10000 }, + }, + }, + perPage: 0, + type: 'synthetics-monitor', + }); + }); + + it('returns expected results after data', async () => { + defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({ + aggregations: { + monitorNames: { + buckets: [{ key: 'Test' }, { key: 'Test 1' }], + }, + }, + }); + + const { result, waitForNextUpdate } = renderHook(() => useMonitorName({ search: 'Test' }), { + wrapper: WrappedHelper, + }); + + expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' }); + + await waitForNextUpdate(); + + expect(result.current).toStrictEqual({ nameAlreadyExists: true, validName: '' }); + }); + + it('returns expected results after data while editing monitor', async () => { + defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({ + aggregations: { + monitorNames: { + buckets: [{ key: 'Test' }, { key: 'Test 1' }], + }, + }, + }); + + jest.spyOn(mockRouter, 'useParams').mockReturnValue({ monitorId: 'test-id' }); + + const { result, waitForNextUpdate } = renderHook(() => useMonitorName({ search: 'Test' }), { + wrapper: WrappedHelper, + }); + + expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' }); + + await waitForNextUpdate(); + + expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts new file mode 100644 index 000000000000000..a173aae5c700d4d --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import { useFetcher } from '@kbn/observability-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useParams } from 'react-router-dom'; +import { syntheticsMonitorType } from '../../../../../../common/types/saved_objects'; + +interface AggsResponse { + monitorNames: { + buckets: Array<{ + key: string; + }>; + }; +} + +export const useMonitorName = ({ search = '' }: { search?: string }) => { + const [values, setValues] = useState([]); + + const { monitorId } = useParams<{ monitorId: string }>(); + + const { savedObjects } = useKibana().services; + + const { data } = useFetcher(() => { + const aggs = { + monitorNames: { + terms: { + field: `${syntheticsMonitorType}.attributes.name.keyword`, + size: 10000, + }, + }, + }; + return savedObjects?.client.find({ + type: syntheticsMonitorType, + perPage: 0, + aggs, + }); + }, []); + + useEffect(() => { + if (data?.aggregations) { + const newValues = (data.aggregations as AggsResponse)?.monitorNames.buckets.map(({ key }) => + key.toLowerCase() + ); + if (monitorId && newValues.includes(search.toLowerCase())) { + setValues(newValues.filter((val) => val !== search.toLowerCase())); + } else { + setValues(newValues); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, monitorId]); + + const hasMonitor = Boolean( + search && values && values.length > 0 && values?.includes(search.trim().toLowerCase()) + ); + + return { nameAlreadyExists: hasMonitor, validName: hasMonitor ? '' : search }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_edit_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_edit_page.tsx deleted file mode 100644 index d0a0c04150f8d89..000000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_edit_page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { useTrackPageview } from '@kbn/observability-plugin/public'; -import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs'; - -export const MonitorAddEditPage: React.FC = () => { - useTrackPageview({ app: 'synthetics', path: 'add-monitor' }); - useTrackPageview({ app: 'synthetics', path: 'add-monitor', delay: 15000 }); - useMonitorAddEditBreadcrumbs(); - - return ( - <> -

Monitor Add or Edit page

- - ); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.tsx new file mode 100644 index 000000000000000..327e7cf574528e0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { useKibanaSpace } from './hooks/use_kibana_space'; +import { getServiceLocations } from '../../state'; +import { MonitorSteps } from './steps'; +import { MonitorForm } from './form'; +import { ADD_MONITOR_STEPS } from './steps/step_config'; +import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs'; + +export const MonitorAddPage = () => { + useTrackPageview({ app: 'synthetics', path: 'add-monitor' }); + const { space, loading, error } = useKibanaSpace(); + useTrackPageview({ app: 'synthetics', path: 'add-monitor', delay: 15000 }); + useMonitorAddEditBreadcrumbs(); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getServiceLocations()); + }, [dispatch]); + + return !loading && !error ? ( + + + + ) : ( + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx new file mode 100644 index 000000000000000..950d439173004c6 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { InPortal } from 'react-reverse-portal'; +import { MonitorDetailsLinkPortalNode } from './portals'; + +export const MonitorDetailsLinkPortal = ({ name, id }: { name: string; id: string }) => { + return ( + + + + ); +}; + +export const MonitorDetailsLink = ({ name, id }: { name: string; id: string }) => { + const history = useHistory(); + const href = history.createHref({ + pathname: `monitor/${id}`, + }); + return ( + + {name} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.tsx new file mode 100644 index 000000000000000..a1aba64f43f4c71 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { useParams } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { useTrackPageview, useFetcher } from '@kbn/observability-plugin/public'; +import { getServiceLocations } from '../../state'; +import { MonitorSteps } from './steps'; +import { MonitorForm } from './form'; +import { MonitorDetailsLinkPortal } from './monitor_details_portal'; +import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs'; +import { getMonitorAPI } from '../../state/monitor_management/api'; +import { EDIT_MONITOR_STEPS } from './steps/step_config'; + +export const MonitorEditPage: React.FC = () => { + useTrackPageview({ app: 'synthetics', path: 'edit-monitor' }); + useTrackPageview({ app: 'synthetics', path: 'edit-monitor', delay: 15000 }); + const { monitorId } = useParams<{ monitorId: string }>(); + useMonitorAddEditBreadcrumbs(true); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getServiceLocations()); + }, [dispatch]); + + const { data, loading, error } = useFetcher(() => { + return getMonitorAPI({ id: monitorId }); + }, []); + + return data && !loading && !error ? ( + + + + + ) : ( + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/portals.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/portals.tsx new file mode 100644 index 000000000000000..cd32ca9d914d476 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/portals.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createHtmlPortalNode } from 'react-reverse-portal'; + +export const MonitorTypePortalNode = createHtmlPortalNode(); + +export const MonitorDetailsLinkPortalNode = createHtmlPortalNode(); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/index.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/index.tsx new file mode 100644 index 000000000000000..e5be1cd627dd6d1 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/index.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSteps, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import { useFormContext } from 'react-hook-form'; +import { ConfigKey, FormMonitorType, StepMap } from '../types'; +import { AdvancedConfig } from '../advanced'; +import { MonitorTypePortal } from './monitor_type_portal'; + +export const MonitorSteps = ({ + stepMap, + isEditFlow = false, +}: { + stepMap: StepMap; + isEditFlow?: boolean; +}) => { + const { watch } = useFormContext(); + const [type]: [FormMonitorType] = watch([ConfigKey.FORM_MONITOR_TYPE]); + const steps = stepMap[type]; + + return ( + <> + {isEditFlow ? ( + steps.map((step) => ( + <> + + +

{step.title}

+
+ + {step.children} +
+ + + )) + ) : ( + + )} + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/monitor_type.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/monitor_type.tsx new file mode 100644 index 000000000000000..8e4056a6b1a9f85 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/monitor_type.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBetaBadge, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormMonitorType } from '../types'; +import { MONITOR_TYPE_CONFIG } from '../form/field_config'; + +export const MonitorType = ({ monitorType }: { monitorType: FormMonitorType }) => { + const config = MONITOR_TYPE_CONFIG[monitorType]; + return ( + <> + + {i18n.translate('xpack.synthetics.monitorConfig.monitorType.label', { + defaultMessage: 'Monitor type', + })} + + + + + {config.descriptionTitle} + + + {config.beta && ( + + )} + + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/monitor_type_portal.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/monitor_type_portal.tsx new file mode 100644 index 000000000000000..684956dadd569ae --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/monitor_type_portal.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { InPortal } from 'react-reverse-portal'; +import { MonitorTypePortalNode } from '../portals'; +import { FormMonitorType } from '../types'; + +import { MonitorType } from './monitor_type'; + +export const MonitorTypePortal = ({ monitorType }: { monitorType: FormMonitorType }) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step.tsx new file mode 100644 index 000000000000000..2abdca9e5c5f3a4 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +interface Props { + description: React.ReactNode; + children: React.ReactNode; +} + +export const Step = ({ description, children }: Props) => { + return ( + + + {description} + + {children} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx new file mode 100644 index 000000000000000..83463610c4a4e92 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { FormMonitorType, Step, StepMap } from '../types'; +import { StepFields } from './step_fields'; + +const MONITOR_TYPE_STEP: Step = { + title: i18n.translate('xpack.synthetics.monitorConfig.monitorTypeStep.title', { + defaultMessage: 'Select a monitor type', + }), + children: ( + + {i18n.translate('xpack.synthetics.monitorConfig.monitorTypeStep.description', { + defaultMessage: 'Choose a monitor that best fits your use case', + })} +

+ } + stepKey="step1" + /> + ), +}; +const MONITOR_DETAILS_STEP: Step = { + title: i18n.translate('xpack.synthetics.monitorConfig.monitorDetailsStep.title', { + defaultMessage: 'Monitor details', + }), + children: ( + + {i18n.translate('xpack.synthetics.monitorConfig.monitorDetailsStep.description', { + defaultMessage: 'Provide some details about how your monitor should run', + })} +

+ } + stepKey="step2" + /> + ), +}; + +const SCRIPT_RECORDER_BTNS = ( + + + + {i18n.translate('xpack.synthetics.monitorConfig.monitorScriptStep.scriptRecorder.launch', { + defaultMessage: 'Launch Synthetics Recorder', + })} + + + + + {i18n.translate( + 'xpack.synthetics.monitorConfig.monitorScriptStep.scriptRecorder.download', + { + defaultMessage: 'Download Synthetics Recorder', + } + )} + + + +); + +const MONITOR_SCRIPT_STEP: Step = { + title: i18n.translate('xpack.synthetics.monitorConfig.monitorScriptStep.title', { + defaultMessage: 'Add a script', + }), + children: ( + +

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

+ {SCRIPT_RECORDER_BTNS} + + } + stepKey="step3" + /> + ), +}; + +const MONITOR_SCRIPT_STEP_EDIT: Step = { + title: i18n.translate('xpack.synthetics.monitorConfig.monitorScriptEditStep.title', { + defaultMessage: 'Monitor script', + }), + children: ( + +

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

+ {SCRIPT_RECORDER_BTNS} + + } + stepKey="scriptEdit" + /> + ), +}; + +export const ADD_MONITOR_STEPS: StepMap = { + [FormMonitorType.MULTISTEP]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP, MONITOR_SCRIPT_STEP], + [FormMonitorType.SINGLE]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP], + [FormMonitorType.HTTP]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP], + [FormMonitorType.ICMP]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP], + [FormMonitorType.TCP]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP], +}; + +export const EDIT_MONITOR_STEPS: StepMap = { + [FormMonitorType.MULTISTEP]: [MONITOR_SCRIPT_STEP_EDIT, MONITOR_DETAILS_STEP], + [FormMonitorType.SINGLE]: [MONITOR_DETAILS_STEP], + [FormMonitorType.HTTP]: [MONITOR_DETAILS_STEP], + [FormMonitorType.ICMP]: [MONITOR_DETAILS_STEP], + [FormMonitorType.TCP]: [MONITOR_DETAILS_STEP], +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_fields.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_fields.tsx new file mode 100644 index 000000000000000..2ac95831859131b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_fields.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useFormContext, FieldError } from 'react-hook-form'; +import { Step } from './step'; +import { FORM_CONFIG } from '../form/form_config'; +import { Field } from '../form/field'; +import { ConfigKey, FormMonitorType, StepKey } from '../types'; + +export const StepFields = ({ + description, + stepKey, +}: { + description: React.ReactNode; + stepKey: StepKey; +}) => { + const { + watch, + formState: { errors }, + } = useFormContext(); + const [type]: [FormMonitorType] = watch([ConfigKey.FORM_MONITOR_TYPE]); + + return ( + + {FORM_CONFIG[type][stepKey]?.map((field) => { + return ( + + ); + })} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts new file mode 100644 index 000000000000000..e82dc2fa1fd9788 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + UseFormReturn, + ControllerRenderProps, + ControllerFieldState, + FormState, +} from 'react-hook-form'; +import { + ServiceLocations, + FormMonitorType, + SyntheticsMonitor, +} from '../../../../../common/runtime_types/monitor_management'; + +export type StepKey = 'step1' | 'step2' | 'step3' | 'scriptEdit'; + +export interface Step { + title: string; + children: React.ReactNode; +} + +export type StepMap = Record; + +export * from '../../../../../common/runtime_types/monitor_management'; +export * from '../../../../../common/types/monitor_validation'; + +export interface FieldMeta { + fieldKey: string; + component: React.ComponentType; + label?: string; + ariaLabel?: string; + helpText?: string | React.ReactNode; + props?: (params: { + field?: ControllerRenderProps; + formState: FormState; + setValue: UseFormReturn['setValue']; + reset: UseFormReturn['reset']; + locations: ServiceLocations; + dependencies: unknown[]; + dependenciesFieldMeta: Record; + space?: string; + isEdit?: boolean; + }) => Record; + controlled?: boolean; + required?: boolean; + shouldUseSetValue?: boolean; + customHook?: (value: unknown) => { + // custom hooks are only supported for controlled components and only supported for determining error validation + func: Function; + params: unknown; + fieldKey: string; + error: string; + }; + onChange?: ( + event: React.ChangeEvent, + formOnChange: (event: React.ChangeEvent) => void + ) => void; + showWhen?: [string, any]; // show field when another field equals an arbitrary value + validation?: (dependencies: unknown[]) => Parameters[1]; + error?: React.ReactNode; + dependencies?: string[]; // fields that another field may depend for or validation. Values are passed to the validation function +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/use_breadcrumbs.ts index 2fb17340c25fcf6..39d69bf64dea8db 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/use_breadcrumbs.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/use_breadcrumbs.ts @@ -7,19 +7,24 @@ import { i18n } from '@kbn/i18n'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; -import { MONITOR_ADD_ROUTE } from '../../../../../common/constants'; +import { MONITOR_ADD_ROUTE, MONITOR_EDIT_ROUTE } from '../../../../../common/constants'; import { PLUGIN } from '../../../../../common/constants/plugin'; -export const useMonitorAddEditBreadcrumbs = () => { +export const useMonitorAddEditBreadcrumbs = (isEdit?: boolean) => { const kibana = useKibana(); const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; - useBreadcrumbs([ - { - text: ADD_MONITOR_CRUMB, - href: `${appPath}/${MONITOR_ADD_ROUTE}`, - }, - ]); + const config = isEdit + ? { + text: EDIT_MONITOR_CRUMB, + href: `${appPath}/${MONITOR_EDIT_ROUTE}`, + } + : { + text: ADD_MONITOR_CRUMB, + href: `${appPath}/${MONITOR_ADD_ROUTE}`, + }; + + useBreadcrumbs([config]); }; export const ADD_MONITOR_CRUMB = i18n.translate( @@ -28,3 +33,10 @@ export const ADD_MONITOR_CRUMB = i18n.translate( defaultMessage: 'Add monitor', } ); + +export const EDIT_MONITOR_CRUMB = i18n.translate( + 'xpack.synthetics.monitorManagement.editMonitorCrumb', + { + defaultMessage: 'Edit monitor', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/actions.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/actions.tsx index aad917bccdee296..aeb5010f3496e9e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/actions.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/actions.tsx @@ -98,6 +98,7 @@ export const Actions = ({ euiTheme, id, name, reloadPage, canEditSynthetics }: P iconType="boxesHorizontal" color="primary" iconSide="right" + data-test-subj="syntheticsMonitorListActions" onClick={openPopover} /> ); @@ -139,7 +140,7 @@ export const Actions = ({ euiTheme, id, name, reloadPage, canEditSynthetics }: P key="xpack.synthetics.editMonitor" icon="pencil" onClick={closePopover} - href={`${basePath}/app/uptime/edit-monitor/${id}`} + href={`${basePath}/app/synthetics/edit-monitor/${id}`} disabled={!canEditSynthetics} > {labels.EDIT_LABEL} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx index 7ac40ae361ae6fd..763965b0af17afe 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx @@ -42,7 +42,7 @@ export const MonitorsPageHeader = () => { fill iconSide="left" iconType="plusInCircleFilled" - href={`${basePath}/app/uptime${MONITOR_ADD_ROUTE}`} + href={`${basePath}/app/synthetics${MONITOR_ADD_ROUTE}`} isDisabled={!isEnabled} data-test-subj="syntheticsAddMonitorBtn" > diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 8112e9f19d98206..34e3b23f48ca687 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -7,25 +7,38 @@ import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types'; import React, { FC, useEffect } from 'react'; -import { EuiPageTemplateProps, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; +import { + EuiPageTemplateProps, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + useEuiTheme, +} from '@elastic/eui'; import { Route, Switch, useHistory } from 'react-router-dom'; +import { OutPortal } from 'react-reverse-portal'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; +import { MonitorAddPage } from './components/monitor_add_edit/monitor_add_page'; +import { MonitorEditPage } from './components/monitor_add_edit/monitor_edit_page'; import { RunTestManually } from './components/monitor_summary/run_test_manually'; import { MonitorSummaryHeaderContent } from './components/monitor_summary/monitor_summary_header_content'; import { MonitorSummaryTitle } from './components/monitor_summary/monitor_summary_title'; import { MonitorSummaryPage } from './components/monitor_summary/monitor_summary'; import { GettingStartedPage } from './components/getting_started/getting_started_page'; -import { MonitorAddEditPage } from './components/monitor_add_edit/monitor_add_edit_page'; import { MonitorsPageHeader } from './components/monitors_page/management/page_header/monitors_page_header'; import { OverviewPage } from './components/monitors_page/overview/overview_page'; import { SyntheticsPageTemplateComponent } from './components/common/pages/synthetics_page_template'; import { NotFoundPage } from './components/common/pages/not_found'; import { ServiceAllowedWrapper } from './components/common/wrappers/service_allowed_wrapper'; +import { + MonitorTypePortalNode, + MonitorDetailsLinkPortalNode, +} from './components/monitor_add_edit/portals'; import { MONITOR_ADD_ROUTE, + MONITOR_EDIT_ROUTE, MONITORS_ROUTE, OVERVIEW_ROUTE, GETTING_STARTED_ROUTE, @@ -185,27 +198,68 @@ const getRoutes = ( }, }, { - title: i18n.translate('xpack.synthetics.addMonitorRoute.title', { - defaultMessage: 'Add Monitor | {baseTitle}', + title: i18n.translate('xpack.synthetics.createMonitorRoute.title', { + defaultMessage: 'Create Monitor | {baseTitle}', values: { baseTitle }, }), path: MONITOR_ADD_ROUTE, component: () => ( - + ), dataTestSubj: 'syntheticsMonitorAddPage', pageHeader: { pageTitle: ( + ), + children: ( + + + + ), + }} + /> + ), + }, + }, + { + title: i18n.translate('xpack.synthetics.editMonitorRoute.title', { + defaultMessage: 'Edit Monitor | {baseTitle}', + values: { baseTitle }, + }), + path: MONITOR_EDIT_ROUTE, + component: () => ( + + + + ), + dataTestSubj: 'syntheticsMonitorEditPage', + pageHeader: { + pageTitle: ( + ), + rightSideItems: [], + breadcrumbs: [ + { + text: , + }, + ], }, - // bottomBar: , - bottomBarProps: { paddingSize: 'm' as const }, }, ]; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts new file mode 100644 index 000000000000000..66d298f26df2bf8 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { apiService } from '../../../../utils/api_service'; +import { + EncryptedSyntheticsMonitor, + ServiceLocationErrors, + SyntheticsMonitor, + SyntheticsMonitorWithId, +} from '../../../../../common/runtime_types'; +import { API_URLS } from '../../../../../common/constants'; +import { DecryptedSyntheticsMonitorSavedObject } from '../../../../../common/types'; + +export const createMonitorAPI = async ({ + monitor, +}: { + monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; +}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitor> => { + return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor); +}; + +export const updateMonitorAPI = async ({ + monitor, + id, +}: { + monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; + id: string; +}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitorWithId> => { + return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor); +}; + +export const getMonitorAPI = async ({ + id, +}: { + id: string; +}): Promise => { + return await apiService.get(`${API_URLS.SYNTHETICS_MONITORS}/${id}`); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx index 1efc0fdc69a4f26..da86d7d9510b68f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -83,6 +83,7 @@ const Application = (props: SyntheticsAppProps) => { triggersActionsUi: startPlugins.triggersActionsUi, observability: startPlugins.observability, cases: startPlugins.cases, + spaces: startPlugins.spaces, }} > diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts index 6d1e51a66f49a25..0746f2cdae27996 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts @@ -112,5 +112,6 @@ export const browserNormalizers: BrowserNormalizerMap = { [ConfigKey.PLAYWRIGHT_OPTIONS]: getBrowserNormalizer(ConfigKey.PLAYWRIGHT_OPTIONS), [ConfigKey.CUSTOM_HEARTBEAT_ID]: getBrowserNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID), [ConfigKey.ORIGINAL_SPACE]: getBrowserNormalizer(ConfigKey.ORIGINAL_SPACE), + [ConfigKey.TEXT_ASSERTION]: getBrowserNormalizer(ConfigKey.TEXT_ASSERTION), ...commonNormalizers, }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts index 4e67eb671fa22d6..bdea46781dfd96a 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts @@ -91,4 +91,5 @@ export const commonNormalizers: CommonNormalizerMap = { fields?.[ConfigKey.NAMESPACE]?.value ?? DEFAULT_NAMESPACE_STRING, [ConfigKey.REVISION]: getCommonNormalizer(ConfigKey.REVISION), [ConfigKey.MONITOR_SOURCE_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_SOURCE_TYPE), + [ConfigKey.FORM_MONITOR_TYPE]: getCommonNormalizer(ConfigKey.FORM_MONITOR_TYPE), }; diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index 4ccb6965a512287..cb70efbd19854b8 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -40,6 +40,7 @@ import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public'; import { CasesUiStart } from '@kbn/cases-plugin/public'; import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import { PLUGIN } from '../common/constants/plugin'; import { MONITORS_ROUTE } from '../common/constants/ui'; import { @@ -76,6 +77,7 @@ export interface ClientPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; cases: CasesUiStart; dataViews: DataViewsPublicPluginStart; + spaces: SpacesPluginStart; cloud?: CloudStart; } @@ -194,7 +196,6 @@ export class UptimePlugin ], mount: async (params: AppMountParameters) => { const [coreStart, corePlugins] = await core.getStartServices(); - const { renderApp } = await import('./legacy_uptime/app/render_app'); return renderApp(coreStart, plugins, corePlugins, params, this.initContext.env.mode.dev); }, diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts index c692bd25415d0ad..a5d8365d0de2ab1 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { mergeWith } from 'lodash'; import { schema } from '@kbn/config-schema'; import { SavedObjectsUpdateResponse, @@ -20,6 +20,7 @@ import { SyntheticsMonitorWithSecrets, SyntheticsMonitor, ConfigKey, + FormMonitorType, } from '../../../common/runtime_types'; import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; @@ -77,10 +78,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ( ); const normalizedPreviousMonitor = normalizeSecrets(decryptedPreviousMonitor).attributes; - const editedMonitor = { - ...normalizedPreviousMonitor, - ...monitor, - }; + const editedMonitor = mergeWith(normalizedPreviousMonitor, monitor, customizer); const validationResult = validateMonitor(editedMonitor as MonitorFields); @@ -94,12 +92,15 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ( revision: (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1, }; const formattedMonitor = formatSecrets(monitorWithRevision); + const isMultiStepMonitor = + monitor.type === 'browser' && + monitor[ConfigKey.FORM_MONITOR_TYPE] !== FormMonitorType.SINGLE; const editedMonitorSavedObject: SavedObjectsUpdateResponse = await savedObjectsClient.update( syntheticsMonitorType, monitorId, - monitor.type === 'browser' ? { ...formattedMonitor, urls: '' } : formattedMonitor + isMultiStepMonitor ? { ...formattedMonitor, urls: '' } : formattedMonitor ); const errors = await syncEditedMonitor({ @@ -185,3 +186,10 @@ export const syncEditedMonitor = async ({ throw e; } }; + +// Ensure that METADATA is merged deeply, to protect AAD and prevent decryption errors +const customizer = (_: any, srcValue: any, key: string) => { + if (key !== ConfigKey.METADATA) { + return srcValue; + } +}; diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts index 58855477ab8a63b..8a532279dda88c1 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts @@ -12,6 +12,7 @@ import { CommonFields, ConfigKey, DataStream, + FormMonitorType, HTTPAdvancedFields, HTTPFields, HTTPSimpleFields, @@ -75,6 +76,7 @@ describe('validateMonitor', () => { }, ], [ConfigKey.NAMESPACE]: 'testnamespace', + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP, }; testMetaData = { is_tls_enabled: false, @@ -90,6 +92,7 @@ describe('validateMonitor', () => { [ConfigKey.HOSTS]: 'test-hosts', [ConfigKey.WAIT]: '', [ConfigKey.MONITOR_TYPE]: DataStream.ICMP, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP, }; testTLSFields = { @@ -105,6 +108,7 @@ describe('validateMonitor', () => { ...testCommonFields, [ConfigKey.METADATA]: testMetaData, [ConfigKey.HOSTS]: 'https://host1.com', + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP, }; testTCPAdvancedFields = { @@ -126,6 +130,7 @@ describe('validateMonitor', () => { [ConfigKey.METADATA]: testMetaData, [ConfigKey.MAX_REDIRECTS]: '3', [ConfigKey.URLS]: 'https://example.com', + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP, }; testHTTPAdvancedFields = { @@ -162,6 +167,7 @@ describe('validateMonitor', () => { testBrowserSimpleFields = { ...testZipUrlTLSFields, ...testCommonFields, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP, [ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.PROJECT, [ConfigKey.JOURNEY_ID]: '', [ConfigKey.PROJECT_ID]: '', @@ -414,6 +420,7 @@ function getJsonPayload() { ' "response.include_body": "never",' + ' "check.response.headers": {},' + ' "response.include_headers": true,' + + ' "form_monitor_type": "http",' + ' "check.response.status": [' + ' "200",' + ' "201"' + diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts index 18812e4f1752578..7e8c75a7084306a 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts @@ -72,5 +72,6 @@ export const browserFormatters: BrowserFormatMap = { stringToObjectFormatter(fields[ConfigKey.PLAYWRIGHT_OPTIONS] || ''), [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, [ConfigKey.ORIGINAL_SPACE]: null, + [ConfigKey.TEXT_ASSERTION]: null, ...commonFormatters, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts index 55427c01a1f22d4..0163ee52981e506 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts @@ -29,6 +29,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.REVISION]: null, [ConfigKey.MONITOR_SOURCE_TYPE]: (fields) => fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI, + [ConfigKey.FORM_MONITOR_TYPE]: null, }; export const arrayFormatter = (value: string[] = []) => (value.length ? value : null); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts index 67347e405545a8e..a1c1d68c4a829f1 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts @@ -25,6 +25,8 @@ const UI_KEYS_TO_SKIP = [ ConfigKey.IS_THROTTLING_ENABLED, ConfigKey.REVISION, ConfigKey.CUSTOM_HEARTBEAT_ID, + ConfigKey.FORM_MONITOR_TYPE, + ConfigKey.TEXT_ASSERTION, 'secrets', ]; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts b/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts index 2e09c5906c65b0f..5f797f34515cd5c 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts @@ -11,6 +11,7 @@ import { BrowserFields, ConfigKey, DataStream, + FormMonitorType, Locations, ProjectBrowserMonitor, ScheduleUnit, @@ -58,6 +59,7 @@ export const normalizeProjectMonitor = ({ const defaultFields = DEFAULT_FIELDS[DataStream.BROWSER]; const normalizedFields: NormalizedPublicFields = { [ConfigKey.MONITOR_TYPE]: DataStream.BROWSER, + [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP, [ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.PROJECT, [ConfigKey.NAME]: monitor.name || '', [ConfigKey.SCHEDULE]: { diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx index 535c856d51e8fca..7d60cbc5d0e003f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx @@ -225,7 +225,7 @@ describe('Combined Queries', () => { ignoreFilterIfFieldNotInIndex: true, dateFormatTZ: 'America/New_York', }; - test('No Data Provider & No kqlQuery & and isEventViewer is false', () => { + test('No Data Provider & No kqlQuery', () => { expect( combineQueries({ config, @@ -239,26 +239,7 @@ describe('Combined Queries', () => { ).toBeNull(); }); - test('No Data Provider & No kqlQuery & isEventViewer is true', () => { - const isEventViewer = true; - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - isEventViewer, - }) - ).toEqual({ - filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}', - }); - }); - test('No Data Provider & No kqlQuery & with Filters', () => { - const isEventViewer = true; expect( combineQueries({ config, @@ -293,7 +274,6 @@ describe('Combined Queries', () => { ], kqlQuery: { query: '', language: 'kuery' }, kqlMode: 'search', - isEventViewer, }) ).toEqual({ filterQuery: diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx index 03830339761bc63..34fb215e708d566 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx @@ -141,7 +141,6 @@ interface CombineQueries { filters: Filter[]; kqlQuery: Query; kqlMode: string; - isEventViewer?: boolean; } export const combineQueries = ({ @@ -152,23 +151,10 @@ export const combineQueries = ({ filters = [], kqlQuery, kqlMode, - isEventViewer, }: CombineQueries): { filterQuery: string | undefined; kqlError: Error | undefined } | null => { const kuery: Query = { query: '', language: kqlQuery.language }; - if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { + if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters)) { return null; - } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { - const [filterQuery, kqlError] = convertToBuildEsQuery({ - config, - queries: [kuery], - indexPattern, - filters, - }); - - return { - filterQuery, - kqlError, - }; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { const [filterQuery, kqlError] = convertToBuildEsQuery({ config, @@ -229,15 +215,6 @@ export const combineQueries = ({ }; }; -export const buildCombinedQuery = (combineQueriesParams: CombineQueries) => { - const combinedQuery = combineQueries(combineQueriesParams); - return combinedQuery?.filterQuery - ? { - filterQuery: combinedQuery.filterQuery, - } - : null; -}; - export const buildTimeRangeFilter = (from: string, to: string): Filter => ({ range: { diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx index 15fd4d2ce75cff7..d33d2fb5635b815 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx @@ -41,9 +41,6 @@ jest.mock('../helpers', () => { filter: [], }, }), - buildCombinedQuery: () => ({ - filterQuery: '{"bool":{"must":[],"filter":[]}}', - }), }; }); const defaultProps: TGridIntegratedProps = tGridIntegratedProps; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 7183c164a24be9e..898611d073a95d6 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -39,7 +39,7 @@ import type { import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { defaultHeaders } from '../body/column_headers/default_headers'; -import { buildCombinedQuery, getCombinedFilterQuery, resolverIsShowing } from '../helpers'; +import { getCombinedFilterQuery, resolverIsShowing } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import { useTimelineEvents, InspectResponse, Refetch } from '../../../container'; import { StatefulBody } from '../body'; @@ -194,26 +194,32 @@ const TGridIntegratedComponent: React.FC = ({ }, [dispatch, id, isQueryLoading]); const justTitle = useMemo(() => {title}, [title]); + const esQueryConfig = getEsQueryConfig(uiSettings); - const combinedQueries = buildCombinedQuery({ - config: getEsQueryConfig(uiSettings), - dataProviders, - indexPattern, - browserFields, - filters, - kqlQuery: query, - kqlMode, - isEventViewer: true, - }); + const filterQuery = useMemo( + () => + getCombinedFilterQuery({ + config: esQueryConfig, + browserFields, + dataProviders, + filters, + from: start, + indexPattern, + kqlMode, + kqlQuery: query, + to: end, + }), + [esQueryConfig, dataProviders, indexPattern, browserFields, filters, start, end, query, kqlMode] + ); const canQueryTimeline = useMemo( () => - combinedQueries != null && + filterQuery != null && isLoadingIndexPattern != null && !isLoadingIndexPattern && !isEmpty(start) && !isEmpty(end), - [isLoadingIndexPattern, combinedQueries, start, end] + [isLoadingIndexPattern, filterQuery, start, end] ); const fields = useMemo( @@ -241,7 +247,7 @@ const TGridIntegratedComponent: React.FC = ({ endDate: end, entityType, fields, - filterQuery: combinedQueries?.filterQuery, + filterQuery, id, indexNames, limit: itemsPerPage, @@ -251,23 +257,6 @@ const TGridIntegratedComponent: React.FC = ({ startDate: start, }); - const filterQuery = useMemo( - () => - getCombinedFilterQuery({ - config: getEsQueryConfig(uiSettings), - browserFields, - dataProviders, - filters, - from: start, - indexPattern, - isEventViewer: true, - kqlMode, - kqlQuery: query, - to: end, - }), - [uiSettings, dataProviders, indexPattern, browserFields, filters, start, end, query, kqlMode] - ); - const totalCountMinusDeleted = useMemo( () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), [deletedEventIds.length, totalCount] diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index 1eb327d95827a43..14a03e8bd5049e2 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -30,7 +30,7 @@ import type { } from '../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { defaultHeaders } from '../body/column_headers/default_headers'; -import { combineQueries, getCombinedFilterQuery } from '../helpers'; +import { getCombinedFilterQuery } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import type { State } from '../../../store/t_grid'; import { useTimelineEvents } from '../../../container'; @@ -170,25 +170,32 @@ const TGridStandaloneComponent: React.FC = ({ }, [dispatch, isQueryLoading]); const justTitle = useMemo(() => {title}, [title]); + const esQueryConfig = getEsQueryConfig(uiSettings); - const combinedQueries = useMemo( + const filterQuery = useMemo( () => - combineQueries({ - config: getEsQueryConfig(uiSettings), - dataProviders: EMPTY_DATA_PROVIDERS, - indexPattern: indexPatterns, + getCombinedFilterQuery({ + config: esQueryConfig, browserFields, + dataProviders: EMPTY_DATA_PROVIDERS, filters, - kqlQuery: query, + from: start, + indexPattern: indexPatterns, kqlMode: 'search', - isEventViewer: true, + kqlQuery: query, + to: end, }), - [uiSettings, indexPatterns, browserFields, filters, query] + [esQueryConfig, indexPatterns, browserFields, filters, start, end, query] ); const canQueryTimeline = useMemo( - () => !indexPatternsLoading && combinedQueries != null && !isEmpty(start) && !isEmpty(end), - [indexPatternsLoading, combinedQueries, start, end] + () => + filterQuery != null && + indexPatternsLoading != null && + !indexPatternsLoading && + !isEmpty(start) && + !isEmpty(end), + [indexPatternsLoading, filterQuery, start, end] ); const fields = useMemo( @@ -221,7 +228,7 @@ const TGridStandaloneComponent: React.FC = ({ entityType, excludeEcsData: true, fields, - filterQuery: combinedQueries?.filterQuery, + filterQuery, id: STANDALONE_ID, indexNames, limit: itemsPerPageStore, @@ -266,23 +273,6 @@ const TGridStandaloneComponent: React.FC = ({ [deletedEventIds, events] ); - const filterQuery = useMemo( - () => - getCombinedFilterQuery({ - config: getEsQueryConfig(uiSettings), - dataProviders: EMPTY_DATA_PROVIDERS, - indexPattern: indexPatterns, - browserFields, - filters, - kqlQuery: query, - kqlMode: 'search', - isEventViewer: true, - from: start, - to: end, - }), - [uiSettings, indexPatterns, browserFields, filters, query, start, end] - ); - useEffect(() => { setIsQueryLoading(loading); }, [loading]); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f8ae6a357f90a7d..d7053e47a137ade 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -390,13 +390,6 @@ "controls.rangeSlider.popover.clearRangeTitle": "Effacer la plage", "controls.rangeSlider.popover.noAvailableDataHelpText": "Il n'y a aucune donnée à afficher. Ajustez la plage temporelle et les filtres.", "controls.rangeSlider.popover.noDataHelpText": "La plage sélectionnée n'a généré aucune donnée. Aucun filtre n'a été appliqué.", - "controls.timeSlider.description": "Ajouter un curseur pour la sélection d'une plage temporelle", - "controls.timeSlider.displayName": "Curseur temporel", - "controls.timeSlider.editor.dataViewTitle": "Vue de données", - "controls.timeSlider.editor.fieldTitle": "Champ", - "controls.timeSlider.editor.noDataViewTitle": "Sélectionner la vue de données", - "controls.timeSlider.noDocuments.label": "Aucun document n'a été trouvé. Sélection de plage non disponible.", - "controls.timeSlider.resetButton.label": "Réinitialiser les sélections", "core.chrome.browserDeprecationWarning": "La prise en charge d'Internet Explorer sera abandonnée dans les futures versions de ce logiciel. Veuillez consulter le site {link}.", "core.deprecations.deprecations.fetchFailedMessage": "Impossible d'extraire les informations de déclassement pour le plug-in {domainId}.", "core.deprecations.deprecations.fetchFailedTitle": "Impossible d'extraire les déclassements pour {domainId}", @@ -25292,9 +25285,6 @@ "xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle": "Affichage de : {modifier}{totalAlertsFormatted} {totalAlerts, plural, =1 {alerte} other {alertes}}", "xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel": "Premiers {fieldName}", "xpack.securitySolution.detectionEngine.alerts.openedAlertSuccessToastMessage": "Ouverture réussie de {totalAlerts} {totalAlerts, plural, =1 {alerte} other {alertes}}.", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.selectAllAlertsTitle": "Sélectionner un total de {totalAlertsFormatted} {totalAlerts, plural, =1 {alerte} other {alertes}}", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.selectedAlertsTitle": "{selectedAlertsFormatted} {selectedAlerts, plural, =1 {alerte} other {alertes}} sélectionnée(s)", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.showingAlertsTitle": "Affichage de {totalAlertsFormatted} {totalAlerts, plural, =1 {alerte} other {alertes}}", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditConfirmation.confirmButtonLabel": "Modifier {customRulesCount, plural, =1 {# règle personnalisée} other {# règles personnalisées}}", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setIndexPatternsWarningCallout": "Vous êtes sur le point d'écraser les modèles d'indexation pour {rulesCount, plural, one {# règle sélectionnée} other {# règles sélectionnées}}. Sélectionnez Enregistrer pour appliquer les modifications.", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setTagsWarningCallout": "Vous êtes sur le point d'écraser les balises pour {rulesCount, plural, one {# règle sélectionnée} other {# règles sélectionnées}}. Sélectionnez Enregistrer pour appliquer les modifications.", @@ -26174,10 +26164,6 @@ "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle": "Inclure les alertes fondamentales", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showOnlyThreatIndicatorAlerts": "Afficher uniquement les alertes d'indicateur de menaces", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle": "Filtres supplémentaires", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.acknowledgedSelectedTitle": "Marquer comme reconnue", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.closeSelectedTitle": "Fermer la sélection", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.openSelectedTitle": "Ouvrir la sélection", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle": "Effacer la sélection", "xpack.securitySolution.detectionEngine.alerts.utilityBar.takeActionTitle": "Entreprendre une action", "xpack.securitySolution.detectionEngine.alertTitle": "Alertes", "xpack.securitySolution.detectionEngine.buttonManageRules": "Gérer les règles", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7029e1e2ed77def..f5361e3c867bd99 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -390,13 +390,6 @@ "controls.rangeSlider.popover.clearRangeTitle": "範囲を消去", "controls.rangeSlider.popover.noAvailableDataHelpText": "表示するデータがありません。時間範囲とフィルターを調整します。", "controls.rangeSlider.popover.noDataHelpText": "選択された範囲にはデータがありません。フィルターが適用されませんでした。", - "controls.timeSlider.description": "時間範囲を選択するためのスライダーを追加", - "controls.timeSlider.displayName": "時間スライダー", - "controls.timeSlider.editor.dataViewTitle": "データビュー", - "controls.timeSlider.editor.fieldTitle": "フィールド", - "controls.timeSlider.editor.noDataViewTitle": "データビューを選択", - "controls.timeSlider.noDocuments.label": "ドキュメントが見つかりませんでした。 範囲選択を使用できません。", - "controls.timeSlider.resetButton.label": "選択項目をリセット", "core.chrome.browserDeprecationWarning": "このソフトウェアの将来のバージョンでは、Internet Explorerのサポートが削除されます。{link}をご確認ください。", "core.deprecations.deprecations.fetchFailedMessage": "プラグイン{domainId}の廃止予定情報を取得できません。", "core.deprecations.deprecations.fetchFailedTitle": "{domainId}の廃止予定を取得できませんでした", @@ -25271,9 +25264,6 @@ "xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle": "{modifier}{totalAlertsFormatted} {totalAlerts, plural, other {件のアラート}}を表示しています", "xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel": "トップ{fieldName}", "xpack.securitySolution.detectionEngine.alerts.openedAlertSuccessToastMessage": "{totalAlerts} {totalAlerts, plural, other {件のアラート}}を正常に開きました。", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.selectAllAlertsTitle": "すべての{totalAlertsFormatted} {totalAlerts, plural, other {件のアラート}}を選択", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.selectedAlertsTitle": "Selected {selectedAlertsFormatted} {selectedAlerts, plural, other {件のアラート}}", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.showingAlertsTitle": "すべての{totalAlertsFormatted} {totalAlerts, plural, other {件のアラート}}を表示しています", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditConfirmation.confirmButtonLabel": "{customRulesCount, plural, other {# 個のカスタムルール}}を編集", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setIndexPatternsWarningCallout": "{rulesCount, plural, other {# 個の選択したルール}} のインデックスパターンを上書きしようとしています。[保存]をクリックすると、変更が適用されます。", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setTagsWarningCallout": "{rulesCount, plural, other {# 個の選択したルール}}のタグを上書きしようとしています。[保存]をクリックすると、変更が適用されます。\n", @@ -26151,10 +26141,6 @@ "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle": "基本アラートを含める", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showOnlyThreatIndicatorAlerts": "脅威インジケーターアラートのみを表示", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle": "追加のフィルター", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.acknowledgedSelectedTitle": "確認済みに設定", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.closeSelectedTitle": "選択した項目を閉じる", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.openSelectedTitle": "選択した項目を開く", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle": "選択した項目をクリア", "xpack.securitySolution.detectionEngine.alerts.utilityBar.takeActionTitle": "アクションを実行", "xpack.securitySolution.detectionEngine.alertTitle": "アラート", "xpack.securitySolution.detectionEngine.buttonManageRules": "ルールの管理", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 466249148fec75d..3082af48e77afe4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -390,13 +390,6 @@ "controls.rangeSlider.popover.clearRangeTitle": "清除范围", "controls.rangeSlider.popover.noAvailableDataHelpText": "没有可显示的数据。调整时间范围和筛选。", "controls.rangeSlider.popover.noDataHelpText": "选定范围未生成任何数据。未应用任何筛选。", - "controls.timeSlider.description": "添加用于选择时间范围的滑块", - "controls.timeSlider.displayName": "时间滑块", - "controls.timeSlider.editor.dataViewTitle": "数据视图", - "controls.timeSlider.editor.fieldTitle": "字段", - "controls.timeSlider.editor.noDataViewTitle": "选择数据视图", - "controls.timeSlider.noDocuments.label": "找不到文档。 范围选择不可用。", - "controls.timeSlider.resetButton.label": "重置选择", "core.chrome.browserDeprecationWarning": "本软件的未来版本将放弃对 Internet Explorer 的支持,请查看{link}。", "core.deprecations.deprecations.fetchFailedMessage": "无法提取插件 {domainId} 的弃用信息。", "core.deprecations.deprecations.fetchFailedTitle": "无法提取 {domainId} 的弃用信息", @@ -25300,9 +25293,6 @@ "xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle": "正在显示:{modifier}{totalAlertsFormatted} 个{totalAlerts, plural, other {告警}}", "xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel": "排名靠前的{fieldName}", "xpack.securitySolution.detectionEngine.alerts.openedAlertSuccessToastMessage": "已成功打开 {totalAlerts} 个{totalAlerts, plural, other {告警}}。", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.selectAllAlertsTitle": "选择全部 {totalAlertsFormatted} 个{totalAlerts, plural, other {告警}}", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.selectedAlertsTitle": "已选择 {selectedAlertsFormatted} 个{selectedAlerts, plural, other {告警}}", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.showingAlertsTitle": "正在显示 {totalAlertsFormatted} 个{totalAlerts, plural, other {告警}}", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditConfirmation.confirmButtonLabel": "编辑 {customRulesCount, plural, other {# 个定制规则}}", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setIndexPatternsWarningCallout": "您即将覆盖 {rulesCount, plural, other {# 个选定规则}}的索引模式,按“保存”可应用更改。", "xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setTagsWarningCallout": "您即将覆盖 {rulesCount, plural, other {# 个选定规则}}的标签,按“保存”可应用更改。", @@ -26182,10 +26172,6 @@ "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle": "包括构建块告警", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showOnlyThreatIndicatorAlerts": "仅显示威胁指标告警", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle": "其他筛选", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.acknowledgedSelectedTitle": "标记为已确认", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.closeSelectedTitle": "关闭所选", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.openSelectedTitle": "打开所选", - "xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle": "清除所选内容", "xpack.securitySolution.detectionEngine.alerts.utilityBar.takeActionTitle": "采取操作", "xpack.securitySolution.detectionEngine.alertTitle": "告警", "xpack.securitySolution.detectionEngine.buttonManageRules": "管理规则", diff --git a/x-pack/test/accessibility/apps/dashboard_controls.ts b/x-pack/test/accessibility/apps/dashboard_controls.ts index 4058bee7dcd13ea..8560b65b5fd3444 100644 --- a/x-pack/test/accessibility/apps/dashboard_controls.ts +++ b/x-pack/test/accessibility/apps/dashboard_controls.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'home', 'dashboardControls']); const browser = getService('browser'); - // FLAKY: https://github.com/elastic/kibana/issues/135508 - describe.skip('Dashboard controls a11y tests', () => { + describe('Dashboard controls a11y tests', () => { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/setup.ts b/x-pack/test/alerting_api_integration/security_and_spaces/setup.ts index 69ca58e1edc1259..74dcdc3989e2aac 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/setup.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/setup.ts @@ -39,7 +39,7 @@ export async function setupSpacesAndUsers(getService: FtrProviderContext['getSer export async function tearDown(getService: FtrProviderContext['getService']) { const securityService = getService('security'); - const esArchiver = getService('esArchiver'); + const spacesService = getService('spaces'); for (const user of Users) { await securityService.user.delete(user.username); @@ -52,5 +52,7 @@ export async function tearDown(getService: FtrProviderContext['getService']) { } } - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + for (const space of Spaces) { + await spacesService.delete(space.id); + } } diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts b/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts index bd9a9a0e7c480b1..aba182274c9a3ce 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts @@ -37,6 +37,7 @@ const COMMON_HEADERS = { export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); describe('log highlight apis', () => { before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs')); @@ -44,8 +45,8 @@ export default function ({ getService }: FtrProviderContext) { describe('/log_entries/highlights', () => { describe('with the default source', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/empty_kibana')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana')); + before(() => kibanaServer.savedObjects.cleanStandardList()); + after(() => kibanaServer.savedObjects.cleanStandardList()); it('Handles empty responses', async () => { const { body } = await supertest diff --git a/x-pack/test/api_integration/apis/metrics_ui/sources.ts b/x-pack/test/api_integration/apis/metrics_ui/sources.ts index 0ba6f49634b54e1..c58b332c7102f4c 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/sources.ts @@ -18,6 +18,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); const SOURCE_API_URL = '/api/metrics/source/default'; + const kibanaServer = getService('kibanaServer'); const patchRequest = async ( body: PartialMetricsSourceConfigurationProperties ): Promise => { @@ -32,8 +33,8 @@ export default function ({ getService }: FtrProviderContext) { describe('sources', () => { before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - beforeEach(() => esArchiver.load('x-pack/test/functional/es_archives/empty_kibana')); - afterEach(() => esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana')); + before(() => kibanaServer.savedObjects.cleanStandardList()); + after(() => kibanaServer.savedObjects.cleanStandardList()); describe('patch request', () => { it('applies all top-level field updates to an existing source', async () => { diff --git a/x-pack/test/api_integration/apis/ml/modules/index.ts b/x-pack/test/api_integration/apis/ml/modules/index.ts index 182de3e2648cba0..de8919976fd3310 100644 --- a/x-pack/test/api_integration/apis/ml/modules/index.ts +++ b/x-pack/test/api_integration/apis/ml/modules/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const ml = getService('ml'); const fleetPackages = ['apache', 'nginx']; @@ -16,8 +16,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('modules', function () { before(async () => { - // use empty_kibana to make sure the fleet setup is removed correctly after the tests - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + // use await kibanaServer.savedObjects.cleanStandardList(); to make sure the fleet setup is removed correctly after the tests + await kibanaServer.savedObjects.cleanStandardList(); // Fleet need to be setup to be able to setup packages await ml.testResources.setupFleet(); @@ -32,7 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { for (const fleetPackage of installedPackages) { await ml.testResources.removeFleetPackage(fleetPackage.pkgName, fleetPackage.version); } - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); loadTestFile(require.resolve('./get_module')); diff --git a/x-pack/test/api_integration/apis/osquery/packs.ts b/x-pack/test/api_integration/apis/osquery/packs.ts index 840d5aecaeae03b..dc019bffeec6fac 100644 --- a/x-pack/test/api_integration/apis/osquery/packs.ts +++ b/x-pack/test/api_integration/apis/osquery/packs.ts @@ -50,13 +50,13 @@ export default function ({ getService }: FtrProviderContext) { let hostedPolicy: Record; let packagePolicyId: string; before(async () => { - await getService('esArchiver').load('x-pack/test/functional/es_archives/empty_kibana'); + await getService('kibanaServer').savedObjects.cleanStandardList(); await getService('esArchiver').load( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); }); after(async () => { - await getService('esArchiver').unload('x-pack/test/functional/es_archives/empty_kibana'); + await getService('kibanaServer').savedObjects.cleanStandardList(); await getService('esArchiver').unload( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); diff --git a/x-pack/test/api_integration/apis/security_solution/saved_objects/notes.ts b/x-pack/test/api_integration/apis/security_solution/saved_objects/notes.ts index 03ea91775d1bf80..a6dc1f88c596e54 100644 --- a/x-pack/test/api_integration/apis/security_solution/saved_objects/notes.ts +++ b/x-pack/test/api_integration/apis/security_solution/saved_objects/notes.ts @@ -10,12 +10,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const supertest = getService('supertest'); describe('Note - Saved Objects', () => { - beforeEach(() => esArchiver.load('x-pack/test/functional/es_archives/empty_kibana')); - afterEach(() => esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana')); + before(() => kibanaServer.savedObjects.cleanStandardList()); + after(() => kibanaServer.savedObjects.cleanStandardList()); describe('create a note', () => { it('should return a timelineId, timelineVersion, noteId and version', async () => { diff --git a/x-pack/test/api_integration/apis/security_solution/saved_objects/pinned_events.ts b/x-pack/test/api_integration/apis/security_solution/saved_objects/pinned_events.ts index 8391f2f9ab18a2f..dcfe8a109b04e14 100644 --- a/x-pack/test/api_integration/apis/security_solution/saved_objects/pinned_events.ts +++ b/x-pack/test/api_integration/apis/security_solution/saved_objects/pinned_events.ts @@ -10,12 +10,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const supertest = getService('supertest'); describe('Pinned Events - Saved Objects', () => { - beforeEach(() => esArchiver.load('x-pack/test/functional/es_archives/empty_kibana')); - afterEach(() => esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana')); + before(() => kibanaServer.savedObjects.cleanStandardList()); + after(() => kibanaServer.savedObjects.cleanStandardList()); describe('Pinned an event', () => { it('return a timelineId, timelineVersion, pinnedEventId and version', async () => { diff --git a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts index 32d307ebc22d561..e3a79ba91b29c93 100644 --- a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts @@ -12,12 +12,12 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { createBasicTimeline } from './helpers'; export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const supertest = getService('supertest'); describe('Timeline - Saved Objects', () => { - beforeEach(() => esArchiver.load('x-pack/test/functional/es_archives/empty_kibana')); - afterEach(() => esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana')); + beforeEach(() => kibanaServer.savedObjects.cleanStandardList()); + afterEach(() => kibanaServer.savedObjects.cleanStandardList()); describe('Persist a timeline', () => { it('Create a timeline just with a title', async () => { diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_private_location.ts index 7668f2d1f840cd9..ea5ecf8a2c235f3 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_private_location.ts @@ -34,7 +34,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await supertestAPI.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); await supertestAPI - .post('/api/fleet/epm/packages/synthetics/0.9.4') + .post('/api/fleet/epm/packages/synthetics/0.10.2') .set('kbn-xsrf', 'true') .send({ force: true }) .expect(200); @@ -115,7 +115,7 @@ export default function ({ getService }: FtrProviderContext) { expect(packagePolicy.policy_id).eql(testFleetPolicyID); - comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name)); + comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name, newMonitorId)); }); let testFleetPolicyID2: string; @@ -154,7 +154,7 @@ export default function ({ getService }: FtrProviderContext) { expect(packagePolicy.policy_id).eql(testFleetPolicyID); - comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name)); + comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name, newMonitorId)); packagePolicy = apiResponsePolicy.body.items.find( (pkgPolicy: PackagePolicy) => @@ -162,7 +162,10 @@ export default function ({ getService }: FtrProviderContext) { ); expect(packagePolicy.policy_id).eql(testFleetPolicyID2); - comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name)); + comparePolicies( + packagePolicy, + getTestSyntheticsPolicy(httpMonitorJson.name, newMonitorId, 'Test private location 1') + ); }); it('deletes integration for a removed location from monitor', async () => { @@ -187,7 +190,7 @@ export default function ({ getService }: FtrProviderContext) { expect(packagePolicy.policy_id).eql(testFleetPolicyID); - comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name)); + comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name, newMonitorId)); packagePolicy = apiResponsePolicy.body.items.find( (pkgPolicy: PackagePolicy) => @@ -274,7 +277,7 @@ export default function ({ getService }: FtrProviderContext) { expect(packagePolicy.policy_id).eql(testFleetPolicyID); expect(packagePolicy.name).eql(`${monitor.name}-Test private location 0-${SPACE_ID}`); - comparePolicies(packagePolicy, getTestSyntheticsPolicy(monitor.name)); + comparePolicies(packagePolicy, getTestSyntheticsPolicy(monitor.name, monitorId)); } finally { await security.user.delete(username); await security.role.delete(roleName); diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts index 08b5306a51e5cf6..d4a82030fd9896a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); await supertest - .post('/api/fleet/epm/packages/synthetics/0.9.4') + .post('/api/fleet/epm/packages/synthetics/0.10.2') .set('kbn-xsrf', 'true') .send({ force: true }) .expect(200); @@ -1011,7 +1011,18 @@ export default function ({ getService }: FtrProviderContext) { ); expect(packagePolicy.policy_id).eql(testPolicyId); - comparePolicies(packagePolicy, getTestProjectSyntheticsPolicy()); + const configId = monitorsResponse.body.monitors[0].id; + const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]; + + comparePolicies( + packagePolicy, + getTestProjectSyntheticsPolicy({ + inputs: {}, + name: 'check if title is present-Test private location 0', + id, + configId, + }) + ); } finally { await deleteMonitor(projectMonitors.monitors[0].id, projectMonitors.project); @@ -1056,7 +1067,18 @@ export default function ({ getService }: FtrProviderContext) { expect(packagePolicy.policy_id).eql(testPolicyId); - comparePolicies(packagePolicy, getTestProjectSyntheticsPolicy()); + const configId = monitorsResponse.body.monitors[0].id; + const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]; + + comparePolicies( + packagePolicy, + getTestProjectSyntheticsPolicy({ + inputs: {}, + name: 'check if title is present-Test private location 0', + id, + configId, + }) + ); await supertest .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) @@ -1124,7 +1146,18 @@ export default function ({ getService }: FtrProviderContext) { expect(packagePolicy.policy_id).eql(testPolicyId); - comparePolicies(packagePolicy, getTestProjectSyntheticsPolicy()); + const configId = monitorsResponse.body.monitors[0].id; + const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]; + + comparePolicies( + packagePolicy, + getTestProjectSyntheticsPolicy({ + inputs: {}, + name: 'check if title is present-Test private location 0', + id, + configId, + }) + ); await supertest .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) @@ -1199,17 +1232,25 @@ export default function ({ getService }: FtrProviderContext) { '/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics' ); + const configId = monitorsResponse.body.monitors[0].id; + const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]; + const policyId = `${id}-${testPolicyId}`; + const packagePolicy = apiResponsePolicy.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === - monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID] + - '-' + - testPolicyId + (pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId ); expect(packagePolicy.policy_id).eql(testPolicyId); - comparePolicies(packagePolicy, getTestProjectSyntheticsPolicy()); + comparePolicies( + packagePolicy, + getTestProjectSyntheticsPolicy({ + inputs: {}, + name: 'check if title is present-Test private location 0', + id, + configId, + }) + ); await supertest .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) @@ -1229,18 +1270,21 @@ export default function ({ getService }: FtrProviderContext) { '/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics' ); + const configId2 = monitorsResponse.body.monitors[0].id; + const id2 = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]; + const policyId2 = `${id}-${testPolicyId}`; + const packagePolicy2 = apiResponsePolicy2.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === - monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID] + - '-' + - testPolicyId + (pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId2 ); comparePolicies( packagePolicy2, getTestProjectSyntheticsPolicy({ inputs: { enabled: { value: false, type: 'bool' } }, + name: 'check if title is present-Test private location 0', + id: id2, + configId: configId2, }) ); } finally { diff --git a/x-pack/test/api_integration/apis/uptime/rest/delete_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/delete_monitor.ts index b26ed4f8972d0fc..37536c329cbb627 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/delete_monitor.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/delete_monitor.ts @@ -41,7 +41,7 @@ export default function ({ getService }: FtrProviderContext) { _httpMonitorJson = getFixtureJson('http_monitor'); await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); await supertest - .post('/api/fleet/epm/packages/synthetics/0.9.4') + .post('/api/fleet/epm/packages/synthetics/0.10.2') .set('kbn-xsrf', 'true') .send({ force: true }) .expect(200); diff --git a/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts index c1911e835fe09e4..480d07e7144f3c4 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts @@ -66,9 +66,37 @@ export default function ({ getService }: FtrProviderContext) { const updates: Partial = { [ConfigKey.URLS]: 'https://modified-host.com', [ConfigKey.NAME]: 'Modified name', + [ConfigKey.LOCATIONS]: [ + { + id: 'eu-west-01', + label: 'Europe West', + geo: { + lat: 33.2343132435, + lon: 73.2342343434, + }, + url: 'https://example-url.com', + isServiceManaged: true, + }, + ], + [ConfigKey.REQUEST_HEADERS_CHECK]: { + sampleHeader2: 'sampleValue2', + }, + [ConfigKey.METADATA]: { + script_source: { + is_generated_script: false, + file_name: 'test-file.name', + }, + }, }; - const modifiedMonitor = { ...newMonitor, ...updates }; + const modifiedMonitor = { + ...newMonitor, + ...updates, + [ConfigKey.METADATA]: { + ...newMonitor[ConfigKey.METADATA], + ...updates[ConfigKey.METADATA], + }, + }; const editResponse = await supertest .put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId) diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json index 55594300709b201..cfd4fc1b7d122f2 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json @@ -45,6 +45,7 @@ "name": "Test HTTP Monitor 03", "namespace": "testnamespace", "origin": "ui", + "form_monitor_type": "multistep", "urls": "", "url.port": null } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json index dfe6df01b81e353..5f1f9cb8af01700 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json @@ -27,7 +27,9 @@ "check.response.body.negative": [], "check.response.body.positive": [], "response.include_body": "never", - "check.response.headers": {}, + "check.request.headers": { + "sampleHeader": "sampleHeaderValue" + }, "response.include_headers": true, "check.response.status": [ "200", @@ -37,7 +39,7 @@ "value": "testValue", "type": "json" }, - "check.request.headers": {}, + "check.response.headers": {}, "check.request.method": "", "username": "test-username", "ssl.certificate_authorities": "t.string", @@ -74,5 +76,6 @@ ], "namespace": "testnamespace", "revision": 1, - "origin": "ui" + "origin": "ui", + "form_monitor_type": "http" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json index d75ecebc1e39dc4..3f2a25ea01ca058 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json @@ -34,5 +34,6 @@ ], "name": "Test HTTP Monitor 04", "namespace": "testnamespace", - "origin": "ui" + "origin": "ui", + "form_monitor_type": "icmp" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json index 2133cecfbc8a77b..1c726c1bcc70ecc 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json @@ -30,5 +30,6 @@ ], "name": "Test HTTP Monitor 04", "namespace": "testnamespace", - "origin": "ui" + "origin": "ui", + "form_monitor_type": "tcp" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/sample_data/test_policy.ts b/x-pack/test/api_integration/apis/uptime/rest/sample_data/test_policy.ts index 36012d4b66d5130..f31704302caf15a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/sample_data/test_policy.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/sample_data/test_policy.ts @@ -9,14 +9,18 @@ import { omit, sortBy } from 'lodash'; import expect from '@kbn/expect'; import { PackagePolicy } from '@kbn/fleet-plugin/common'; -export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ - id: '5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167', - version: 'WzMyNTcsMV0=', - name: '5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167', +export const getTestSyntheticsPolicy = ( + name: string, + id: string, + locationName?: string +): PackagePolicy => ({ + id: '2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', + version: 'WzE2MjYsMV0=', + name: 'test-monitor-name-Test private location 0-default', namespace: 'default', - package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.9.4' }, + package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.10.2' }, enabled: true, - policy_id: '5347cd10-0368-11ed-8df7-a7424c6f5167', + policy_id: '27337270-22ed-11ed-8c6b-09a2d21dfbc3', output_id: '', inputs: [ { @@ -48,7 +52,10 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ 'response.include_headers': { value: true, type: 'bool' }, 'response.include_body': { value: 'never', type: 'text' }, 'check.request.method': { value: '', type: 'text' }, - 'check.request.headers': { value: null, type: 'yaml' }, + 'check.request.headers': { + value: '{"sampleHeader":"sampleHeaderValue"}', + type: 'yaml', + }, 'check.request.body': { value: '"testValue"', type: 'yaml' }, 'check.response.status': { value: '["200","201"]', type: 'yaml' }, 'check.response.headers': { value: null, type: 'yaml' }, @@ -60,8 +67,13 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ 'ssl.key_passphrase': { value: 't.string', type: 'text' }, 'ssl.verification_mode': { value: 'certificate', type: 'text' }, 'ssl.supported_protocols': { value: '["TLSv1.1","TLSv1.2"]', type: 'yaml' }, + location_name: { value: locationName || 'Test private location 0', type: 'text' }, + id: { value: id, type: 'text' }, + config_id: { value: id, type: 'text' }, + run_once: { value: false, type: 'bool' }, + origin: { value: 'ui', type: 'text' }, }, - id: 'synthetics/http-http-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167', + id: 'synthetics/http-http-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', compiled_stream: { __ui: { is_tls_enabled: false, @@ -70,6 +82,8 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ }, type: 'http', name, + id, + origin: 'ui', enabled: true, urls: 'https://nextjs-test-synthetics.vercel.app/api/users', schedule: '@every 5m', @@ -82,6 +96,7 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ 'response.include_headers': true, 'response.include_body': 'never', 'check.request.method': null, + 'check.request.headers': { sampleHeader: 'sampleHeaderValue' }, 'check.request.body': 'testValue', 'check.response.status': ['200', '201'], 'ssl.certificate': 't.string', @@ -91,8 +106,18 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ 'ssl.verification_mode': 'certificate', 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2'], processors: [ - { add_observer_metadata: { geo: { name: 'Fleet managed' } } }, - { add_fields: { target: '', fields: { 'monitor.fleet_managed': true } } }, + { + add_observer_metadata: { geo: { name: locationName || 'Test private location 0' } }, + }, + { + add_fields: { + target: '', + fields: { + 'monitor.fleet_managed': true, + config_id: id, + }, + }, + }, ], }, }, @@ -126,8 +151,13 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ 'ssl.key_passphrase': { type: 'text' }, 'ssl.verification_mode': { type: 'text' }, 'ssl.supported_protocols': { type: 'yaml' }, + location_name: { value: 'Fleet managed', type: 'text' }, + id: { type: 'text' }, + config_id: { type: 'text' }, + run_once: { value: false, type: 'bool' }, + origin: { type: 'text' }, }, - id: 'synthetics/tcp-tcp-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167', + id: 'synthetics/tcp-tcp-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', }, ], }, @@ -150,8 +180,13 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ 'service.name': { type: 'text' }, timeout: { type: 'text' }, tags: { type: 'yaml' }, + location_name: { value: 'Fleet managed', type: 'text' }, + id: { type: 'text' }, + config_id: { type: 'text' }, + run_once: { value: false, type: 'bool' }, + origin: { type: 'text' }, }, - id: 'synthetics/icmp-icmp-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167', + id: 'synthetics/icmp-icmp-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', }, ], }, @@ -177,7 +212,9 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ 'source.zip_url.folder': { type: 'text' }, 'source.zip_url.password': { type: 'password' }, 'source.inline.script': { type: 'yaml' }, + 'source.project.content': { type: 'text' }, params: { type: 'yaml' }, + playwright_options: { type: 'yaml' }, screenshots: { type: 'text' }, synthetics_args: { type: 'text' }, ignore_https_errors: { type: 'bool' }, @@ -191,8 +228,15 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ 'source.zip_url.ssl.verification_mode': { type: 'text' }, 'source.zip_url.ssl.supported_protocols': { type: 'yaml' }, 'source.zip_url.proxy_url': { type: 'text' }, + location_name: { value: 'Fleet managed', type: 'text' }, + id: { type: 'text' }, + config_id: { type: 'text' }, + run_once: { value: false, type: 'bool' }, + origin: { type: 'text' }, + 'monitor.project.id': { type: 'text' }, + 'monitor.project.name': { type: 'text' }, }, - id: 'synthetics/browser-browser-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167', + id: 'synthetics/browser-browser-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', compiled_stream: { __ui: null, type: 'browser', @@ -210,7 +254,7 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ { enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.network' }, - id: 'synthetics/browser-browser.network-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167', + id: 'synthetics/browser-browser.network-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', compiled_stream: { processors: [ { add_observer_metadata: { geo: { name: 'Fleet managed' } } }, @@ -221,7 +265,7 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ { enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.screenshot' }, - id: 'synthetics/browser-browser.screenshot-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167', + id: 'synthetics/browser-browser.screenshot-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', compiled_stream: { processors: [ { add_observer_metadata: { geo: { name: 'Fleet managed' } } }, @@ -234,9 +278,9 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({ ], is_managed: true, revision: 1, - created_at: '2022-07-14T11:30:23.034Z', + created_at: '2022-08-23T14:09:17.176Z', created_by: 'system', - updated_at: '2022-07-14T11:30:23.034Z', + updated_at: '2022-08-23T14:09:17.176Z', updated_by: 'system', }); @@ -244,21 +288,27 @@ export const getTestProjectSyntheticsPolicy = ( { name, inputs = {}, + configId, + id, }: { name?: string; inputs: Record; + configId: string; + id: string; } = { name: 'check if title is present-Test private location 0', inputs: {}, + configId: '', + id: '', } ): PackagePolicy => ({ - id: 'cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b', - version: 'WzMwNTMsMV0=', - name: 'check if title is present-Test private location 0', + id: '4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', + version: 'WzEzMDksMV0=', + name: '4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-Test private location 0', namespace: 'default', - package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.9.4' }, + package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.10.2' }, enabled: true, - policy_id: '46034710-0ba6-11ed-ba04-5f123b9faa8b', + policy_id: 'd70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', output_id: '', inputs: [ { @@ -298,8 +348,13 @@ export const getTestProjectSyntheticsPolicy = ( 'ssl.key_passphrase': { type: 'text' }, 'ssl.verification_mode': { type: 'text' }, 'ssl.supported_protocols': { type: 'yaml' }, + location_name: { value: 'Fleet managed', type: 'text' }, + id: { type: 'text' }, + config_id: { type: 'text' }, + run_once: { value: false, type: 'bool' }, + origin: { type: 'text' }, }, - id: 'synthetics/http-http-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b', + id: 'synthetics/http-http-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', }, ], }, @@ -331,8 +386,13 @@ export const getTestProjectSyntheticsPolicy = ( 'ssl.key_passphrase': { type: 'text' }, 'ssl.verification_mode': { type: 'text' }, 'ssl.supported_protocols': { type: 'yaml' }, + location_name: { value: 'Fleet managed', type: 'text' }, + id: { type: 'text' }, + config_id: { type: 'text' }, + run_once: { value: false, type: 'bool' }, + origin: { type: 'text' }, }, - id: 'synthetics/tcp-tcp-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b', + id: 'synthetics/tcp-tcp-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', }, ], }, @@ -355,8 +415,13 @@ export const getTestProjectSyntheticsPolicy = ( 'service.name': { type: 'text' }, timeout: { type: 'text' }, tags: { type: 'yaml' }, + location_name: { value: 'Fleet managed', type: 'text' }, + id: { type: 'text' }, + config_id: { type: 'text' }, + run_once: { value: false, type: 'bool' }, + origin: { type: 'text' }, }, - id: 'synthetics/icmp-icmp-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b', + id: 'synthetics/icmp-icmp-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', }, ], }, @@ -386,7 +451,16 @@ export const getTestProjectSyntheticsPolicy = ( 'source.zip_url.folder': { value: '', type: 'text' }, 'source.zip_url.password': { value: '', type: 'password' }, 'source.inline.script': { value: null, type: 'yaml' }, + 'source.project.content': { + value: + 'UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA', + type: 'text', + }, params: { value: '', type: 'yaml' }, + playwright_options: { + value: '{"headless":true,"chromiumSandbox":false}', + type: 'yaml', + }, screenshots: { value: 'on', type: 'text' }, synthetics_args: { value: null, type: 'text' }, ignore_https_errors: { value: false, type: 'bool' }, @@ -400,9 +474,16 @@ export const getTestProjectSyntheticsPolicy = ( 'source.zip_url.ssl.verification_mode': { value: null, type: 'text' }, 'source.zip_url.ssl.supported_protocols': { value: null, type: 'yaml' }, 'source.zip_url.proxy_url': { value: '', type: 'text' }, + location_name: { value: 'Test private location 0', type: 'text' }, + id: { value: id, type: 'text' }, + config_id: { value: configId, type: 'text' }, + run_once: { value: false, type: 'bool' }, + origin: { value: 'project', type: 'text' }, + 'monitor.project.id': { value: 'test-suite', type: 'text' }, + 'monitor.project.name': { value: 'test-suite', type: 'text' }, ...inputs, }, - id: 'synthetics/browser-browser-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b', + id: 'synthetics/browser-browser-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', compiled_stream: { __ui: { script_source: { is_generated_script: false, file_name: '' }, @@ -410,15 +491,30 @@ export const getTestProjectSyntheticsPolicy = ( }, type: 'browser', name: 'check if title is present', + id, + origin: 'project', enabled: true, schedule: '@every 10m', timeout: null, throttling: '5d/3u/20l', + 'source.project.content': + 'UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA', + playwright_options: { headless: true, chromiumSandbox: false }, screenshots: 'on', 'filter_journeys.match': 'check if title is present', processors: [ - { add_observer_metadata: { geo: { name: 'Fleet managed' } } }, - { add_fields: { target: '', fields: { 'monitor.fleet_managed': true } } }, + { add_observer_metadata: { geo: { name: 'Test private location 0' } } }, + { + add_fields: { + target: '', + fields: { + 'monitor.fleet_managed': true, + config_id: configId, + 'monitor.project.name': 'test-suite', + 'monitor.project.id': 'test-suite', + }, + }, + }, ], ...Object.keys(inputs).reduce((acc: Record, key) => { acc[key] = inputs[key].value; @@ -429,7 +525,7 @@ export const getTestProjectSyntheticsPolicy = ( { enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.network' }, - id: 'synthetics/browser-browser.network-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b', + id: 'synthetics/browser-browser.network-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', compiled_stream: { processors: [ { add_observer_metadata: { geo: { name: 'Fleet managed' } } }, @@ -440,7 +536,7 @@ export const getTestProjectSyntheticsPolicy = ( { enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.screenshot' }, - id: 'synthetics/browser-browser.screenshot-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b', + id: 'synthetics/browser-browser.screenshot-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', compiled_stream: { processors: [ { add_observer_metadata: { geo: { name: 'Fleet managed' } } }, @@ -453,9 +549,9 @@ export const getTestProjectSyntheticsPolicy = ( ], is_managed: true, revision: 1, - created_at: '2022-07-24T23:13:55.606Z', + created_at: '2022-08-23T13:52:42.531Z', created_by: 'system', - updated_at: '2022-07-24T23:13:55.606Z', + updated_at: '2022-08-23T13:52:42.531Z', updated_by: 'system', }); diff --git a/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts b/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts new file mode 100644 index 000000000000000..96d0cbf3f8e7bd4 --- /dev/null +++ b/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { apm, createLogger, LogLevel } from '@kbn/apm-synthtrace'; +import { esTestConfig } from '@kbn/test'; +import { APM_TEST_PASSWORD } from './authentication'; +import { InheritedFtrProviderContext } from './ftr_provider_context'; + +export async function bootstrapApmSynthtrace( + context: InheritedFtrProviderContext, + kibanaServerUrl: string +) { + const es = context.getService('es'); + const kibanaVersion = esTestConfig.getVersion(); + + const kibanaClient = new apm.ApmSynthtraceKibanaClient(createLogger(LogLevel.info)); + await kibanaClient.installApmPackage( + kibanaServerUrl, + kibanaVersion, + 'elastic', + APM_TEST_PASSWORD + ); + + const esClient = new apm.ApmSynthtraceEsClient(es, createLogger(LogLevel.info), { + forceLegacyIndices: false, + refreshAfterIndex: true, + }); + + return esClient; +} diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index 3c9e99d645c7b5d..6bb177ff770b3e2 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -16,7 +16,7 @@ import { createApmUser, APM_TEST_PASSWORD, ApmUsername } from './authentication' import { APMFtrConfigName } from '../configs'; import { createApmApiClient } from './apm_api_supertest'; import { RegistryProvider } from './registry'; -import { synthtraceEsClientService } from './synthtrace_es_client_service'; +import { bootstrapApmSynthtrace } from './bootstrap_apm_synthtrace'; import { MachineLearningAPIProvider } from '../../functional/services/ml/api'; export interface ApmFtrConfig { @@ -80,7 +80,7 @@ export function createTestConfig(config: ApmFtrConfig) { const services = xPackAPITestsConfig.get('services') as InheritedServices; const servers = xPackAPITestsConfig.get('servers'); - const kibanaServer = servers.kibana; + const kibanaServer = servers.kibana as UrlObject; return { testFiles: [require.resolve('../tests')], @@ -90,7 +90,10 @@ export function createTestConfig(config: ApmFtrConfig) { ...services, apmFtrConfig: () => config, registry: RegistryProvider, - synthtraceEsClient: synthtraceEsClientService, + synthtraceEsClient: (context: InheritedFtrProviderContext) => { + const kibanaServerUrl = format(kibanaServer); + return bootstrapApmSynthtrace(context, kibanaServerUrl); + }, apmApiClient: async (context: InheritedFtrProviderContext) => { const security = context.getService('security'); const es = context.getService('es'); @@ -100,49 +103,49 @@ export function createTestConfig(config: ApmFtrConfig) { return { noAccessUser: await getApmApiClient({ - kibanaServer: servers.kibana, + kibanaServer, security, username: ApmUsername.noAccessUser, es, logger, }), readUser: await getApmApiClient({ - kibanaServer: servers.kibana, + kibanaServer, security, username: ApmUsername.viewerUser, es, logger, }), writeUser: await getApmApiClient({ - kibanaServer: servers.kibana, + kibanaServer, security, username: ApmUsername.editorUser, es, logger, }), annotationWriterUser: await getApmApiClient({ - kibanaServer: servers.kibana, + kibanaServer, security, username: ApmUsername.apmAnnotationsWriteUser, es, logger, }), noMlAccessUser: await getApmApiClient({ - kibanaServer: servers.kibana, + kibanaServer, security, username: ApmUsername.apmReadUserWithoutMlAccess, es, logger, }), manageOwnAgentKeysUser: await getApmApiClient({ - kibanaServer: servers.kibana, + kibanaServer, security, username: ApmUsername.apmManageOwnAgentKeys, es, logger, }), createAndAllAgentKeysUser: await getApmApiClient({ - kibanaServer: servers.kibana, + kibanaServer, security, username: ApmUsername.apmManageOwnAndCreateAgentKeys, es, diff --git a/x-pack/test/apm_api_integration/common/registry.ts b/x-pack/test/apm_api_integration/common/registry.ts index 51f4bb6a84e7206..7611d054852df04 100644 --- a/x-pack/test/apm_api_integration/common/registry.ts +++ b/x-pack/test/apm_api_integration/common/registry.ts @@ -15,7 +15,6 @@ import { FtrProviderContext } from './ftr_provider_context'; type ArchiveName = | 'apm_8.0.0' - | 'apm_mappings_only_8.0.0' | '8.0.0' | 'metrics_8.0.0' | 'ml_8.0.0' diff --git a/x-pack/test/apm_api_integration/common/utils/create_and_run_apm_ml_job.ts b/x-pack/test/apm_api_integration/common/utils/create_and_run_apm_ml_job.ts index 3a42456742caac4..2302fac7337f688 100644 --- a/x-pack/test/apm_api_integration/common/utils/create_and_run_apm_ml_job.ts +++ b/x-pack/test/apm_api_integration/common/utils/create_and_run_apm_ml_job.ts @@ -10,11 +10,12 @@ import datafeed from '@kbn/ml-plugin/server/models/data_recognizer/modules/apm_t import { MlApi } from '../../../functional/services/ml/api'; export function createAndRunApmMlJob({ ml, environment }: { ml: MlApi; environment: string }) { + const jobId = `apm-tx-metrics-${environment}`; return ml.createAndRunAnomalyDetectionLookbackJob( // @ts-expect-error not entire job config { ...job, - job_id: `apm-tx-metrics-${environment}`, + job_id: jobId, allow_lazy_open: false, custom_settings: { job_tags: { @@ -25,8 +26,9 @@ export function createAndRunApmMlJob({ ml, environment }: { ml: MlApi; environme }, { ...datafeed, - job_id: `apm-tx-metrics-${environment}`, - indices: ['apm-*'], + indices_options: { allow_no_indices: true }, + job_id: jobId, + indices: ['metrics-apm*', 'apm-*'], datafeed_id: `apm-tx-metrics-${environment}-datafeed`, query: { bool: { diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index d1c1c465dc576f3..43ba70cc100a15f 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -24,7 +24,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'fetching service anomalies with a trial license', - { config: 'trial', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'trial', archives: [] }, () => { const start = '2021-01-01T00:00:00.000Z'; const end = '2021-01-08T00:15:00.000Z'; diff --git a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts index b85a96c25869a92..0416a0124627af3 100644 --- a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts @@ -18,7 +18,6 @@ import { createAndRunApmMlJob } from '../../common/utils/create_and_run_apm_ml_j export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); const ml = getService('ml'); @@ -70,7 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'fetching service anomalies with a basic license', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'basic', archives: [] }, () => { it('returns a 501', async () => { const status = await statusOf( @@ -90,7 +89,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'fetching service anomalies with a trial license', - { config: 'trial', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'trial', archives: [] }, () => { const start = '2021-01-01T00:00:00.000Z'; const end = '2021-01-08T00:15:00.000Z'; diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts index fad428a96f18aa0..721f178a6413ec0 100644 --- a/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts +++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts @@ -66,139 +66,135 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Cold start rate when data is generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('without comparison', () => { - let body: ColdStartRate; - let status: number; - - before(async () => { - await generateData({ - synthtraceEsClient, - start, - end, - coldStartRate: 10, - warmStartRate: 30, - }); - const response = await callApi(); - body = response.body; - status = response.status; + registry.when('Cold start rate when data is generated', { config: 'basic', archives: [] }, () => { + describe('without comparison', () => { + let body: ColdStartRate; + let status: number; + + before(async () => { + await generateData({ + synthtraceEsClient, + start, + end, + coldStartRate: 10, + warmStartRate: 30, }); + const response = await callApi(); + body = response.body; + status = response.status; + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - it('returns correct HTTP status', () => { - expect(status).to.be(200); - }); + it('returns correct HTTP status', () => { + expect(status).to.be(200); + }); - it('returns an array of transaction cold start rates', () => { - expect(body).to.have.property('currentPeriod'); - expect(body.currentPeriod.transactionColdstartRate).to.have.length(15); - expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be( - true - ); - }); + it('returns an array of transaction cold start rates', () => { + expect(body).to.have.property('currentPeriod'); + expect(body.currentPeriod.transactionColdstartRate).to.have.length(15); + expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be( + true + ); + }); - it('returns correct average rate', () => { - expect(body.currentPeriod.average).to.be(0.25); - }); + it('returns correct average rate', () => { + expect(body.currentPeriod.average).to.be(0.25); + }); - it("doesn't have data for the previous period", () => { - expect(body).to.have.property('previousPeriod'); - expect(body.previousPeriod.transactionColdstartRate).to.have.length(0); - expect(body.previousPeriod.average).to.be(null); - }); + it("doesn't have data for the previous period", () => { + expect(body).to.have.property('previousPeriod'); + expect(body.previousPeriod.transactionColdstartRate).to.have.length(0); + expect(body.previousPeriod.average).to.be(null); }); + }); - describe('with comparison', () => { - let body: ColdStartRate; - let status: number; - - before(async () => { - const startDate = moment(start).add(6, 'minutes'); - const endDate = moment(start).add(9, 'minutes'); - const comparisonStartDate = new Date(start); - const comparisonEndDate = moment(start).add(3, 'minutes'); - - await generateData({ - synthtraceEsClient, - start: startDate.valueOf(), - end: endDate.valueOf(), - coldStartRate: 10, - warmStartRate: 30, - }); - await generateData({ - synthtraceEsClient, - start: comparisonStartDate.getTime(), - end: comparisonEndDate.valueOf(), - coldStartRate: 20, - warmStartRate: 20, - }); - - const response = await callApi({ - query: { - start: startDate.toISOString(), - end: endDate.subtract(1, 'seconds').toISOString(), - offset: '6m', - }, - }); - body = response.body; - status = response.status; + describe('with comparison', () => { + let body: ColdStartRate; + let status: number; + + before(async () => { + const startDate = moment(start).add(6, 'minutes'); + const endDate = moment(start).add(9, 'minutes'); + const comparisonStartDate = new Date(start); + const comparisonEndDate = moment(start).add(3, 'minutes'); + + await generateData({ + synthtraceEsClient, + start: startDate.valueOf(), + end: endDate.valueOf(), + coldStartRate: 10, + warmStartRate: 30, }); - - after(() => synthtraceEsClient.clean()); - - it('returns correct HTTP status', () => { - expect(status).to.be(200); + await generateData({ + synthtraceEsClient, + start: comparisonStartDate.getTime(), + end: comparisonEndDate.valueOf(), + coldStartRate: 20, + warmStartRate: 20, }); - it('returns some data', () => { - expect(body.currentPeriod.average).not.to.be(null); - expect(body.currentPeriod.transactionColdstartRate.length).to.be.greaterThan(0); - const hasCurrentPeriodData = body.currentPeriod.transactionColdstartRate.some(({ y }) => - isFiniteNumber(y) - ); - expect(hasCurrentPeriodData).to.equal(true); - - expect(body.previousPeriod.average).not.to.be(null); - expect(body.previousPeriod.transactionColdstartRate.length).to.be.greaterThan(0); - const hasPreviousPeriodData = body.previousPeriod.transactionColdstartRate.some(({ y }) => - isFiniteNumber(y) - ); - expect(hasPreviousPeriodData).to.equal(true); + const response = await callApi({ + query: { + start: startDate.toISOString(), + end: endDate.subtract(1, 'seconds').toISOString(), + offset: '6m', + }, }); + body = response.body; + status = response.status; + }); - it('has same start time for both periods', () => { - expect(first(body.currentPeriod.transactionColdstartRate)?.x).to.equal( - first(body.previousPeriod.transactionColdstartRate)?.x - ); - }); + after(() => synthtraceEsClient.clean()); - it('has same end time for both periods', () => { - expect(last(body.currentPeriod.transactionColdstartRate)?.x).to.equal( - last(body.previousPeriod.transactionColdstartRate)?.x - ); - }); + it('returns correct HTTP status', () => { + expect(status).to.be(200); + }); - it('returns an array of transaction cold start rates', () => { - expect(body.currentPeriod.transactionColdstartRate).to.have.length(3); - expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be( - true - ); + it('returns some data', () => { + expect(body.currentPeriod.average).not.to.be(null); + expect(body.currentPeriod.transactionColdstartRate.length).to.be.greaterThan(0); + const hasCurrentPeriodData = body.currentPeriod.transactionColdstartRate.some(({ y }) => + isFiniteNumber(y) + ); + expect(hasCurrentPeriodData).to.equal(true); + + expect(body.previousPeriod.average).not.to.be(null); + expect(body.previousPeriod.transactionColdstartRate.length).to.be.greaterThan(0); + const hasPreviousPeriodData = body.previousPeriod.transactionColdstartRate.some(({ y }) => + isFiniteNumber(y) + ); + expect(hasPreviousPeriodData).to.equal(true); + }); - expect(body.previousPeriod.transactionColdstartRate).to.have.length(3); - expect(body.previousPeriod.transactionColdstartRate.every(({ y }) => y === 0.5)).to.be( - true - ); - }); + it('has same start time for both periods', () => { + expect(first(body.currentPeriod.transactionColdstartRate)?.x).to.equal( + first(body.previousPeriod.transactionColdstartRate)?.x + ); + }); - it('has same average value for both periods', () => { - expect(body.currentPeriod.average).to.be(0.25); - expect(body.previousPeriod.average).to.be(0.5); - }); + it('has same end time for both periods', () => { + expect(last(body.currentPeriod.transactionColdstartRate)?.x).to.equal( + last(body.previousPeriod.transactionColdstartRate)?.x + ); }); - } - ); + + it('returns an array of transaction cold start rates', () => { + expect(body.currentPeriod.transactionColdstartRate).to.have.length(3); + expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be( + true + ); + + expect(body.previousPeriod.transactionColdstartRate).to.have.length(3); + expect(body.previousPeriod.transactionColdstartRate.every(({ y }) => y === 0.5)).to.be( + true + ); + }); + + it('has same average value for both periods', () => { + expect(body.currentPeriod.average).to.be(0.25); + expect(body.previousPeriod.average).to.be(0.5); + }); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts index ed432f58d47aaf0..25e4cd208aeb02a 100644 --- a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts +++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts @@ -70,7 +70,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'Cold start rate by transaction name when data is generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'basic', archives: [] }, () => { describe('without comparison', () => { let body: ColdStartRate; diff --git a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts index efe8f37f1c5dce7..a77a2e443b9d5e3 100644 --- a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts +++ b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts @@ -59,79 +59,75 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'mappings exists', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is generated', () => { - let response: SupertestReturnType<'POST /internal/apm/data_view/static'>; + registry.when('mappings exists', { config: 'basic', archives: [] }, () => { + describe('when data is generated', () => { + let response: SupertestReturnType<'POST /internal/apm/data_view/static'>; + + before(async () => { + await generateApmData(synthtrace); + response = await createDataViewViaApmApi(); + }); + + after(async () => { + await deleteDataView(); + await synthtrace.clean(); + }); + + it('successfully creates the apm data view', async () => { + expect(response.status).to.be(200); + + expect(response.body.dataView!.id).to.be('apm_static_index_pattern_id'); + expect(response.body.dataView!.name).to.be('APM'); + expect(response.body.dataView!.title).to.be( + 'traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*' + ); + }); + + describe('when fetching the data view', async () => { + let resBody: any; before(async () => { - await generateApmData(synthtrace); - response = await createDataViewViaApmApi(); + const res = await getDataView().expect(200); + resBody = res.body; }); - after(async () => { - await deleteDataView(); - await synthtrace.clean(); + it('has correct id', () => { + expect(resBody.id).to.be('apm_static_index_pattern_id'); }); - it('successfully creates the apm data view', async () => { - expect(response.status).to.be(200); - - expect(response.body.dataView!.id).to.be('apm_static_index_pattern_id'); - expect(response.body.dataView!.name).to.be('APM'); - expect(response.body.dataView!.title).to.be( - 'traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*' - ); + it('has correct title', () => { + expect(resBody.attributes.title).to.be(dataViewPattern); }); - describe('when fetching the data view', async () => { - let resBody: any; - - before(async () => { - const res = await getDataView().expect(200); - resBody = res.body; - }); - - it('has correct id', () => { - expect(resBody.id).to.be('apm_static_index_pattern_id'); - }); - - it('has correct title', () => { - expect(resBody.attributes.title).to.be(dataViewPattern); - }); - - it('has correct attributes', () => { - expect(resBody.attributes.fieldFormatMap).to.be( - JSON.stringify({ - 'trace.id': { - id: 'url', - params: { - urlTemplate: 'apm/link-to/trace/{{value}}', - labelTemplate: '{{value}}', - }, + it('has correct attributes', () => { + expect(resBody.attributes.fieldFormatMap).to.be( + JSON.stringify({ + 'trace.id': { + id: 'url', + params: { + urlTemplate: 'apm/link-to/trace/{{value}}', + labelTemplate: '{{value}}', }, - 'transaction.id': { - id: 'url', - params: { - urlTemplate: 'apm/link-to/transaction/{{value}}', - labelTemplate: '{{value}}', - }, + }, + 'transaction.id': { + id: 'url', + params: { + urlTemplate: 'apm/link-to/transaction/{{value}}', + labelTemplate: '{{value}}', }, - }) - ); - }); - - // this test ensures that the default APM Data View doesn't interfere with suggestions returned in the kuery bar (this has been a problem in the past) - it('can get suggestions for `trace.id`', async () => { - const suggestions = await getDataViewSuggestions('trace.id'); - expect(suggestions.body.length).to.be(10); - }); + }, + }) + ); + }); + + // this test ensures that the default APM Data View doesn't interfere with suggestions returned in the kuery bar (this has been a problem in the past) + it('can get suggestions for `trace.id`', async () => { + const suggestions = await getDataViewSuggestions('trace.id'); + expect(suggestions.body.length).to.be(10); }); }); - } - ); + }); + }); } function generateApmData(synthtrace: ApmSynthtraceEsClient) { diff --git a/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts index 686f6ba313d7e51..7d8dab710d7e0e6 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts @@ -94,180 +94,125 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Dependency metrics when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - before(async () => { - await generateOperationData({ - synthtraceEsClient, - start, - end, - }); + registry.when('Dependency metrics when data is loaded', { config: 'basic', archives: [] }, () => { + before(async () => { + await generateOperationData({ + synthtraceEsClient, + start, + end, }); + }); - describe('without spanName', () => { - describe('without a kuery or environment', () => { - it('returns the correct latency', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'latency', - }); - - const searchRate = - ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; - const bulkRate = ES_BULK_RATE; - - expect(avg(response.body.currentTimeseries)).to.eql( - roundNumber( - ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / - (searchRate + bulkRate)) * - 1000 - ) - ); - }); - - it('returns the correct throughput', async () => { - const response = await callApi({ - dependencyName: 'redis', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'throughput', - }); - - expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); + describe('without spanName', () => { + describe('without a kuery or environment', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'latency', }); - it('returns the correct failure rate', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'error_rate', - }); + const searchRate = + ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; - const expectedErrorRate = - ES_SEARCH_FAILURE_RATE / (ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE); + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); - expect(avg(response.body.currentTimeseries)).to.eql(expectedErrorRate); + it('returns the correct throughput', async () => { + const response = await callApi({ + dependencyName: 'redis', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'throughput', }); + + expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); }); - describe('with a kuery', () => { - it('returns the correct latency', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'latency', - kuery: `event.outcome:unknown`, - }); - - const searchRate = ES_SEARCH_UNKNOWN_RATE; - const bulkRate = ES_BULK_RATE; - - expect(avg(response.body.currentTimeseries)).to.eql( - roundNumber( - ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / - (searchRate + bulkRate)) * - 1000 - ) - ); + it('returns the correct failure rate', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'error_rate', }); - it('returns the correct throughput', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'throughput', - kuery: `event.outcome:unknown`, - }); + const expectedErrorRate = + ES_SEARCH_FAILURE_RATE / (ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE); - const searchRate = ES_SEARCH_UNKNOWN_RATE; - const bulkRate = ES_BULK_RATE; + expect(avg(response.body.currentTimeseries)).to.eql(expectedErrorRate); + }); + }); - expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); + describe('with a kuery', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'latency', + kuery: `event.outcome:unknown`, }); - it('returns the correct failure rate', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'error_rate', - kuery: 'event.outcome:success', - }); + const searchRate = ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; - expect(avg(response.body.currentTimeseries)).to.eql(0); - }); + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); }); - describe('with an environment', () => { - it('returns the correct latency', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'latency', - environment: 'production', - }); - - const searchRate = ES_SEARCH_UNKNOWN_RATE; - const bulkRate = 0; - - expect(avg(response.body.currentTimeseries)).to.eql( - roundNumber( - ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / - (searchRate + bulkRate)) * - 1000 - ) - ); + it('returns the correct throughput', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'throughput', + kuery: `event.outcome:unknown`, }); - it('returns the correct throughput', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'throughput', - environment: 'production', - }); + const searchRate = ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; - const searchRate = - ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; - const bulkRate = 0; + expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); + }); - expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); + it('returns the correct failure rate', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'error_rate', + kuery: 'event.outcome:success', }); - it('returns the correct failure rate', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'error_rate', - environment: 'development', - }); - - expect(avg(response.body.currentTimeseries)).to.eql(null); - }); + expect(avg(response.body.currentTimeseries)).to.eql(0); }); }); - describe('with spanName', () => { + describe('with an environment', () => { it('returns the correct latency', async () => { const response = await callApi({ dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: false, - spanName: '/_search', + searchServiceDestinationMetrics: true, + spanName: '', metric: 'latency', + environment: 'production', }); - const searchRate = - ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const searchRate = ES_SEARCH_UNKNOWN_RATE; const bulkRate = 0; expect(avg(response.body.currentTimeseries)).to.eql( @@ -281,28 +226,78 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the correct throughput', async () => { const response = await callApi({ - dependencyName: 'redis', - searchServiceDestinationMetrics: false, - spanName: 'SET', + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', metric: 'throughput', + environment: 'production', }); - expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); + const searchRate = + ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = 0; + + expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); }); it('returns the correct failure rate', async () => { const response = await callApi({ dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: false, - spanName: '/_bulk', + searchServiceDestinationMetrics: true, + spanName: '', metric: 'error_rate', + environment: 'development', }); expect(avg(response.body.currentTimeseries)).to.eql(null); }); }); + }); - after(() => synthtraceEsClient.clean()); - } - ); + describe('with spanName', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: false, + spanName: '/_search', + metric: 'latency', + }); + + const searchRate = ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = 0; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + dependencyName: 'redis', + searchServiceDestinationMetrics: false, + spanName: 'SET', + metric: 'throughput', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: false, + spanName: '/_bulk', + metric: 'error_rate', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(null); + }); + }); + + after(() => synthtraceEsClient.clean()); + }); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts index 0d7e4f01733e2d1..006a67f3f772477 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts @@ -44,7 +44,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'Dependency metadata when data is generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'basic', archives: [] }, () => { after(() => synthtraceEsClient.clean()); diff --git a/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts index e5d3b8a43608663..69e61e52ca1a421 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts @@ -47,33 +47,29 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Dependency for services', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded', () => { - before(async () => { - await generateData({ synthtraceEsClient, start, end }); - }); - after(() => synthtraceEsClient.clean()); + registry.when('Dependency for services', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + }); + after(() => synthtraceEsClient.clean()); - it('returns a list of dependencies for a service', async () => { - const { status, body } = await callApi(); + it('returns a list of dependencies for a service', async () => { + const { status, body } = await callApi(); - expect(status).to.be(200); - expect( - body.serviceDependencies.map( - ({ location }) => (location as DependencyNode).dependencyName - ) - ).to.eql([dependencyName]); + expect(status).to.be(200); + expect( + body.serviceDependencies.map( + ({ location }) => (location as DependencyNode).dependencyName + ) + ).to.eql([dependencyName]); - const currentStatsLatencyValues = - body.serviceDependencies[0].currentStats.latency.timeseries; - expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); - }); + const currentStatsLatencyValues = + body.serviceDependencies[0].currentStats.latency.timeseries; + expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); }); - } - ); + }); + }); registry.when( 'Dependency for service breakdown when data is not loaded', @@ -88,31 +84,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Dependency for services breakdown', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded', () => { - before(async () => { - await generateData({ synthtraceEsClient, start, end }); - }); - after(() => synthtraceEsClient.clean()); + registry.when('Dependency for services breakdown', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + }); + after(() => synthtraceEsClient.clean()); - it('returns a list of dependencies for a service', async () => { - const { status, body } = await callApi(); + it('returns a list of dependencies for a service', async () => { + const { status, body } = await callApi(); - expect(status).to.be(200); - expect( - body.serviceDependencies.map( - ({ location }) => (location as DependencyNode).dependencyName - ) - ).to.eql([dependencyName]); + expect(status).to.be(200); + expect( + body.serviceDependencies.map( + ({ location }) => (location as DependencyNode).dependencyName + ) + ).to.eql([dependencyName]); - const currentStatsLatencyValues = - body.serviceDependencies[0].currentStats.latency.timeseries; - expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); - }); + const currentStatsLatencyValues = + body.serviceDependencies[0].currentStats.latency.timeseries; + expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts index 5160ea52b094e64..c0c67d9bb8f1924 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts @@ -49,94 +49,90 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Top dependencies', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is generated', () => { - let topDependencies: TopDependencies; + registry.when('Top dependencies', { config: 'basic', archives: [] }, () => { + describe('when data is generated', () => { + let topDependencies: TopDependencies; + + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + const response = await callApi(); + topDependencies = response.body; + }); + + after(() => synthtraceEsClient.clean()); + + it('returns an array of dependencies', () => { + expect(topDependencies).to.have.property('dependencies'); + expect(topDependencies.dependencies).to.have.length(1); + }); - before(async () => { - await generateData({ synthtraceEsClient, start, end }); - const response = await callApi(); - topDependencies = response.body; + it('returns correct dependency information', () => { + const location = topDependencies.dependencies[0].location as DependencyNode; + const { span } = dataConfig; + + expect(location.type).to.be(NodeType.dependency); + expect(location.dependencyName).to.be(span.destination); + expect(location.spanType).to.be(span.type); + expect(location.spanSubtype).to.be(span.subType); + expect(location).to.have.property('id'); + }); + + describe('returns the correct stats', () => { + let dependencies: TopDependencies['dependencies'][number]; + + before(() => { + dependencies = topDependencies.dependencies[0]; }); - after(() => synthtraceEsClient.clean()); + it("doesn't have previous stats", () => { + expect(dependencies.previousStats).to.be(null); + }); - it('returns an array of dependencies', () => { - expect(topDependencies).to.have.property('dependencies'); - expect(topDependencies.dependencies).to.have.length(1); + it('has an "impact" property', () => { + expect(dependencies.currentStats).to.have.property('impact'); }); - it('returns correct dependency information', () => { - const location = topDependencies.dependencies[0].location as DependencyNode; - const { span } = dataConfig; + it('returns the correct latency', () => { + const { + currentStats: { latency }, + } = dependencies; - expect(location.type).to.be(NodeType.dependency); - expect(location.dependencyName).to.be(span.destination); - expect(location.spanType).to.be(span.type); - expect(location.spanSubtype).to.be(span.subType); - expect(location).to.have.property('id'); + const { transaction } = dataConfig; + + expect(latency.value).to.be(transaction.duration * 1000); + expect(latency.timeseries.every(({ y }) => y === transaction.duration * 1000)).to.be( + true + ); + }); + + it('returns the correct throughput', () => { + const { + currentStats: { throughput }, + } = dependencies; + const { rate } = dataConfig; + + expect(roundNumber(throughput.value)).to.be(roundNumber(rate)); + }); + + it('returns the correct total time', () => { + const { + currentStats: { totalTime }, + } = dependencies; + const { rate, transaction } = dataConfig; + + expect( + totalTime.timeseries.every(({ y }) => y === rate * transaction.duration * 1000) + ).to.be(true); }); - describe('returns the correct stats', () => { - let dependencies: TopDependencies['dependencies'][number]; - - before(() => { - dependencies = topDependencies.dependencies[0]; - }); - - it("doesn't have previous stats", () => { - expect(dependencies.previousStats).to.be(null); - }); - - it('has an "impact" property', () => { - expect(dependencies.currentStats).to.have.property('impact'); - }); - - it('returns the correct latency', () => { - const { - currentStats: { latency }, - } = dependencies; - - const { transaction } = dataConfig; - - expect(latency.value).to.be(transaction.duration * 1000); - expect(latency.timeseries.every(({ y }) => y === transaction.duration * 1000)).to.be( - true - ); - }); - - it('returns the correct throughput', () => { - const { - currentStats: { throughput }, - } = dependencies; - const { rate } = dataConfig; - - expect(roundNumber(throughput.value)).to.be(roundNumber(rate)); - }); - - it('returns the correct total time', () => { - const { - currentStats: { totalTime }, - } = dependencies; - const { rate, transaction } = dataConfig; - - expect( - totalTime.timeseries.every(({ y }) => y === rate * transaction.duration * 1000) - ).to.be(true); - }); - - it('returns the correct error rate', () => { - const { - currentStats: { errorRate }, - } = dependencies; - expect(errorRate.value).to.be(0); - expect(errorRate.timeseries.every(({ y }) => y === 0)).to.be(true); - }); + it('returns the correct error rate', () => { + const { + currentStats: { errorRate }, + } = dependencies; + expect(errorRate.value).to.be(0); + expect(errorRate.timeseries.every(({ y }) => y === 0)).to.be(true); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts index 1040bd58417f502..8d8de18eb6e0137 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts @@ -65,154 +65,150 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'Top operations when data is generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - before(() => - generateOperationData({ - synthtraceEsClient, - start, - end, - }) - ); - - after(() => synthtraceEsClient.clean()); - - describe('requested for elasticsearch', () => { - let response: TopOperations; - let searchOperation: ValuesType; - let bulkOperation: ValuesType; - - before(async () => { - response = await callApi({ dependencyName: 'elasticsearch' }); - searchOperation = response.find((op) => op.spanName === '/_search')!; - bulkOperation = response.find((op) => op.spanName === '/_bulk')!; - }); + registry.when('Top operations when data is generated', { config: 'basic', archives: [] }, () => { + before(() => + generateOperationData({ + synthtraceEsClient, + start, + end, + }) + ); - it('returns the correct operations', () => { - expect(response.length).to.eql(2); + after(() => synthtraceEsClient.clean()); - expect(searchOperation).to.be.ok(); - expect(bulkOperation).to.be.ok(); - }); + describe('requested for elasticsearch', () => { + let response: TopOperations; + let searchOperation: ValuesType; + let bulkOperation: ValuesType; - it('returns the correct latency', () => { - expect(searchOperation.latency).to.eql(ES_SEARCH_DURATION * 1000); - expect(bulkOperation.latency).to.eql(ES_BULK_DURATION * 1000); - }); + before(async () => { + response = await callApi({ dependencyName: 'elasticsearch' }); + searchOperation = response.find((op) => op.spanName === '/_search')!; + bulkOperation = response.find((op) => op.spanName === '/_bulk')!; + }); - it('returns the correct throughput', () => { - const expectedSearchThroughput = roundNumber( - ES_SEARCH_UNKNOWN_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE - ); - const expectedBulkThroughput = ES_BULK_RATE; + it('returns the correct operations', () => { + expect(response.length).to.eql(2); - expect(roundNumber(searchOperation.throughput)).to.eql(expectedSearchThroughput); - expect(roundNumber(bulkOperation.throughput)).to.eql(expectedBulkThroughput); + expect(searchOperation).to.be.ok(); + expect(bulkOperation).to.be.ok(); + }); - expect( - searchOperation.timeseries.throughput - .map((bucket) => bucket.y) - .every((val) => val === expectedSearchThroughput) - ); - }); + it('returns the correct latency', () => { + expect(searchOperation.latency).to.eql(ES_SEARCH_DURATION * 1000); + expect(bulkOperation.latency).to.eql(ES_BULK_DURATION * 1000); + }); - it('returns the correct failure rate', () => { - const expectedSearchFailureRate = - ES_SEARCH_FAILURE_RATE / (ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE); - const expectedBulkFailureRate = null; + it('returns the correct throughput', () => { + const expectedSearchThroughput = roundNumber( + ES_SEARCH_UNKNOWN_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE + ); + const expectedBulkThroughput = ES_BULK_RATE; - expect(searchOperation.failureRate).to.be(expectedSearchFailureRate); + expect(roundNumber(searchOperation.throughput)).to.eql(expectedSearchThroughput); + expect(roundNumber(bulkOperation.throughput)).to.eql(expectedBulkThroughput); - expect(bulkOperation.failureRate).to.be(expectedBulkFailureRate); + expect( + searchOperation.timeseries.throughput + .map((bucket) => bucket.y) + .every((val) => val === expectedSearchThroughput) + ); + }); - expect( - searchOperation.timeseries.failureRate - .map((bucket) => bucket.y) - .every((val) => val === expectedSearchFailureRate) - ); + it('returns the correct failure rate', () => { + const expectedSearchFailureRate = + ES_SEARCH_FAILURE_RATE / (ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE); + const expectedBulkFailureRate = null; - expect( - bulkOperation.timeseries.failureRate - .map((bucket) => bucket.y) - .every((val) => val === expectedBulkFailureRate) - ); - }); + expect(searchOperation.failureRate).to.be(expectedSearchFailureRate); - it('returns the correct impact', () => { - expect(searchOperation.impact).to.eql(0); - expect(bulkOperation.impact).to.eql(100); - }); + expect(bulkOperation.failureRate).to.be(expectedBulkFailureRate); + + expect( + searchOperation.timeseries.failureRate + .map((bucket) => bucket.y) + .every((val) => val === expectedSearchFailureRate) + ); + + expect( + bulkOperation.timeseries.failureRate + .map((bucket) => bucket.y) + .every((val) => val === expectedBulkFailureRate) + ); }); - describe('requested for redis', () => { - let response: TopOperations; - let setOperation: ValuesType; + it('returns the correct impact', () => { + expect(searchOperation.impact).to.eql(0); + expect(bulkOperation.impact).to.eql(100); + }); + }); - before(async () => { - response = await callApi({ dependencyName: 'redis' }); - setOperation = response.find((op) => op.spanName === 'SET')!; - }); + describe('requested for redis', () => { + let response: TopOperations; + let setOperation: ValuesType; - it('returns the correct operations', () => { - expect(response.length).to.eql(1); + before(async () => { + response = await callApi({ dependencyName: 'redis' }); + setOperation = response.find((op) => op.spanName === 'SET')!; + }); - expect(setOperation).to.be.ok(); - }); + it('returns the correct operations', () => { + expect(response.length).to.eql(1); - it('returns the correct latency', () => { - expect(setOperation.latency).to.eql(REDIS_SET_DURATION * 1000); - }); + expect(setOperation).to.be.ok(); + }); - it('returns the correct throughput', () => { - expect(roundNumber(setOperation.throughput)).to.eql(roundNumber(REDIS_SET_RATE)); - }); + it('returns the correct latency', () => { + expect(setOperation.latency).to.eql(REDIS_SET_DURATION * 1000); }); - describe('requested for a specific service', () => { - let response: TopOperations; - let searchOperation: ValuesType; - let bulkOperation: ValuesType | undefined; - - before(async () => { - response = await callApi({ - dependencyName: 'elasticsearch', - kuery: `service.name:"synth-go"`, - }); - searchOperation = response.find((op) => op.spanName === '/_search')!; - bulkOperation = response.find((op) => op.spanName === '/_bulk'); - }); + it('returns the correct throughput', () => { + expect(roundNumber(setOperation.throughput)).to.eql(roundNumber(REDIS_SET_RATE)); + }); + }); - it('returns the correct operations', () => { - expect(response.length).to.eql(1); + describe('requested for a specific service', () => { + let response: TopOperations; + let searchOperation: ValuesType; + let bulkOperation: ValuesType | undefined; - expect(searchOperation).to.be.ok(); - expect(bulkOperation).not.to.be.ok(); + before(async () => { + response = await callApi({ + dependencyName: 'elasticsearch', + kuery: `service.name:"synth-go"`, }); + searchOperation = response.find((op) => op.spanName === '/_search')!; + bulkOperation = response.find((op) => op.spanName === '/_bulk'); }); - describe('requested for a specific environment', () => { - let response: TopOperations; - let searchOperation: ValuesType | undefined; - let bulkOperation: ValuesType; - - before(async () => { - response = await callApi({ - dependencyName: 'elasticsearch', - environment: 'development', - }); - searchOperation = response.find((op) => op.spanName === '/_search'); - bulkOperation = response.find((op) => op.spanName === '/_bulk')!; - }); + it('returns the correct operations', () => { + expect(response.length).to.eql(1); - it('returns the correct operations', () => { - expect(response.length).to.eql(1); + expect(searchOperation).to.be.ok(); + expect(bulkOperation).not.to.be.ok(); + }); + }); - expect(searchOperation).not.to.be.ok(); - expect(bulkOperation).to.be.ok(); + describe('requested for a specific environment', () => { + let response: TopOperations; + let searchOperation: ValuesType | undefined; + let bulkOperation: ValuesType; + + before(async () => { + response = await callApi({ + dependencyName: 'elasticsearch', + environment: 'development', }); + searchOperation = response.find((op) => op.spanName === '/_search'); + bulkOperation = response.find((op) => op.spanName === '/_bulk')!; + }); + + it('returns the correct operations', () => { + expect(response.length).to.eql(1); + + expect(searchOperation).not.to.be.ok(); + expect(bulkOperation).to.be.ok(); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts index 97d9846e06f57ca..06890c0b6fd592a 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts @@ -68,7 +68,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'Top dependency spans when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'basic', archives: [] }, () => { const javaInstance = apm.service('java', 'production', 'java').instance('instance-a'); diff --git a/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts index 2d3a51054541bd6..b2ff45685de2e5d 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts @@ -47,28 +47,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Dependency upstream services', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded', () => { - before(async () => { - await generateData({ synthtraceEsClient, start, end }); - }); - after(() => synthtraceEsClient.clean()); + registry.when('Dependency upstream services', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + }); + after(() => synthtraceEsClient.clean()); - it('returns a list of upstream services for the dependency', async () => { - const { status, body } = await callApi(); + it('returns a list of upstream services for the dependency', async () => { + const { status, body } = await callApi(); - expect(status).to.be(200); - expect(body.services.map(({ location }) => (location as ServiceNode).serviceName)).to.eql( - ['synth-go'] - ); + expect(status).to.be(200); + expect(body.services.map(({ location }) => (location as ServiceNode).serviceName)).to.eql([ + 'synth-go', + ]); - const currentStatsLatencyValues = body.services[0].currentStats.latency.timeseries; - expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); - }); + const currentStatsLatencyValues = body.services[0].currentStats.latency.timeseries; + expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts index 23cc77320b0ebc0..fe2f9c8cd2cf808 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts @@ -114,7 +114,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let errorRateMetricValues: Awaited>; let errorTransactionValues: Awaited>; - registry.when('Services APIs', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { + registry.when('Services APIs', { config: 'basic', archives: [] }, () => { describe('when data is loaded ', () => { const GO_PROD_LIST_RATE = 75; const GO_PROD_LIST_ERROR_RATE = 25; diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts index 5374ddefc946e42..8c14a203f78007e 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts @@ -60,87 +60,83 @@ export default function ApiTest({ getService }: FtrProviderContext) { let errorRateMetricValues: Awaited>; let errorTransactionValues: Awaited>; - registry.when( - 'Service maps APIs', - { config: 'trial', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded ', () => { - const GO_PROD_LIST_RATE = 75; - const GO_PROD_LIST_ERROR_RATE = 25; - const GO_PROD_ID_RATE = 50; - const GO_PROD_ID_ERROR_RATE = 50; - before(async () => { - const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') - .instance('instance-a'); + registry.when('Service maps APIs', { config: 'trial', archives: [] }, () => { + describe('when data is loaded ', () => { + const GO_PROD_LIST_RATE = 75; + const GO_PROD_LIST_ERROR_RATE = 25; + const GO_PROD_ID_RATE = 50; + const GO_PROD_ID_ERROR_RATE = 50; + before(async () => { + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); - const transactionNameProductList = 'GET /api/product/list'; - const transactionNameProductId = 'GET /api/product/:id'; + const transactionNameProductList = 'GET /api/product/list'; + const transactionNameProductId = 'GET /api/product/:id'; - await synthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_LIST_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction(transactionNameProductList, 'Worker') - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(GO_PROD_LIST_ERROR_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction(transactionNameProductList, 'Worker') - .duration(1000) - .timestamp(timestamp) - .failure() - ), - timerange(start, end) - .interval('1m') - .rate(GO_PROD_ID_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction(transactionNameProductId) - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(GO_PROD_ID_ERROR_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction(transactionNameProductId) - .duration(1000) - .timestamp(timestamp) - .failure() - ), - ]); - }); + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_LIST_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction(transactionNameProductList, 'Worker') + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(GO_PROD_LIST_ERROR_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction(transactionNameProductList, 'Worker') + .duration(1000) + .timestamp(timestamp) + .failure() + ), + timerange(start, end) + .interval('1m') + .rate(GO_PROD_ID_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction(transactionNameProductId) + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(GO_PROD_ID_ERROR_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction(transactionNameProductId) + .duration(1000) + .timestamp(timestamp) + .failure() + ), + ]); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - describe('compare latency value between service inventory and service maps', () => { - before(async () => { - [errorTransactionValues, errorRateMetricValues] = await Promise.all([ - getErrorRateValues('transaction'), - getErrorRateValues('metric'), - ]); - }); + describe('compare latency value between service inventory and service maps', () => { + before(async () => { + [errorTransactionValues, errorRateMetricValues] = await Promise.all([ + getErrorRateValues('transaction'), + getErrorRateValues('metric'), + ]); + }); - it('returns same avg error rate value for Transaction-based and Metric-based data', () => { - [ - errorTransactionValues.serviceInventoryErrorRate, - errorTransactionValues.serviceMapsNodeDetailsErrorRate, - errorRateMetricValues.serviceInventoryErrorRate, - errorRateMetricValues.serviceMapsNodeDetailsErrorRate, - ].forEach((value) => expect(value).to.be.equal(GO_PROD_ID_ERROR_RATE / 100)); - }); + it('returns same avg error rate value for Transaction-based and Metric-based data', () => { + [ + errorTransactionValues.serviceInventoryErrorRate, + errorTransactionValues.serviceMapsNodeDetailsErrorRate, + errorRateMetricValues.serviceInventoryErrorRate, + errorRateMetricValues.serviceMapsNodeDetailsErrorRate, + ].forEach((value) => expect(value).to.be.equal(GO_PROD_ID_ERROR_RATE / 100)); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts b/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts index 4cd338441eefa5c..6d57addcc7bb7e5 100644 --- a/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts @@ -60,128 +60,124 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('errors distribution', () => { - const { appleTransaction, bananaTransaction } = config; + registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { + describe('errors distribution', () => { + const { appleTransaction, bananaTransaction } = config; + before(async () => { + await generateData({ serviceName, start, end, synthtraceEsClient }); + }); + + after(() => synthtraceEsClient.clean()); + + describe('without comparison', () => { + let errorsDistribution: ErrorsDistribution; before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + const response = await callApi(); + errorsDistribution = response.body; }); - after(() => synthtraceEsClient.clean()); - - describe('without comparison', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const response = await callApi(); - errorsDistribution = response.body; - }); + it('displays combined number of occurrences', () => { + const countSum = sumBy(errorsDistribution.currentPeriod, 'y'); + const numberOfBuckets = 15; + expect(countSum).to.equal( + (appleTransaction.failureRate + bananaTransaction.failureRate) * numberOfBuckets + ); + }); + }); - it('displays combined number of occurrences', () => { - const countSum = sumBy(errorsDistribution.currentPeriod, 'y'); - const numberOfBuckets = 15; - expect(countSum).to.equal( - (appleTransaction.failureRate + bananaTransaction.failureRate) * numberOfBuckets - ); + describe('displays occurrences for type "apple transaction" only', () => { + let errorsDistribution: ErrorsDistribution; + before(async () => { + const response = await callApi({ + query: { kuery: `error.exception.type:"${appleTransaction.name}"` }, }); + errorsDistribution = response.body; }); + it('displays combined number of occurrences', () => { + const countSum = sumBy(errorsDistribution.currentPeriod, 'y'); + const numberOfBuckets = 15; + expect(countSum).to.equal(appleTransaction.failureRate * numberOfBuckets); + }); + }); - describe('displays occurrences for type "apple transaction" only', () => { + describe('with comparison', () => { + describe('when data is returned', () => { let errorsDistribution: ErrorsDistribution; before(async () => { + const fiveMinutes = 5 * 60 * 1000; const response = await callApi({ - query: { kuery: `error.exception.type:"${appleTransaction.name}"` }, + query: { + start: new Date(end - fiveMinutes).toISOString(), + end: new Date(end).toISOString(), + offset: '5m', + }, }); errorsDistribution = response.body; }); - it('displays combined number of occurrences', () => { - const countSum = sumBy(errorsDistribution.currentPeriod, 'y'); - const numberOfBuckets = 15; - expect(countSum).to.equal(appleTransaction.failureRate * numberOfBuckets); - }); - }); - - describe('with comparison', () => { - describe('when data is returned', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const fiveMinutes = 5 * 60 * 1000; - const response = await callApi({ - query: { - start: new Date(end - fiveMinutes).toISOString(), - end: new Date(end).toISOString(), - offset: '5m', - }, - }); - errorsDistribution = response.body; - }); - it('returns some data', () => { - const hasCurrentPeriodData = errorsDistribution.currentPeriod.some(({ y }) => - isFiniteNumber(y) - ); + it('returns some data', () => { + const hasCurrentPeriodData = errorsDistribution.currentPeriod.some(({ y }) => + isFiniteNumber(y) + ); - const hasPreviousPeriodData = errorsDistribution.previousPeriod.some(({ y }) => - isFiniteNumber(y) - ); + const hasPreviousPeriodData = errorsDistribution.previousPeriod.some(({ y }) => + isFiniteNumber(y) + ); - expect(hasCurrentPeriodData).to.equal(true); - expect(hasPreviousPeriodData).to.equal(true); - }); + expect(hasCurrentPeriodData).to.equal(true); + expect(hasPreviousPeriodData).to.equal(true); + }); - it('has same start time for both periods', () => { - expect(first(errorsDistribution.currentPeriod)?.x).to.equal( - first(errorsDistribution.previousPeriod)?.x - ); - }); + it('has same start time for both periods', () => { + expect(first(errorsDistribution.currentPeriod)?.x).to.equal( + first(errorsDistribution.previousPeriod)?.x + ); + }); - it('has same end time for both periods', () => { - expect(last(errorsDistribution.currentPeriod)?.x).to.equal( - last(errorsDistribution.previousPeriod)?.x - ); - }); + it('has same end time for both periods', () => { + expect(last(errorsDistribution.currentPeriod)?.x).to.equal( + last(errorsDistribution.previousPeriod)?.x + ); + }); - it('returns same number of buckets for both periods', () => { - expect(errorsDistribution.currentPeriod.length).to.equal( - errorsDistribution.previousPeriod.length - ); - }); + it('returns same number of buckets for both periods', () => { + expect(errorsDistribution.currentPeriod.length).to.equal( + errorsDistribution.previousPeriod.length + ); }); + }); - describe('when no data is returned', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const response = await callApi({ - query: { - start: '2021-01-03T00:00:00.000Z', - end: '2021-01-03T00:15:00.000Z', - offset: '1d', - }, - }); - errorsDistribution = response.body; + describe('when no data is returned', () => { + let errorsDistribution: ErrorsDistribution; + before(async () => { + const response = await callApi({ + query: { + start: '2021-01-03T00:00:00.000Z', + end: '2021-01-03T00:15:00.000Z', + offset: '1d', + }, }); + errorsDistribution = response.body; + }); - it('has same start time for both periods', () => { - expect(first(errorsDistribution.currentPeriod)?.x).to.equal( - first(errorsDistribution.previousPeriod)?.x - ); - }); + it('has same start time for both periods', () => { + expect(first(errorsDistribution.currentPeriod)?.x).to.equal( + first(errorsDistribution.previousPeriod)?.x + ); + }); - it('has same end time for both periods', () => { - expect(last(errorsDistribution.currentPeriod)?.x).to.equal( - last(errorsDistribution.previousPeriod)?.x - ); - }); + it('has same end time for both periods', () => { + expect(last(errorsDistribution.currentPeriod)?.x).to.equal( + last(errorsDistribution.previousPeriod)?.x + ); + }); - it('returns same number of buckets for both periods', () => { - expect(errorsDistribution.currentPeriod.length).to.equal( - errorsDistribution.previousPeriod.length - ); - }); + it('returns same number of buckets for both periods', () => { + expect(errorsDistribution.currentPeriod.length).to.equal( + errorsDistribution.previousPeriod.length + ); }); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts index db630ba71cd1815..5156ed7f0547862 100644 --- a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts @@ -53,97 +53,91 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('errors group', () => { - const appleTransaction = { - name: 'GET /apple 🍎 ', - successRate: 75, - failureRate: 25, - }; + registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { + describe('errors group', () => { + const appleTransaction = { + name: 'GET /apple 🍎 ', + successRate: 75, + failureRate: 25, + }; - const bananaTransaction = { - name: 'GET /banana 🍌', - successRate: 50, - failureRate: 50, - }; + const bananaTransaction = { + name: 'GET /banana 🍌', + successRate: 50, + failureRate: 50, + }; - before(async () => { - const serviceInstance = apm - .service(serviceName, 'production', 'go') - .instance('instance-a'); + before(async () => { + const serviceInstance = apm.service(serviceName, 'production', 'go').instance('instance-a'); - await synthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(appleTransaction.successRate) - .generator((timestamp) => - serviceInstance - .transaction(appleTransaction.name) - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(appleTransaction.failureRate) - .generator((timestamp) => - serviceInstance - .transaction(appleTransaction.name) - .errors(serviceInstance.error('error 1', 'foo').timestamp(timestamp)) - .duration(1000) - .timestamp(timestamp) - .failure() - ), - timerange(start, end) - .interval('1m') - .rate(bananaTransaction.successRate) - .generator((timestamp) => - serviceInstance - .transaction(bananaTransaction.name) - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(bananaTransaction.failureRate) - .generator((timestamp) => - serviceInstance - .transaction(bananaTransaction.name) - .errors(serviceInstance.error('error 2', 'bar').timestamp(timestamp)) - .duration(1000) - .timestamp(timestamp) - .failure() - ), - ]); - }); + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(appleTransaction.successRate) + .generator((timestamp) => + serviceInstance + .transaction(appleTransaction.name) + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(appleTransaction.failureRate) + .generator((timestamp) => + serviceInstance + .transaction(appleTransaction.name) + .errors(serviceInstance.error('error 1', 'foo').timestamp(timestamp)) + .duration(1000) + .timestamp(timestamp) + .failure() + ), + timerange(start, end) + .interval('1m') + .rate(bananaTransaction.successRate) + .generator((timestamp) => + serviceInstance + .transaction(bananaTransaction.name) + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(bananaTransaction.failureRate) + .generator((timestamp) => + serviceInstance + .transaction(bananaTransaction.name) + .errors(serviceInstance.error('error 2', 'bar').timestamp(timestamp)) + .duration(1000) + .timestamp(timestamp) + .failure() + ), + ]); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - describe('returns the correct data', () => { - let errorGroups: ErrorGroups; - before(async () => { - const response = await callApi(); - errorGroups = response.body.errorGroups; - }); + describe('returns the correct data', () => { + let errorGroups: ErrorGroups; + before(async () => { + const response = await callApi(); + errorGroups = response.body.errorGroups; + }); - it('returns correct number of errors', () => { - expect(errorGroups.length).to.equal(2); - expect(errorGroups.map((error) => error.name).sort()).to.eql(['error 1', 'error 2']); - }); + it('returns correct number of errors', () => { + expect(errorGroups.length).to.equal(2); + expect(errorGroups.map((error) => error.name).sort()).to.eql(['error 1', 'error 2']); + }); - it('returns correct occurences', () => { - const numberOfBuckets = 15; - expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([ - appleTransaction.failureRate * numberOfBuckets, - bananaTransaction.failureRate * numberOfBuckets, - ]); - }); + it('returns correct occurences', () => { + const numberOfBuckets = 15; + expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([ + appleTransaction.failureRate * numberOfBuckets, + bananaTransaction.failureRate * numberOfBuckets, + ]); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/errors/group_id.spec.ts b/x-pack/test/apm_api_integration/tests/errors/group_id.spec.ts index af9a3d01f08a787..93965744c4ff35f 100644 --- a/x-pack/test/apm_api_integration/tests/errors/group_id.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/group_id.spec.ts @@ -58,35 +58,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - const { bananaTransaction } = config; - describe('error group id', () => { - before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); - }); + registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { + const { bananaTransaction } = config; + describe('error group id', () => { + before(async () => { + await generateData({ serviceName, start, end, synthtraceEsClient }); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - describe('return correct data', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const response = await callApi({ - path: { groupId: '0000000000000000000000000Error 1' }, - }); - errorsDistribution = response.body; + describe('return correct data', () => { + let errorsDistribution: ErrorsDistribution; + before(async () => { + const response = await callApi({ + path: { groupId: '0000000000000000000000000Error 1' }, }); + errorsDistribution = response.body; + }); - it('displays correct number of occurrences', () => { - const numberOfBuckets = 15; - expect(errorsDistribution.occurrencesCount).to.equal( - bananaTransaction.failureRate * numberOfBuckets - ); - }); + it('displays correct number of occurrences', () => { + const numberOfBuckets = 15; + expect(errorsDistribution.occurrencesCount).to.equal( + bananaTransaction.failureRate * numberOfBuckets + ); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts index 8e8078e00ab7a6f..cd01ca49afcc553 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts @@ -63,149 +63,141 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - const { - firstTransaction: { name: firstTransactionName, failureRate: firstTransactionFailureRate }, - secondTransaction: { - name: secondTransactionName, - failureRate: secondTransactionFailureRate, - }, - } = config; + registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { + const { + firstTransaction: { name: firstTransactionName, failureRate: firstTransactionFailureRate }, + secondTransaction: { name: secondTransactionName, failureRate: secondTransactionFailureRate }, + } = config; + + describe('returns the correct data', () => { + before(async () => { + await generateData({ serviceName, start, end, synthtraceEsClient }); + }); + + after(() => synthtraceEsClient.clean()); + + describe('without comparison', () => { + const numberOfBuckets = 15; + let erroneousTransactions: ErroneousTransactions; - describe('returns the correct data', () => { before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + const response = await callApi({ + path: { groupId: '0000000000000000000000Error test' }, + }); + erroneousTransactions = response.body; }); - after(() => synthtraceEsClient.clean()); + it('displays the correct number of occurrences', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(topErroneousTransactions.length).to.be(2); + + const firstTransaction = topErroneousTransactions.find( + (x) => x.transactionName === firstTransactionName + ); + expect(firstTransaction).to.not.be(undefined); + expect(firstTransaction?.occurrences).to.be( + firstTransactionFailureRate * numberOfBuckets + ); + + const secondTransaction = topErroneousTransactions.find( + (x) => x.transactionName === secondTransactionName + ); + expect(secondTransaction).to.not.be(undefined); + expect(secondTransaction?.occurrences).to.be( + secondTransactionFailureRate * numberOfBuckets + ); + }); + + it('displays the correct number of occurrences in time series', () => { + const { topErroneousTransactions } = erroneousTransactions; + + const firstTransaction = topErroneousTransactions.find( + (x) => x.transactionName === firstTransactionName + ); + const firstErrorCount = sumBy(firstTransaction?.currentPeriodTimeseries, 'y'); + expect(firstErrorCount).to.be(firstTransactionFailureRate * numberOfBuckets); + + const secondTransaction = topErroneousTransactions.find( + (x) => x.transactionName === secondTransactionName + ); + const secondErrorCount = sumBy(secondTransaction?.currentPeriodTimeseries, 'y'); + expect(secondErrorCount).to.be(secondTransactionFailureRate * numberOfBuckets); + }); + }); - describe('without comparison', () => { - const numberOfBuckets = 15; + describe('with comparison', () => { + describe('when there are data for the time periods', () => { let erroneousTransactions: ErroneousTransactions; before(async () => { + const fiveMinutes = 5 * 60 * 1000; const response = await callApi({ path: { groupId: '0000000000000000000000Error test' }, + query: { + start: new Date(end - fiveMinutes).toISOString(), + end: new Date(end).toISOString(), + offset: '5m', + }, }); erroneousTransactions = response.body; }); - it('displays the correct number of occurrences', () => { + it('returns some data', () => { const { topErroneousTransactions } = erroneousTransactions; - expect(topErroneousTransactions.length).to.be(2); - const firstTransaction = topErroneousTransactions.find( - (x) => x.transactionName === firstTransactionName - ); - expect(firstTransaction).to.not.be(undefined); - expect(firstTransaction?.occurrences).to.be( - firstTransactionFailureRate * numberOfBuckets + const hasCurrentPeriodData = topErroneousTransactions[0].currentPeriodTimeseries.some( + ({ y }) => isFiniteNumber(y) ); - const secondTransaction = topErroneousTransactions.find( - (x) => x.transactionName === secondTransactionName - ); - expect(secondTransaction).to.not.be(undefined); - expect(secondTransaction?.occurrences).to.be( - secondTransactionFailureRate * numberOfBuckets + const hasPreviousPeriodData = topErroneousTransactions[0].previousPeriodTimeseries.some( + ({ y }) => isFiniteNumber(y) ); + + expect(hasCurrentPeriodData).to.be(true); + expect(hasPreviousPeriodData).to.be(true); }); - it('displays the correct number of occurrences in time series', () => { + it('has the same start time for both periods', () => { const { topErroneousTransactions } = erroneousTransactions; + expect(first(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be( + first(topErroneousTransactions[0].previousPeriodTimeseries)?.x + ); + }); - const firstTransaction = topErroneousTransactions.find( - (x) => x.transactionName === firstTransactionName + it('has same end time for both periods', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(last(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be( + last(topErroneousTransactions[0].previousPeriodTimeseries)?.x ); - const firstErrorCount = sumBy(firstTransaction?.currentPeriodTimeseries, 'y'); - expect(firstErrorCount).to.be(firstTransactionFailureRate * numberOfBuckets); + }); - const secondTransaction = topErroneousTransactions.find( - (x) => x.transactionName === secondTransactionName + it('returns same number of buckets for both periods', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(topErroneousTransactions[0].currentPeriodTimeseries.length).to.be( + topErroneousTransactions[0].previousPeriodTimeseries.length ); - const secondErrorCount = sumBy(secondTransaction?.currentPeriodTimeseries, 'y'); - expect(secondErrorCount).to.be(secondTransactionFailureRate * numberOfBuckets); }); }); - describe('with comparison', () => { - describe('when there are data for the time periods', () => { - let erroneousTransactions: ErroneousTransactions; - - before(async () => { - const fiveMinutes = 5 * 60 * 1000; - const response = await callApi({ - path: { groupId: '0000000000000000000000Error test' }, - query: { - start: new Date(end - fiveMinutes).toISOString(), - end: new Date(end).toISOString(), - offset: '5m', - }, - }); - erroneousTransactions = response.body; - }); - - it('returns some data', () => { - const { topErroneousTransactions } = erroneousTransactions; - - const hasCurrentPeriodData = topErroneousTransactions[0].currentPeriodTimeseries.some( - ({ y }) => isFiniteNumber(y) - ); - - const hasPreviousPeriodData = - topErroneousTransactions[0].previousPeriodTimeseries.some(({ y }) => - isFiniteNumber(y) - ); - - expect(hasCurrentPeriodData).to.be(true); - expect(hasPreviousPeriodData).to.be(true); - }); - - it('has the same start time for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(first(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be( - first(topErroneousTransactions[0].previousPeriodTimeseries)?.x - ); - }); - - it('has same end time for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(last(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be( - last(topErroneousTransactions[0].previousPeriodTimeseries)?.x - ); + describe('when there are no data for the time period', () => { + it('returns an empty array', async () => { + const response = await callApi({ + path: { groupId: '0000000000000000000000Error test' }, + query: { + start: '2021-01-03T00:00:00.000Z', + end: '2021-01-03T00:15:00.000Z', + offset: '1d', + }, }); - it('returns same number of buckets for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(topErroneousTransactions[0].currentPeriodTimeseries.length).to.be( - topErroneousTransactions[0].previousPeriodTimeseries.length - ); - }); - }); + const { + body: { topErroneousTransactions }, + } = response; - describe('when there are no data for the time period', () => { - it('returns an empty array', async () => { - const response = await callApi({ - path: { groupId: '0000000000000000000000Error test' }, - query: { - start: '2021-01-03T00:00:00.000Z', - end: '2021-01-03T00:15:00.000Z', - offset: '1d', - }, - }); - - const { - body: { topErroneousTransactions }, - } = response; - - expect(topErroneousTransactions).to.be.empty(); - }); + expect(topErroneousTransactions).to.be.empty(); }); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts index 75d3ba0624375f8..830e5032a8de7f0 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts @@ -58,54 +58,47 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('top errors for transaction', () => { - const { - firstTransaction: { - name: firstTransactionName, - failureRate: firstTransactionFailureRate, - }, - } = config; + registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { + describe('top errors for transaction', () => { + const { + firstTransaction: { name: firstTransactionName, failureRate: firstTransactionFailureRate }, + } = config; - before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); - }); + before(async () => { + await generateData({ serviceName, start, end, synthtraceEsClient }); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - describe('returns the correct data', () => { - let errorGroups: ErrorGroups; - before(async () => { - const response = await callApi({ query: { transactionName: firstTransactionName } }); - errorGroups = response.body.errorGroups; - }); + describe('returns the correct data', () => { + let errorGroups: ErrorGroups; + before(async () => { + const response = await callApi({ query: { transactionName: firstTransactionName } }); + errorGroups = response.body.errorGroups; + }); - it('returns correct number of errors and error data', () => { - const numberOfBuckets = 15; + it('returns correct number of errors and error data', () => { + const numberOfBuckets = 15; - expect(errorGroups.length).to.equal(2); + expect(errorGroups.length).to.equal(2); - const firstErrorId = `Error 1 transaction ${firstTransactionName}`; - const firstError = errorGroups.find((x) => x.groupId === firstErrorId); - expect(firstError).to.not.be(undefined); - expect(firstError?.groupId).to.be(firstErrorId); - expect(firstError?.name).to.be(firstErrorId); - expect(firstError?.occurrences).to.be(firstTransactionFailureRate * numberOfBuckets); - expect(firstError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); + const firstErrorId = `Error 1 transaction ${firstTransactionName}`; + const firstError = errorGroups.find((x) => x.groupId === firstErrorId); + expect(firstError).to.not.be(undefined); + expect(firstError?.groupId).to.be(firstErrorId); + expect(firstError?.name).to.be(firstErrorId); + expect(firstError?.occurrences).to.be(firstTransactionFailureRate * numberOfBuckets); + expect(firstError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); - const secondErrorId = `Error 2 transaction ${firstTransactionName}`; - const secondError = errorGroups.find((x) => x.groupId === secondErrorId); - expect(secondError).to.not.be(undefined); - expect(secondError?.groupId).to.be(secondErrorId); - expect(secondError?.name).to.be(secondErrorId); - expect(secondError?.occurrences).to.be(firstTransactionFailureRate * numberOfBuckets); - expect(secondError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); - }); + const secondErrorId = `Error 2 transaction ${firstTransactionName}`; + const secondError = errorGroups.find((x) => x.groupId === secondErrorId); + expect(secondError).to.not.be(undefined); + expect(secondError?.groupId).to.be(secondErrorId); + expect(secondError?.name).to.be(secondErrorId); + expect(secondError?.occurrences).to.be(firstTransactionFailureRate * numberOfBuckets); + expect(secondError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts b/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts index 39a45a9b396c715..19df36703591174 100644 --- a/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts +++ b/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts @@ -48,39 +48,33 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Infrastructure attributes', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded', () => { - beforeEach(async () => { - await generateData({ start, end, synthtraceEsClient }); - }); + registry.when('Infrastructure attributes', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + beforeEach(async () => { + await generateData({ start, end, synthtraceEsClient }); + }); - afterEach(() => synthtraceEsClient.clean()); + afterEach(() => synthtraceEsClient.clean()); - describe('when service runs in container', () => { - it('returns arrays of container ids and pod names', async () => { - const response = await callApi('synth-go'); - expect(response.status).to.be(200); - expect(response.body.containerIds.length).to.be(1); - // hostNames is always returning empty - // we can not test the infra indices api call with synthtrace - expect(response.body.hostNames.length).to.be(0); - expect(response.body.podNames.length).to.be(1); - }); + describe('when service runs in container', () => { + it('returns arrays of container ids and pod names', async () => { + const response = await callApi('synth-go'); + expect(response.status).to.be(200); + expect(response.body.containerIds.length).to.be(1); + expect(response.body.hostNames.length).to.be(1); + expect(response.body.podNames.length).to.be(1); }); + }); - describe('when service does NOT run in container', () => { - it('returns array of host names', async () => { - const response = await callApi('synth-java'); - expect(response.status).to.be(200); - expect(response.body.containerIds.length).to.be(0); - expect(response.body.hostNames.length).to.be(1); - expect(response.body.podNames.length).to.be(0); - }); + describe('when service does NOT run in container', () => { + it('returns array of host names', async () => { + const response = await callApi('synth-java'); + expect(response.status).to.be(200); + expect(response.body.containerIds.length).to.be(0); + expect(response.body.hostNames.length).to.be(1); + expect(response.body.podNames.length).to.be(0); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts index 1ad8322443e1aec..b38a7ef3052c2c7 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts @@ -116,7 +116,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let latencyMetricValues: Awaited>; let latencyTransactionValues: Awaited>; - registry.when('Services APIs', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { + registry.when('Services APIs', { config: 'basic', archives: [] }, () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; diff --git a/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts index e898dbb67f2e562..e9b9749b659e214 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts @@ -59,68 +59,63 @@ export default function ApiTest({ getService }: FtrProviderContext) { let latencyMetricValues: Awaited>; let latencyTransactionValues: Awaited>; - registry.when( - 'Service maps APIs', - { config: 'trial', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded ', () => { - const GO_PROD_RATE = 80; - const GO_DEV_RATE = 20; - const GO_PROD_DURATION = 1000; - const GO_DEV_DURATION = 500; - before(async () => { - const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') - .instance('instance-a'); - const serviceGoDevInstance = apm - .service(serviceName, 'development', 'go') - .instance('instance-b'); + registry.when('Service maps APIs', { config: 'trial', archives: [] }, () => { + describe('when data is loaded ', () => { + const GO_PROD_RATE = 80; + const GO_DEV_RATE = 20; + const GO_PROD_DURATION = 1000; + const GO_DEV_DURATION = 500; + before(async () => { + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); + const serviceGoDevInstance = apm + .service(serviceName, 'development', 'go') + .instance('instance-b'); - await synthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction('GET /api/product/list', 'Worker') - .duration(GO_PROD_DURATION) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(GO_DEV_RATE) - .generator((timestamp) => - serviceGoDevInstance - .transaction('GET /api/product/:id') - .duration(GO_DEV_DURATION) - .timestamp(timestamp) - ), - ]); - }); + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction('GET /api/product/list', 'Worker') + .duration(GO_PROD_DURATION) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .generator((timestamp) => + serviceGoDevInstance + .transaction('GET /api/product/:id') + .duration(GO_DEV_DURATION) + .timestamp(timestamp) + ), + ]); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - describe('compare latency value between service inventory and service maps', () => { - before(async () => { - [latencyTransactionValues, latencyMetricValues] = await Promise.all([ - getLatencyValues('transaction'), - getLatencyValues('metric'), - ]); - }); + describe('compare latency value between service inventory and service maps', () => { + before(async () => { + [latencyTransactionValues, latencyMetricValues] = await Promise.all([ + getLatencyValues('transaction'), + getLatencyValues('metric'), + ]); + }); - it('returns same avg latency value for Transaction-based and Metric-based data', () => { - const expectedLatencyAvgValueMs = - ((GO_DEV_RATE * GO_DEV_DURATION) / GO_DEV_RATE) * 1000; + it('returns same avg latency value for Transaction-based and Metric-based data', () => { + const expectedLatencyAvgValueMs = ((GO_DEV_RATE * GO_DEV_DURATION) / GO_DEV_RATE) * 1000; - [ - latencyTransactionValues.serviceMapsNodeDetailsLatency, - latencyTransactionValues.serviceInventoryLatency, - latencyMetricValues.serviceMapsNodeDetailsLatency, - latencyMetricValues.serviceInventoryLatency, - ].forEach((value) => expect(value).to.be.equal(expectedLatencyAvgValueMs)); - }); + [ + latencyTransactionValues.serviceMapsNodeDetailsLatency, + latencyTransactionValues.serviceInventoryLatency, + latencyMetricValues.serviceMapsNodeDetailsLatency, + latencyMetricValues.serviceInventoryLatency, + ].forEach((value) => expect(value).to.be.equal(expectedLatencyAvgValueMs)); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts index e924fc865c42f58..d25b17cc0dc6b33 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts @@ -83,93 +83,87 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('Observability overview api ', () => { - const GO_PROD_RATE = 50; - const GO_DEV_RATE = 5; - const JAVA_PROD_RATE = 45; - before(async () => { - const serviceGoProdInstance = apm - .service('synth-go', 'production', 'go') - .instance('instance-a'); - const serviceGoDevInstance = apm - .service('synth-go', 'development', 'go') - .instance('instance-b'); + registry.when('data is loaded', { config: 'basic', archives: [] }, () => { + describe('Observability overview api ', () => { + const GO_PROD_RATE = 50; + const GO_DEV_RATE = 5; + const JAVA_PROD_RATE = 45; + before(async () => { + const serviceGoProdInstance = apm + .service('synth-go', 'production', 'go') + .instance('instance-a'); + const serviceGoDevInstance = apm + .service('synth-go', 'development', 'go') + .instance('instance-b'); - const serviceJavaInstance = apm - .service('synth-java', 'production', 'java') - .instance('instance-c'); + const serviceJavaInstance = apm + .service('synth-java', 'production', 'java') + .instance('instance-c'); - await synthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction('GET /api/product/list') - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(GO_DEV_RATE) - .generator((timestamp) => - serviceGoDevInstance - .transaction('GET /api/product/:id') - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(JAVA_PROD_RATE) - .generator((timestamp) => - serviceJavaInstance - .transaction('POST /api/product/buy') - .duration(1000) - .timestamp(timestamp) - ), - ]); - }); + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction('GET /api/product/list') + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .generator((timestamp) => + serviceGoDevInstance + .transaction('GET /api/product/:id') + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(JAVA_PROD_RATE) + .generator((timestamp) => + serviceJavaInstance + .transaction('POST /api/product/buy') + .duration(1000) + .timestamp(timestamp) + ), + ]); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - describe('compare throughput values', () => { - let throughputValues: Awaited>; - before(async () => { - throughputValues = await getThroughputValues(); - }); + describe('compare throughput values', () => { + let throughputValues: Awaited>; + before(async () => { + throughputValues = await getThroughputValues(); + }); - it('returns same number of service as shown on service inventory API', () => { - const { serviceInventoryCount, observabilityOverview } = throughputValues; - [serviceInventoryCount, observabilityOverview.serviceCount].forEach((value) => - expect(value).to.be.equal(2) - ); - }); + it('returns same number of service as shown on service inventory API', () => { + const { serviceInventoryCount, observabilityOverview } = throughputValues; + [serviceInventoryCount, observabilityOverview.serviceCount].forEach((value) => + expect(value).to.be.equal(2) + ); + }); - it('returns same throughput value on service inventory and obs throughput count', () => { - const { serviceInventoryThroughputSum, observabilityOverview } = throughputValues; - const obsThroughputCount = roundNumber( - observabilityOverview.transactionPerMinute.value - ); - [serviceInventoryThroughputSum, obsThroughputCount].forEach((value) => - expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE + JAVA_PROD_RATE)) - ); - }); + it('returns same throughput value on service inventory and obs throughput count', () => { + const { serviceInventoryThroughputSum, observabilityOverview } = throughputValues; + const obsThroughputCount = roundNumber(observabilityOverview.transactionPerMinute.value); + [serviceInventoryThroughputSum, obsThroughputCount].forEach((value) => + expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE + JAVA_PROD_RATE)) + ); + }); - it('returns same throughput value on service inventory and obs mean throughput timeseries', () => { - const { serviceInventoryThroughputSum, observabilityOverview } = throughputValues; - const obsThroughputMean = roundNumber( - meanBy(observabilityOverview.transactionPerMinute.timeseries, 'y') - ); - [serviceInventoryThroughputSum, obsThroughputMean].forEach((value) => - expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE + JAVA_PROD_RATE)) - ); - }); + it('returns same throughput value on service inventory and obs mean throughput timeseries', () => { + const { serviceInventoryThroughputSum, observabilityOverview } = throughputValues; + const obsThroughputMean = roundNumber( + meanBy(observabilityOverview.transactionPerMinute.timeseries, 'y') + ); + [serviceInventoryThroughputSum, obsThroughputMean].forEach((value) => + expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE + JAVA_PROD_RATE)) + ); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts b/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts index a6c53e0bade0523..c582c929c67cb45 100644 --- a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts @@ -48,36 +48,33 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'Service nodes when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - before(async () => { - const instance = apm.service(serviceName, 'production', 'go').instance(instanceName); - await synthtraceEsClient.index( - timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => - instance - .appMetrics({ - 'system.process.cpu.total.norm.pct': 1, - 'jvm.memory.heap.used': 1000, - 'jvm.memory.non_heap.used': 100, - 'jvm.thread.count': 25, - }) - .timestamp(timestamp) - ) - ); - }); - after(() => synthtraceEsClient.clean()); + registry.when('Service nodes when data is loaded', { config: 'basic', archives: [] }, () => { + before(async () => { + const instance = apm.service(serviceName, 'production', 'go').instance(instanceName); + await synthtraceEsClient.index( + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + instance + .appMetrics({ + 'system.process.cpu.total.norm.pct': 1, + 'jvm.memory.heap.used': 1000, + 'jvm.memory.non_heap.used': 100, + 'jvm.thread.count': 25, + }) + .timestamp(timestamp) + ) + ); + }); + after(() => synthtraceEsClient.clean()); - it('returns service nodes', async () => { - const response = await callApi(); + it('returns service nodes', async () => { + const response = await callApi(); - expect(response.status).to.be(200); + expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` + expectSnapshot(response.body).toMatchInline(` Object { "serviceNodes": Array [ Object { @@ -91,7 +88,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { ], } `); - }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts index 8d92ec6a38573b4..8313ec635c69af1 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts @@ -283,7 +283,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'Service overview instances main statistics when data is generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'basic', archives: [] }, () => { describe('for two go instances and one java instance', () => { const GO_A_INSTANCE_RATE_SUCCESS = 10; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts index d6dc2bfb4b9ed7f..b9db3ae5e8fdd3c 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts @@ -64,136 +64,130 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Error groups detailed statistics', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded', () => { - const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE } = config; + registry.when('Error groups detailed statistics', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE } = config; + before(async () => { + await generateData({ serviceName, start, end, synthtraceEsClient }); + }); + + after(() => synthtraceEsClient.clean()); + + describe('without data comparison', () => { + let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; + let errorIds: string[] = []; before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient }); + const response = await callApi({ + body: { + groupIds: JSON.stringify(errorIds), + }, + }); + errorGroupsDetailedStatistics = response.body; + }); + + it('return detailed statistics for all errors found', () => { + expect(Object.keys(errorGroupsDetailedStatistics.currentPeriod).sort()).to.eql(errorIds); }); - after(() => synthtraceEsClient.clean()); - - describe('without data comparison', () => { - let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; - let errorIds: string[] = []; - before(async () => { - errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient }); - const response = await callApi({ - body: { - groupIds: JSON.stringify(errorIds), - }, + it('returns correct number of occurrencies', () => { + const numberOfBuckets = 15; + const detailedStatisticsOccurrenciesSum = Object.values( + errorGroupsDetailedStatistics.currentPeriod + ) + .sort() + .map(({ timeseries }) => { + return sumBy(timeseries, 'y'); }); - errorGroupsDetailedStatistics = response.body; - }); - it('return detailed statistics for all errors found', () => { - expect(Object.keys(errorGroupsDetailedStatistics.currentPeriod).sort()).to.eql( - errorIds - ); - }); + expect(detailedStatisticsOccurrenciesSum).to.eql([ + PROD_ID_ERROR_RATE * numberOfBuckets, + PROD_LIST_ERROR_RATE * numberOfBuckets, + ]); + }); + }); - it('returns correct number of occurrencies', () => { - const numberOfBuckets = 15; - const detailedStatisticsOccurrenciesSum = Object.values( - errorGroupsDetailedStatistics.currentPeriod - ) - .sort() - .map(({ timeseries }) => { - return sumBy(timeseries, 'y'); - }); - - expect(detailedStatisticsOccurrenciesSum).to.eql([ - PROD_ID_ERROR_RATE * numberOfBuckets, - PROD_LIST_ERROR_RATE * numberOfBuckets, - ]); + describe('return empty state when invalid group id', () => { + let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; + before(async () => { + const response = await callApi({ + body: { + groupIds: JSON.stringify(['foo']), + }, }); + errorGroupsDetailedStatistics = response.body; }); - describe('return empty state when invalid group id', () => { - let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; - before(async () => { - const response = await callApi({ - body: { - groupIds: JSON.stringify(['foo']), - }, - }); - errorGroupsDetailedStatistics = response.body; - }); - - it('returns empty state', () => { - expect(errorGroupsDetailedStatistics).to.be.eql({ - currentPeriod: {}, - previousPeriod: {}, - }); + it('returns empty state', () => { + expect(errorGroupsDetailedStatistics).to.be.eql({ + currentPeriod: {}, + previousPeriod: {}, }); }); + }); - describe('with comparison', () => { - let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; - let errorIds: string[] = []; - before(async () => { - errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient }); - const response = await callApi({ - query: { - start: moment(end).subtract(7, 'minutes').toISOString(), - end: new Date(end).toISOString(), - offset: '7m', - }, - body: { - groupIds: JSON.stringify(errorIds), - }, - }); - errorGroupsDetailedStatistics = response.body; + describe('with comparison', () => { + let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; + let errorIds: string[] = []; + before(async () => { + errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient }); + const response = await callApi({ + query: { + start: moment(end).subtract(7, 'minutes').toISOString(), + end: new Date(end).toISOString(), + offset: '7m', + }, + body: { + groupIds: JSON.stringify(errorIds), + }, }); + errorGroupsDetailedStatistics = response.body; + }); - it('returns some data', () => { - expect( - Object.keys(errorGroupsDetailedStatistics.currentPeriod).length - ).to.be.greaterThan(0); - expect( - Object.keys(errorGroupsDetailedStatistics.previousPeriod).length - ).to.be.greaterThan(0); + it('returns some data', () => { + expect(Object.keys(errorGroupsDetailedStatistics.currentPeriod).length).to.be.greaterThan( + 0 + ); + expect( + Object.keys(errorGroupsDetailedStatistics.previousPeriod).length + ).to.be.greaterThan(0); - const hasCurrentPeriodData = Object.values( - errorGroupsDetailedStatistics.currentPeriod - )[0].timeseries.some(({ y }) => isFiniteNumber(y)); + const hasCurrentPeriodData = Object.values( + errorGroupsDetailedStatistics.currentPeriod + )[0].timeseries.some(({ y }) => isFiniteNumber(y)); - const hasPreviousPeriodData = Object.values( - errorGroupsDetailedStatistics.previousPeriod - )[0].timeseries.some(({ y }) => isFiniteNumber(y)); + const hasPreviousPeriodData = Object.values( + errorGroupsDetailedStatistics.previousPeriod + )[0].timeseries.some(({ y }) => isFiniteNumber(y)); - expect(hasCurrentPeriodData).to.equal(true); - expect(hasPreviousPeriodData).to.equal(true); - }); + expect(hasCurrentPeriodData).to.equal(true); + expect(hasPreviousPeriodData).to.equal(true); + }); - it('has same start time for both periods', () => { - expect( - first(Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries)?.x - ).to.equal( - first(Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries)?.x - ); - }); + it('has same start time for both periods', () => { + expect( + first(Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries)?.x + ).to.equal( + first(Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries)?.x + ); + }); - it('has same end time for both periods', () => { - expect( - last(Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries)?.x - ).to.equal( - last(Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries)?.x - ); - }); + it('has same end time for both periods', () => { + expect( + last(Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries)?.x + ).to.equal( + last(Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries)?.x + ); + }); - it('returns same number of buckets for both periods', () => { - expect( - Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries.length - ).to.equal( - Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries.length - ); - }); + it('returns same number of buckets for both periods', () => { + expect( + Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries.length + ).to.equal( + Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries.length + ); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts index df86dc2bda16fb1..32ae3b9bfd5acd1 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts @@ -58,51 +58,44 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Error groups main statistics', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded', () => { - const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE, ERROR_NAME_1, ERROR_NAME_2 } = config; + registry.when('Error groups main statistics', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE, ERROR_NAME_1, ERROR_NAME_2 } = config; - before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); - }); + before(async () => { + await generateData({ serviceName, start, end, synthtraceEsClient }); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - describe('returns the correct data', () => { - let errorGroupMainStatistics: ErrorGroupsMainStatistics; - before(async () => { - const response = await callApi(); - errorGroupMainStatistics = response.body; - }); + describe('returns the correct data', () => { + let errorGroupMainStatistics: ErrorGroupsMainStatistics; + before(async () => { + const response = await callApi(); + errorGroupMainStatistics = response.body; + }); - it('returns correct number of occurrences', () => { - expect(errorGroupMainStatistics.errorGroups.length).to.equal(2); - expect(errorGroupMainStatistics.errorGroups.map((error) => error.name).sort()).to.eql([ - ERROR_NAME_1, - ERROR_NAME_2, - ]); - }); + it('returns correct number of occurrences', () => { + expect(errorGroupMainStatistics.errorGroups.length).to.equal(2); + expect(errorGroupMainStatistics.errorGroups.map((error) => error.name).sort()).to.eql([ + ERROR_NAME_1, + ERROR_NAME_2, + ]); + }); - it('returns correct occurences', () => { - const numberOfBuckets = 15; - expect( - errorGroupMainStatistics.errorGroups.map((error) => error.occurrences).sort() - ).to.eql([ - PROD_LIST_ERROR_RATE * numberOfBuckets, - PROD_ID_ERROR_RATE * numberOfBuckets, - ]); - }); + it('returns correct occurences', () => { + const numberOfBuckets = 15; + expect( + errorGroupMainStatistics.errorGroups.map((error) => error.occurrences).sort() + ).to.eql([PROD_LIST_ERROR_RATE * numberOfBuckets, PROD_ID_ERROR_RATE * numberOfBuckets]); + }); - it('has same last seen value as end date', () => { - errorGroupMainStatistics.errorGroups.map((error) => { - expect(error.lastSeen).to.equal(moment(end).startOf('minute').valueOf()); - }); + it('has same last seen value as end date', () => { + errorGroupMainStatistics.errorGroups.map((error) => { + expect(error.lastSeen).to.equal(moment(end).startOf('minute').valueOf()); }); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts b/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts index 0033c5b8f026b33..49160f9b5caf2df 100644 --- a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts @@ -54,7 +54,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'Service node metadata when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'basic', archives: [] }, () => { before(async () => { const instance = apm.service(serviceName, 'production', 'go').instance(instanceName); diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts index c53c9f9f16e8633..f54fbf26c7b99dd 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts @@ -50,79 +50,75 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Service details when data is generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - let body: ServiceDetails; - let status: number; - - before(async () => { - await generateData({ synthtraceEsClient, start, end }); - const response = await callApi(); - body = response.body; - status = response.status; - }); + registry.when('Service details when data is generated', { config: 'basic', archives: [] }, () => { + let body: ServiceDetails; + let status: number; + + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + const response = await callApi(); + body = response.body; + status = response.status; + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - it('returns correct HTTP status', () => { - expect(status).to.be(200); - }); + it('returns correct HTTP status', () => { + expect(status).to.be(200); + }); - it('returns correct cloud details', () => { - const { cloud } = dataConfig; - const { - provider, - availabilityZone, - region, - machineType, - projectName, - serviceName: cloudServiceName, - } = cloud; - - expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone); - expect(first(body?.cloud?.machineTypes)).to.be(machineType); - expect(body?.cloud?.provider).to.be(provider); - expect(body?.cloud?.projectName).to.be(projectName); - expect(body?.cloud?.serviceName).to.be(cloudServiceName); - expect(first(body?.cloud?.regions)).to.be(region); - }); + it('returns correct cloud details', () => { + const { cloud } = dataConfig; + const { + provider, + availabilityZone, + region, + machineType, + projectName, + serviceName: cloudServiceName, + } = cloud; + + expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone); + expect(first(body?.cloud?.machineTypes)).to.be(machineType); + expect(body?.cloud?.provider).to.be(provider); + expect(body?.cloud?.projectName).to.be(projectName); + expect(body?.cloud?.serviceName).to.be(cloudServiceName); + expect(first(body?.cloud?.regions)).to.be(region); + }); - it('returns correct container details', () => { - const { containerOs } = dataConfig; + it('returns correct container details', () => { + const { containerOs } = dataConfig; - expect(body?.container?.isContainerized).to.be(true); - expect(body?.container?.os).to.be(containerOs); - expect(body?.container?.totalNumberInstances).to.be(1); - expect(body?.container?.type).to.be('Kubernetes'); - }); + expect(body?.container?.isContainerized).to.be(true); + expect(body?.container?.os).to.be(containerOs); + expect(body?.container?.totalNumberInstances).to.be(1); + expect(body?.container?.type).to.be('Kubernetes'); + }); - it('returns correct serverless details', () => { - const { cloud, serverless } = dataConfig; - const { serviceName: cloudServiceName } = cloud; - const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless; + it('returns correct serverless details', () => { + const { cloud, serverless } = dataConfig; + const { serviceName: cloudServiceName } = cloud; + const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless; - expect(body?.serverless?.type).to.be(cloudServiceName); - expect(body?.serverless?.functionNames).to.have.length(2); - expect(body?.serverless?.functionNames).to.contain(firstFunctionName); - expect(body?.serverless?.functionNames).to.contain(secondFunctionName); - expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType); - }); + expect(body?.serverless?.type).to.be(cloudServiceName); + expect(body?.serverless?.functionNames).to.have.length(2); + expect(body?.serverless?.functionNames).to.contain(firstFunctionName); + expect(body?.serverless?.functionNames).to.contain(secondFunctionName); + expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType); + }); - it('returns correct service details', () => { - const { service } = dataConfig; - const { version, runtime, framework, agent } = service; - const { name: runTimeName, version: runTimeVersion } = runtime; - const { name: agentName, version: agentVersion } = agent; - - expect(body?.service?.framework).to.be(framework); - expect(body?.service?.agent.name).to.be(agentName); - expect(body?.service?.agent.version).to.be(agentVersion); - expect(body?.service?.runtime?.name).to.be(runTimeName); - expect(body?.service?.runtime?.version).to.be(runTimeVersion); - expect(first(body?.service?.versions)).to.be(version); - }); - } - ); + it('returns correct service details', () => { + const { service } = dataConfig; + const { version, runtime, framework, agent } = service; + const { name: runTimeName, version: runTimeVersion } = runtime; + const { name: agentName, version: agentVersion } = agent; + + expect(body?.service?.framework).to.be(framework); + expect(body?.service?.agent.name).to.be(agentName); + expect(body?.service?.agent.version).to.be(agentVersion); + expect(body?.service?.runtime?.name).to.be(runTimeName); + expect(body?.service?.runtime?.version).to.be(runTimeVersion); + expect(first(body?.service?.versions)).to.be(version); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts index 11cb59fff075619..756babbadd9f0d4 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts @@ -43,35 +43,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'Service icons when data is generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - let body: ServiceIconMetadata; - let status: number; + registry.when('Service icons when data is generated', { config: 'basic', archives: [] }, () => { + let body: ServiceIconMetadata; + let status: number; - before(async () => { - await generateData({ synthtraceEsClient, start, end }); - const response = await callApi(); - body = response.body; - status = response.status; - }); + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + const response = await callApi(); + body = response.body; + status = response.status; + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - it('returns correct HTTP status', () => { - expect(status).to.be(200); - }); + it('returns correct HTTP status', () => { + expect(status).to.be(200); + }); - it('returns correct metadata', () => { - const { agentName, cloud } = dataConfig; - const { provider, serviceName: cloudServiceName } = cloud; + it('returns correct metadata', () => { + const { agentName, cloud } = dataConfig; + const { provider, serviceName: cloudServiceName } = cloud; - expect(body.agentName).to.be(agentName); - expect(body.cloudProvider).to.be(provider); - expect(body.containerType).to.be('Kubernetes'); - expect(body.serverlessType).to.be(cloudServiceName); - }); - } - ); + expect(body.agentName).to.be(agentName); + expect(body.cloudProvider).to.be(provider); + expect(body.containerType).to.be('Kubernetes'); + expect(body.serverlessType).to.be(cloudServiceName); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts index 92a6997244b20f4..ca2cb0c3d20a714 100644 --- a/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts @@ -51,102 +51,98 @@ export default function ApiTest({ getService }: FtrProviderContext) { type ServiceListItem = ValuesType>>; // FLAKY: https://github.com/elastic/kibana/issues/127939 - registry.when.skip( - 'Sorted and filtered services', - { config: 'trial', archives: ['apm_mappings_only_8.0.0'] }, - () => { - before(async () => { - const serviceA = apm.service(SERVICE_NAME_PREFIX + 'a', 'production', 'java').instance('a'); - - const serviceB = apm.service(SERVICE_NAME_PREFIX + 'b', 'development', 'go').instance('b'); - - const serviceC = apm.service(SERVICE_NAME_PREFIX + 'c', 'development', 'go').instance('c'); - - const spikeStart = new Date('2021-01-07T12:00:00.000Z').getTime(); - const spikeEnd = new Date('2021-01-07T14:00:00.000Z').getTime(); - - const eventsWithinTimerange = timerange(new Date(start).getTime(), new Date(end).getTime()) - .interval('15m') - .rate(1) - .generator((timestamp) => { - const isInSpike = spikeStart <= timestamp && spikeEnd >= timestamp; - return [ - serviceA - .transaction('GET /api') - .duration(isInSpike ? 1000 : 1100) - .timestamp(timestamp), - serviceB - .transaction('GET /api') - .duration(isInSpike ? 1000 : 4000) - .timestamp(timestamp), - ]; - }); - - const eventsOutsideOfTimerange = timerange( - new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date(start).getTime() - 1 - ) - .interval('15m') - .rate(1) - .generator((timestamp) => { - return serviceC.transaction('GET /api', 'custom').duration(1000).timestamp(timestamp); - }); - - await synthtraceClient.index(eventsWithinTimerange.merge(eventsOutsideOfTimerange)); - - await Promise.all([ - createAndRunApmMlJob({ environment: 'production', ml }), - createAndRunApmMlJob({ environment: 'development', ml }), - ]); - }); + registry.when.skip('Sorted and filtered services', { config: 'trial', archives: [] }, () => { + before(async () => { + const serviceA = apm.service(SERVICE_NAME_PREFIX + 'a', 'production', 'java').instance('a'); + + const serviceB = apm.service(SERVICE_NAME_PREFIX + 'b', 'development', 'go').instance('b'); + + const serviceC = apm.service(SERVICE_NAME_PREFIX + 'c', 'development', 'go').instance('c'); + + const spikeStart = new Date('2021-01-07T12:00:00.000Z').getTime(); + const spikeEnd = new Date('2021-01-07T14:00:00.000Z').getTime(); + + const eventsWithinTimerange = timerange(new Date(start).getTime(), new Date(end).getTime()) + .interval('15m') + .rate(1) + .generator((timestamp) => { + const isInSpike = spikeStart <= timestamp && spikeEnd >= timestamp; + return [ + serviceA + .transaction('GET /api') + .duration(isInSpike ? 1000 : 1100) + .timestamp(timestamp), + serviceB + .transaction('GET /api') + .duration(isInSpike ? 1000 : 4000) + .timestamp(timestamp), + ]; + }); - after(() => { - return Promise.all([synthtraceClient.clean(), ml.cleanMlIndices()]); - }); + const eventsOutsideOfTimerange = timerange( + new Date('2021-01-01T00:00:00.000Z').getTime(), + new Date(start).getTime() - 1 + ) + .interval('15m') + .rate(1) + .generator((timestamp) => { + return serviceC.transaction('GET /api', 'custom').duration(1000).timestamp(timestamp); + }); - describe('with no kuery or environment are set', () => { - let items: ServiceListItem[]; + await synthtraceClient.index(eventsWithinTimerange.merge(eventsOutsideOfTimerange)); - before(async () => { - items = await getSortedAndFilteredServices(); - }); + await Promise.all([ + createAndRunApmMlJob({ environment: 'production', ml }), + createAndRunApmMlJob({ environment: 'development', ml }), + ]); + }); - it('returns services based on the terms enum API and ML data', () => { - const serviceNames = items.map((item) => item.serviceName); + after(() => { + return Promise.all([synthtraceClient.clean(), ml.cleanMlIndices()]); + }); - expect(serviceNames.sort()).to.eql(['a', 'b', 'c']); - }); + describe('with no kuery or environment are set', () => { + let items: ServiceListItem[]; + + before(async () => { + items = await getSortedAndFilteredServices(); }); - describe('with kuery set', () => { - let items: ServiceListItem[]; + it('returns services based on the terms enum API and ML data', () => { + const serviceNames = items.map((item) => item.serviceName); - before(async () => { - items = await getSortedAndFilteredServices({ - kuery: 'service.name:*', - }); - }); + expect(serviceNames.sort()).to.eql(['a', 'b', 'c']); + }); + }); + + describe('with kuery set', () => { + let items: ServiceListItem[]; - it('does not return any services', () => { - expect(items.length).to.be(0); + before(async () => { + items = await getSortedAndFilteredServices({ + kuery: 'service.name:*', }); }); - describe('with environment set to production', () => { - let items: ServiceListItem[]; + it('does not return any services', () => { + expect(items.length).to.be(0); + }); + }); - before(async () => { - items = await getSortedAndFilteredServices({ - environment: 'production', - }); + describe('with environment set to production', () => { + let items: ServiceListItem[]; + + before(async () => { + items = await getSortedAndFilteredServices({ + environment: 'production', }); + }); - it('returns services for production only', () => { - const serviceNames = items.map((item) => item.serviceName); + it('returns services for production only', () => { + const serviceNames = items.map((item) => item.serviceName); - expect(serviceNames.sort()).to.eql(['a']); - }); + expect(serviceNames.sort()).to.eql(['a']); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts index 12c4c809193e2bd..c9c95b2e99bbc5f 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts @@ -63,219 +63,213 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when( - 'Throughput when data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('Throughput chart api', () => { - const GO_PROD_RATE = 50; - const GO_DEV_RATE = 5; - const JAVA_PROD_RATE = 45; + registry.when('Throughput when data is loaded', { config: 'basic', archives: [] }, () => { + describe('Throughput chart api', () => { + const GO_PROD_RATE = 50; + const GO_DEV_RATE = 5; + const JAVA_PROD_RATE = 45; + + before(async () => { + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); + const serviceGoDevInstance = apm + .service(serviceName, 'development', 'go') + .instance('instance-b'); + + const serviceJavaInstance = apm + .service('synth-java', 'development', 'java') + .instance('instance-c'); + + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction('GET /api/product/list') + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .generator((timestamp) => + serviceGoDevInstance + .transaction('GET /api/product/:id') + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(JAVA_PROD_RATE) + .generator((timestamp) => + serviceJavaInstance + .transaction('POST /api/product/buy') + .duration(1000) + .timestamp(timestamp) + ), + ]); + }); + + after(() => synthtraceEsClient.clean()); + + describe('compare transactions and metrics based throughput', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; before(async () => { - const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') - .instance('instance-a'); - const serviceGoDevInstance = apm - .service(serviceName, 'development', 'go') - .instance('instance-b'); - - const serviceJavaInstance = apm - .service('synth-java', 'development', 'java') - .instance('instance-c'); - - await synthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction('GET /api/product/list') - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(GO_DEV_RATE) - .generator((timestamp) => - serviceGoDevInstance - .transaction('GET /api/product/:id') - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(JAVA_PROD_RATE) - .generator((timestamp) => - serviceJavaInstance - .transaction('POST /api/product/buy') - .duration(1000) - .timestamp(timestamp) - ), + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi({ query: { kuery: 'processor.event : "metric"' } }), + callApi({ query: { kuery: 'processor.event : "transaction"' } }), ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; }); - after(() => synthtraceEsClient.clean()); - - describe('compare transactions and metrics based throughput', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi({ query: { kuery: 'processor.event : "metric"' } }), - callApi({ query: { kuery: 'processor.event : "transaction"' } }), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE)) + ); + }); - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE)) - ); - }); + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); + describe('production environment', () => { + let throughput: ThroughputReturn; - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); + before(async () => { + const throughputResponse = await callApi({ query: { environment: 'production' } }); + throughput = throughputResponse.body; }); - describe('production environment', () => { - let throughput: ThroughputReturn; + it('returns some data', () => { + expect(throughput.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); - before(async () => { - const throughputResponse = await callApi({ query: { environment: 'production' } }); - throughput = throughputResponse.body; - }); + it('returns correct average throughput', () => { + const throughputMean = meanBy(throughput.currentPeriod, 'y'); + expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE)); + }); + }); - it('returns some data', () => { - expect(throughput.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); + describe('when synth-java is selected', () => { + let throughput: ThroughputReturn; - it('returns correct average throughput', () => { - const throughputMean = meanBy(throughput.currentPeriod, 'y'); - expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE)); - }); + before(async () => { + const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } }); + throughput = throughputResponse.body; }); - describe('when synth-java is selected', () => { - let throughput: ThroughputReturn; + it('returns some data', () => { + expect(throughput.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); - before(async () => { - const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } }); - throughput = throughputResponse.body; - }); + it('returns throughput related to java agent', () => { + const throughputMean = meanBy(throughput.currentPeriod, 'y'); + expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE)); + }); + }); - it('returns some data', () => { - expect(throughput.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); + describe('time comparisons', () => { + let throughputResponse: ThroughputReturn; - it('returns throughput related to java agent', () => { - const throughputMean = meanBy(throughput.currentPeriod, 'y'); - expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE)); + before(async () => { + const response = await callApi({ + query: { + start: moment(end).subtract(7, 'minutes').toISOString(), + end: new Date(end).toISOString(), + offset: '7m', + }, }); + throughputResponse = response.body; }); - describe('time comparisons', () => { - let throughputResponse: ThroughputReturn; - - before(async () => { - const response = await callApi({ - query: { - start: moment(end).subtract(7, 'minutes').toISOString(), - end: new Date(end).toISOString(), - offset: '7m', - }, - }); - throughputResponse = response.body; - }); + it('returns some data', () => { + expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); + expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0); - it('returns some data', () => { - expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); - expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0); + const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) => + isFiniteNumber(y) + ); + const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) => + isFiniteNumber(y) + ); - const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) => - isFiniteNumber(y) - ); - const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) => - isFiniteNumber(y) - ); - - expect(hasCurrentPeriodData).to.equal(true); - expect(hasPreviousPeriodData).to.equal(true); - }); + expect(hasCurrentPeriodData).to.equal(true); + expect(hasPreviousPeriodData).to.equal(true); + }); - it('has same start time for both periods', () => { - expect(first(throughputResponse.currentPeriod)?.x).to.equal( - first(throughputResponse.previousPeriod)?.x - ); - }); + it('has same start time for both periods', () => { + expect(first(throughputResponse.currentPeriod)?.x).to.equal( + first(throughputResponse.previousPeriod)?.x + ); + }); - it('has same end time for both periods', () => { - expect(last(throughputResponse.currentPeriod)?.x).to.equal( - last(throughputResponse.previousPeriod)?.x - ); - }); + it('has same end time for both periods', () => { + expect(last(throughputResponse.currentPeriod)?.x).to.equal( + last(throughputResponse.previousPeriod)?.x + ); + }); - it('returns same number of buckets for both periods', () => { - expect(throughputResponse.currentPeriod.length).to.be( - throughputResponse.previousPeriod.length - ); - }); + it('returns same number of buckets for both periods', () => { + expect(throughputResponse.currentPeriod.length).to.be( + throughputResponse.previousPeriod.length + ); + }); - it('has same mean value for both periods', () => { - const currentPeriodMean = meanBy( - throughputResponse.currentPeriod.filter( - (item) => isFiniteNumber(item.y) && item.y > 0 - ), - 'y' - ); - const previousPeriodMean = meanBy( - throughputResponse.previousPeriod.filter( - (item) => isFiniteNumber(item.y) && item.y > 0 - ), - 'y' - ); - const currentPeriod = throughputResponse.currentPeriod; - const bucketSize = currentPeriod[1].x - currentPeriod[0].x; - const durationAsMinutes = bucketSize / 1000 / 60; - [currentPeriodMean, previousPeriodMean].every((value) => - expect(roundNumber(value)).to.be.equal( - roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes) - ) - ); - }); + it('has same mean value for both periods', () => { + const currentPeriodMean = meanBy( + throughputResponse.currentPeriod.filter((item) => isFiniteNumber(item.y) && item.y > 0), + 'y' + ); + const previousPeriodMean = meanBy( + throughputResponse.previousPeriod.filter( + (item) => isFiniteNumber(item.y) && item.y > 0 + ), + 'y' + ); + const currentPeriod = throughputResponse.currentPeriod; + const bucketSize = currentPeriod[1].x - currentPeriod[0].x; + const durationAsMinutes = bucketSize / 1000 / 60; + [currentPeriodMean, previousPeriodMean].every((value) => + expect(roundNumber(value)).to.be.equal( + roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes) + ) + ); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts index 960c7e55f415340..898f12ceaeffb27 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts @@ -33,7 +33,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'APM Services Overview with a basic license when data is not generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { const response = await apmApiClient.readUser({ @@ -57,7 +57,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'APM Services Overview with a basic license when data is generated', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + { config: 'basic', archives: [] }, () => { let response: { status: number; diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts index e4960791eee5acd..696c57779fb30c3 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts @@ -386,73 +386,69 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte } ); - registry.when( - 'Agent configurations through fleet', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - const name = 'myservice'; - const environment = 'development'; - const testConfig = { - service: { name, environment }, - settings: { transaction_sample_rate: '0.9' }, - }; - - let agentConfiguration: - | APIReturnType<'GET /api/apm/settings/agent-configuration/view'> - | undefined; - - before(async () => { - log.debug('creating agent configuration'); - await createConfiguration(testConfig); - const { body } = await findExactConfiguration(name, environment); - agentConfiguration = body; - }); + registry.when('Agent configurations through fleet', { config: 'basic', archives: [] }, () => { + const name = 'myservice'; + const environment = 'development'; + const testConfig = { + service: { name, environment }, + settings: { transaction_sample_rate: '0.9' }, + }; + + let agentConfiguration: + | APIReturnType<'GET /api/apm/settings/agent-configuration/view'> + | undefined; + + before(async () => { + log.debug('creating agent configuration'); + await createConfiguration(testConfig); + const { body } = await findExactConfiguration(name, environment); + agentConfiguration = body; + }); - after(async () => { - await deleteConfiguration(testConfig); - }); + after(async () => { + await deleteConfiguration(testConfig); + }); - it(`should have 'applied_by_agent=false' when there are no agent config metrics for this etag`, async () => { - expect(agentConfiguration?.applied_by_agent).to.be(false); - }); + it(`should have 'applied_by_agent=false' when there are no agent config metrics for this etag`, async () => { + expect(agentConfiguration?.applied_by_agent).to.be(false); + }); - describe('when there are agent config metrics for this etag', () => { - before(async () => { - const start = new Date().getTime(); - const end = moment(start).add(15, 'minutes').valueOf(); - - await addAgentConfigMetrics({ - synthtraceEsClient, - start, - end, - etag: agentConfiguration?.etag, - }); + describe('when there are agent config metrics for this etag', () => { + before(async () => { + const start = new Date().getTime(); + const end = moment(start).add(15, 'minutes').valueOf(); + + await addAgentConfigMetrics({ + synthtraceEsClient, + start, + end, + etag: agentConfiguration?.etag, }); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - it(`should have 'applied_by_agent=true' when getting a config from all configurations`, async () => { - const { - body: { configurations }, - } = await getAllConfigurations(); + it(`should have 'applied_by_agent=true' when getting a config from all configurations`, async () => { + const { + body: { configurations }, + } = await getAllConfigurations(); - const updatedConfig = configurations.find( - (x) => x.service.name === name && x.service.environment === environment - ); + const updatedConfig = configurations.find( + (x) => x.service.name === name && x.service.environment === environment + ); - expect(updatedConfig?.applied_by_agent).to.be(true); - }); + expect(updatedConfig?.applied_by_agent).to.be(true); + }); - it(`should have 'applied_by_agent=true' when getting a single config`, async () => { - const { - body: { applied_by_agent: appliedByAgent }, - } = await findExactConfiguration(name, environment); + it(`should have 'applied_by_agent=true' when getting a single config`, async () => { + const { + body: { applied_by_agent: appliedByAgent }, + } = await findExactConfiguration(name, environment); - expect(appliedByAgent).to.be(true); - }); + expect(appliedByAgent).to.be(true); }); - } - ); + }); + }); registry.when( 'agent configuration when data is loaded', diff --git a/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts b/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts index 3c9e669db62d433..09ca2f454f1e6c9 100644 --- a/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts +++ b/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts @@ -18,479 +18,467 @@ export default function ApiTest({ getService }: FtrProviderContext) { const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; - registry.when( - 'contains linked children', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - let ids: ReturnType['ids']; - - before(async () => { - const spanLinksData = generateSpanLinksData(); - - ids = spanLinksData.ids; - - await synthtraceEsClient.index( - new EntityArrayIterable(spanLinksData.apmFields.producerInternalOnly).merge( - new EntityArrayIterable(spanLinksData.apmFields.producerExternalOnly), - new EntityArrayIterable(spanLinksData.apmFields.producerConsumer), - new EntityArrayIterable(spanLinksData.apmFields.producerMultiple) - ) - ); - }); - - after(() => synthtraceEsClient.clean()); - - describe('Span links count on traces', () => { - async function fetchTraces({ traceId }: { traceId: string }) { - return await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/{traceId}`, - params: { - path: { traceId }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - }, + registry.when('contains linked children', { config: 'basic', archives: [] }, () => { + let ids: ReturnType['ids']; + + before(async () => { + const spanLinksData = generateSpanLinksData(); + + ids = spanLinksData.ids; + + await synthtraceEsClient.index( + new EntityArrayIterable(spanLinksData.apmFields.producerInternalOnly).merge( + new EntityArrayIterable(spanLinksData.apmFields.producerExternalOnly), + new EntityArrayIterable(spanLinksData.apmFields.producerConsumer), + new EntityArrayIterable(spanLinksData.apmFields.producerMultiple) + ) + ); + }); + + after(() => synthtraceEsClient.clean()); + + describe('Span links count on traces', () => { + async function fetchTraces({ traceId }: { traceId: string }) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}`, + params: { + path: { traceId }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), }, - }); - } + }, + }); + } - describe('producer-internal-only trace', () => { - let traces: Awaited>['body']; - before(async () => { - const tracesResponse = await fetchTraces({ traceId: ids.producerInternalOnly.traceId }); - traces = tracesResponse.body; - }); + describe('producer-internal-only trace', () => { + let traces: Awaited>['body']; + before(async () => { + const tracesResponse = await fetchTraces({ traceId: ids.producerInternalOnly.traceId }); + traces = tracesResponse.body; + }); - it('contains two children link on Span A', () => { - expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(1); - expect( - traces.linkedChildrenOfSpanCountBySpanId[ids.producerInternalOnly.spanAId] - ).to.equal(2); - }); + it('contains two children link on Span A', () => { + expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(1); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerInternalOnly.spanAId] + ).to.equal(2); }); + }); - describe('producer-external-only trace', () => { - let traces: Awaited>['body']; - before(async () => { - const tracesResponse = await fetchTraces({ traceId: ids.producerExternalOnly.traceId }); - traces = tracesResponse.body; - }); + describe('producer-external-only trace', () => { + let traces: Awaited>['body']; + before(async () => { + const tracesResponse = await fetchTraces({ traceId: ids.producerExternalOnly.traceId }); + traces = tracesResponse.body; + }); - it('contains two children link on Span B', () => { - expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(2); - expect( - traces.linkedChildrenOfSpanCountBySpanId[ids.producerExternalOnly.spanBId] - ).to.equal(1); - expect( - traces.linkedChildrenOfSpanCountBySpanId[ids.producerExternalOnly.transactionBId] - ).to.equal(1); - }); + it('contains two children link on Span B', () => { + expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(2); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerExternalOnly.spanBId] + ).to.equal(1); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerExternalOnly.transactionBId] + ).to.equal(1); }); + }); - describe('producer-consumer trace', () => { - let traces: Awaited>['body']; - before(async () => { - const tracesResponse = await fetchTraces({ traceId: ids.producerConsumer.traceId }); - traces = tracesResponse.body; - }); + describe('producer-consumer trace', () => { + let traces: Awaited>['body']; + before(async () => { + const tracesResponse = await fetchTraces({ traceId: ids.producerConsumer.traceId }); + traces = tracesResponse.body; + }); - it('contains one children link on transaction C and two on span C', () => { - expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(2); - expect( - traces.linkedChildrenOfSpanCountBySpanId[ids.producerConsumer.transactionCId] - ).to.equal(1); - expect(traces.linkedChildrenOfSpanCountBySpanId[ids.producerConsumer.spanCId]).to.equal( - 1 - ); - }); + it('contains one children link on transaction C and two on span C', () => { + expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(2); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerConsumer.transactionCId] + ).to.equal(1); + expect(traces.linkedChildrenOfSpanCountBySpanId[ids.producerConsumer.spanCId]).to.equal( + 1 + ); }); + }); - describe('consumer-multiple trace', () => { - let traces: Awaited>['body']; - before(async () => { - const tracesResponse = await fetchTraces({ traceId: ids.producerMultiple.traceId }); - traces = tracesResponse.body; - }); + describe('consumer-multiple trace', () => { + let traces: Awaited>['body']; + before(async () => { + const tracesResponse = await fetchTraces({ traceId: ids.producerMultiple.traceId }); + traces = tracesResponse.body; + }); - it('contains no children', () => { - expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(0); - expect( - traces.linkedChildrenOfSpanCountBySpanId[ids.producerMultiple.transactionDId] - ).to.equal(undefined); - expect(traces.linkedChildrenOfSpanCountBySpanId[ids.producerMultiple.spanEId]).to.equal( - undefined - ); - }); + it('contains no children', () => { + expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(0); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerMultiple.transactionDId] + ).to.equal(undefined); + expect(traces.linkedChildrenOfSpanCountBySpanId[ids.producerMultiple.spanEId]).to.equal( + undefined + ); }); }); - - describe('Span links details', () => { - async function fetchChildrenAndParentsDetails({ - kuery, - traceId, - spanId, - processorEvent, - }: { - kuery: string; - traceId: string; - spanId: string; - processorEvent: ProcessorEvent; - }) { - const [childrenLinksResponse, parentsLinksResponse] = await Promise.all([ - await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/children', - params: { - path: { traceId, spanId }, - query: { - kuery, - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - }, + }); + + describe('Span links details', () => { + async function fetchChildrenAndParentsDetails({ + kuery, + traceId, + spanId, + processorEvent, + }: { + kuery: string; + traceId: string; + spanId: string; + processorEvent: ProcessorEvent; + }) { + const [childrenLinksResponse, parentsLinksResponse] = await Promise.all([ + await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/children', + params: { + path: { traceId, spanId }, + query: { + kuery, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), }, - }), - apmApiClient.readUser({ - endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents', - params: { - path: { traceId, spanId }, - query: { - kuery, - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - processorEvent, - }, + }, + }), + apmApiClient.readUser({ + endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents', + params: { + path: { traceId, spanId }, + query: { + kuery, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + processorEvent, }, + }, + }), + ]); + + return { + childrenLinks: childrenLinksResponse.body, + parentsLinks: parentsLinksResponse.body, + }; + } + + describe('producer-internal-only span links details', () => { + let transactionALinksDetails: Awaited>; + let spanALinksDetails: Awaited>; + before(async () => { + const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all([ + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerInternalOnly.traceId, + spanId: ids.producerInternalOnly.transactionAId, + processorEvent: ProcessorEvent.transaction, + }), + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerInternalOnly.traceId, + spanId: ids.producerInternalOnly.spanAId, + processorEvent: ProcessorEvent.span, }), ]); + transactionALinksDetails = transactionALinksDetailsResponse; + spanALinksDetails = spanALinksDetailsResponse; + }); - return { - childrenLinks: childrenLinksResponse.body, - parentsLinks: parentsLinksResponse.body, - }; - } - - describe('producer-internal-only span links details', () => { - let transactionALinksDetails: Awaited>; - let spanALinksDetails: Awaited>; - before(async () => { - const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all( - [ - fetchChildrenAndParentsDetails({ - kuery: '', - traceId: ids.producerInternalOnly.traceId, - spanId: ids.producerInternalOnly.transactionAId, - processorEvent: ProcessorEvent.transaction, - }), - fetchChildrenAndParentsDetails({ - kuery: '', - traceId: ids.producerInternalOnly.traceId, - spanId: ids.producerInternalOnly.spanAId, - processorEvent: ProcessorEvent.span, - }), - ] - ); - transactionALinksDetails = transactionALinksDetailsResponse; - spanALinksDetails = spanALinksDetailsResponse; - }); + it('returns no links for transaction A', () => { + expect(transactionALinksDetails.childrenLinks.spanLinksDetails).to.eql([]); + expect(transactionALinksDetails.parentsLinks.spanLinksDetails).to.eql([]); + }); - it('returns no links for transaction A', () => { - expect(transactionALinksDetails.childrenLinks.spanLinksDetails).to.eql([]); - expect(transactionALinksDetails.parentsLinks.spanLinksDetails).to.eql([]); - }); + it('returns no parents on Span A', () => { + expect(spanALinksDetails.parentsLinks.spanLinksDetails).to.eql([]); + }); - it('returns no parents on Span A', () => { - expect(spanALinksDetails.parentsLinks.spanLinksDetails).to.eql([]); + it('returns two children on Span A', () => { + expect(spanALinksDetails.childrenLinks.spanLinksDetails.length).to.eql(2); + const serviceCDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find( + (childDetails) => { + return ( + childDetails.traceId === ids.producerConsumer.traceId && + childDetails.spanId === ids.producerConsumer.transactionCId + ); + } + ); + expect(serviceCDetails?.details).to.eql({ + serviceName: 'producer-consumer', + agentName: 'ruby', + transactionId: ids.producerConsumer.transactionCId, + spanName: 'Transaction C', + duration: 1000000, }); - it('returns two children on Span A', () => { - expect(spanALinksDetails.childrenLinks.spanLinksDetails.length).to.eql(2); - const serviceCDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find( - (childDetails) => { - return ( - childDetails.traceId === ids.producerConsumer.traceId && - childDetails.spanId === ids.producerConsumer.transactionCId - ); - } - ); - expect(serviceCDetails?.details).to.eql({ - serviceName: 'producer-consumer', - agentName: 'ruby', - transactionId: ids.producerConsumer.transactionCId, - spanName: 'Transaction C', - duration: 1000000, - }); - - const serviceDDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find( - (childDetails) => { - return ( - childDetails.traceId === ids.producerMultiple.traceId && - childDetails.spanId === ids.producerMultiple.transactionDId - ); - } - ); - expect(serviceDDetails?.details).to.eql({ - serviceName: 'consumer-multiple', - agentName: 'nodejs', - transactionId: ids.producerMultiple.transactionDId, - spanName: 'Transaction D', - duration: 1000000, - }); + const serviceDDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find( + (childDetails) => { + return ( + childDetails.traceId === ids.producerMultiple.traceId && + childDetails.spanId === ids.producerMultiple.transactionDId + ); + } + ); + expect(serviceDDetails?.details).to.eql({ + serviceName: 'consumer-multiple', + agentName: 'nodejs', + transactionId: ids.producerMultiple.transactionDId, + spanName: 'Transaction D', + duration: 1000000, }); }); + }); - describe('producer-external-only span links details', () => { - let transactionBLinksDetails: Awaited>; - let spanBLinksDetails: Awaited>; - before(async () => { - const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all( - [ - fetchChildrenAndParentsDetails({ - kuery: '', - traceId: ids.producerExternalOnly.traceId, - spanId: ids.producerExternalOnly.transactionBId, - processorEvent: ProcessorEvent.transaction, - }), - fetchChildrenAndParentsDetails({ - kuery: '', - traceId: ids.producerExternalOnly.traceId, - spanId: ids.producerExternalOnly.spanBId, - processorEvent: ProcessorEvent.span, - }), - ] - ); - transactionBLinksDetails = transactionALinksDetailsResponse; - spanBLinksDetails = spanALinksDetailsResponse; - }); + describe('producer-external-only span links details', () => { + let transactionBLinksDetails: Awaited>; + let spanBLinksDetails: Awaited>; + before(async () => { + const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all([ + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerExternalOnly.traceId, + spanId: ids.producerExternalOnly.transactionBId, + processorEvent: ProcessorEvent.transaction, + }), + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerExternalOnly.traceId, + spanId: ids.producerExternalOnly.spanBId, + processorEvent: ProcessorEvent.span, + }), + ]); + transactionBLinksDetails = transactionALinksDetailsResponse; + spanBLinksDetails = spanALinksDetailsResponse; + }); - it('returns producer-consumer as children of transaction B', () => { - expect(transactionBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); - }); + it('returns producer-consumer as children of transaction B', () => { + expect(transactionBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); + }); - it('returns no parent for transaction B', () => { - expect(transactionBLinksDetails.parentsLinks.spanLinksDetails).to.eql([]); - }); + it('returns no parent for transaction B', () => { + expect(transactionBLinksDetails.parentsLinks.spanLinksDetails).to.eql([]); + }); - it('returns external parent on Span B', () => { - expect(spanBLinksDetails.parentsLinks.spanLinksDetails.length).to.be(1); - expect(spanBLinksDetails.parentsLinks.spanLinksDetails).to.eql([ - { traceId: 'trace#1', spanId: 'span#1' }, - ]); - }); + it('returns external parent on Span B', () => { + expect(spanBLinksDetails.parentsLinks.spanLinksDetails.length).to.be(1); + expect(spanBLinksDetails.parentsLinks.spanLinksDetails).to.eql([ + { traceId: 'trace#1', spanId: 'span#1' }, + ]); + }); - it('returns consumer-multiple as child on Span B', () => { - expect(spanBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); - expect(spanBLinksDetails.childrenLinks.spanLinksDetails).to.eql([ - { - traceId: ids.producerMultiple.traceId, - spanId: ids.producerMultiple.spanEId, - details: { - serviceName: 'consumer-multiple', - agentName: 'nodejs', - transactionId: ids.producerMultiple.transactionDId, - spanName: 'Span E', - duration: 100000, - spanSubtype: 'http', - spanType: 'external', - }, + it('returns consumer-multiple as child on Span B', () => { + expect(spanBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); + expect(spanBLinksDetails.childrenLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.spanEId, + details: { + serviceName: 'consumer-multiple', + agentName: 'nodejs', + transactionId: ids.producerMultiple.transactionDId, + spanName: 'Span E', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', }, - ]); - }); + }, + ]); }); + }); - describe('producer-consumer span links details', () => { - let transactionCLinksDetails: Awaited>; - let spanCLinksDetails: Awaited>; - before(async () => { - const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all( - [ - fetchChildrenAndParentsDetails({ - kuery: '', - traceId: ids.producerConsumer.traceId, - spanId: ids.producerConsumer.transactionCId, - processorEvent: ProcessorEvent.transaction, - }), - fetchChildrenAndParentsDetails({ - kuery: '', - traceId: ids.producerConsumer.traceId, - spanId: ids.producerConsumer.spanCId, - processorEvent: ProcessorEvent.span, - }), - ] - ); - transactionCLinksDetails = transactionALinksDetailsResponse; - spanCLinksDetails = spanALinksDetailsResponse; - }); + describe('producer-consumer span links details', () => { + let transactionCLinksDetails: Awaited>; + let spanCLinksDetails: Awaited>; + before(async () => { + const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all([ + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerConsumer.traceId, + spanId: ids.producerConsumer.transactionCId, + processorEvent: ProcessorEvent.transaction, + }), + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerConsumer.traceId, + spanId: ids.producerConsumer.spanCId, + processorEvent: ProcessorEvent.span, + }), + ]); + transactionCLinksDetails = transactionALinksDetailsResponse; + spanCLinksDetails = spanALinksDetailsResponse; + }); - it('returns producer-internal-only Span A, producer-external-only Transaction B, and External link as parents of Transaction C', () => { - expect(transactionCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(3); - expect(transactionCLinksDetails.parentsLinks.spanLinksDetails).to.eql([ - { - traceId: ids.producerInternalOnly.traceId, - spanId: ids.producerInternalOnly.spanAId, - details: { - serviceName: 'producer-internal-only', - agentName: 'go', - transactionId: ids.producerInternalOnly.transactionAId, - spanName: 'Span A', - duration: 100000, - spanSubtype: 'http', - spanType: 'external', - }, - }, - { - traceId: ids.producerExternalOnly.traceId, - spanId: ids.producerExternalOnly.transactionBId, - details: { - serviceName: 'producer-external-only', - agentName: 'java', - transactionId: ids.producerExternalOnly.transactionBId, - duration: 1000000, - spanName: 'Transaction B', - }, + it('returns producer-internal-only Span A, producer-external-only Transaction B, and External link as parents of Transaction C', () => { + expect(transactionCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(3); + expect(transactionCLinksDetails.parentsLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerInternalOnly.traceId, + spanId: ids.producerInternalOnly.spanAId, + details: { + serviceName: 'producer-internal-only', + agentName: 'go', + transactionId: ids.producerInternalOnly.transactionAId, + spanName: 'Span A', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', }, - { - traceId: ids.producerConsumer.externalTraceId, - spanId: ids.producerExternalOnly.spanBId, + }, + { + traceId: ids.producerExternalOnly.traceId, + spanId: ids.producerExternalOnly.transactionBId, + details: { + serviceName: 'producer-external-only', + agentName: 'java', + transactionId: ids.producerExternalOnly.transactionBId, + duration: 1000000, + spanName: 'Transaction B', }, - ]); - }); + }, + { + traceId: ids.producerConsumer.externalTraceId, + spanId: ids.producerExternalOnly.spanBId, + }, + ]); + }); - it('returns consumer-multiple Span E as child of Transaction C', () => { - expect(transactionCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); - expect(transactionCLinksDetails.childrenLinks.spanLinksDetails).to.eql([ - { - traceId: ids.producerMultiple.traceId, - spanId: ids.producerMultiple.spanEId, - details: { - serviceName: 'consumer-multiple', - agentName: 'nodejs', - transactionId: ids.producerMultiple.transactionDId, - spanName: 'Span E', - duration: 100000, - spanSubtype: 'http', - spanType: 'external', - }, + it('returns consumer-multiple Span E as child of Transaction C', () => { + expect(transactionCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); + expect(transactionCLinksDetails.childrenLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.spanEId, + details: { + serviceName: 'consumer-multiple', + agentName: 'nodejs', + transactionId: ids.producerMultiple.transactionDId, + spanName: 'Span E', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', }, - ]); - }); + }, + ]); + }); - it('returns no child on Span C', () => { - expect(spanCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(0); - }); + it('returns no child on Span C', () => { + expect(spanCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(0); + }); - it('returns consumer-multiple as Child on producer-consumer', () => { - expect(spanCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); - expect(spanCLinksDetails.childrenLinks.spanLinksDetails).to.eql([ - { - traceId: ids.producerMultiple.traceId, - spanId: ids.producerMultiple.transactionDId, - details: { - serviceName: 'consumer-multiple', - agentName: 'nodejs', - transactionId: ids.producerMultiple.transactionDId, - spanName: 'Transaction D', - duration: 1000000, - }, + it('returns consumer-multiple as Child on producer-consumer', () => { + expect(spanCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); + expect(spanCLinksDetails.childrenLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.transactionDId, + details: { + serviceName: 'consumer-multiple', + agentName: 'nodejs', + transactionId: ids.producerMultiple.transactionDId, + spanName: 'Transaction D', + duration: 1000000, }, - ]); - }); + }, + ]); }); + }); - describe('consumer-multiple span links details', () => { - let transactionDLinksDetails: Awaited>; - let spanELinksDetails: Awaited>; - before(async () => { - const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all( - [ - fetchChildrenAndParentsDetails({ - kuery: '', - traceId: ids.producerMultiple.traceId, - spanId: ids.producerMultiple.transactionDId, - processorEvent: ProcessorEvent.transaction, - }), - fetchChildrenAndParentsDetails({ - kuery: '', - traceId: ids.producerMultiple.traceId, - spanId: ids.producerMultiple.spanEId, - processorEvent: ProcessorEvent.span, - }), - ] - ); - transactionDLinksDetails = transactionALinksDetailsResponse; - spanELinksDetails = spanALinksDetailsResponse; - }); + describe('consumer-multiple span links details', () => { + let transactionDLinksDetails: Awaited>; + let spanELinksDetails: Awaited>; + before(async () => { + const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all([ + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.transactionDId, + processorEvent: ProcessorEvent.transaction, + }), + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.spanEId, + processorEvent: ProcessorEvent.span, + }), + ]); + transactionDLinksDetails = transactionALinksDetailsResponse; + spanELinksDetails = spanALinksDetailsResponse; + }); - it('returns producer-internal-only Span A and producer-consumer Span C as parents of Transaction D', () => { - expect(transactionDLinksDetails.parentsLinks.spanLinksDetails.length).to.be(2); - expect(transactionDLinksDetails.parentsLinks.spanLinksDetails).to.eql([ - { - traceId: ids.producerInternalOnly.traceId, - spanId: ids.producerInternalOnly.spanAId, - details: { - serviceName: 'producer-internal-only', - agentName: 'go', - transactionId: ids.producerInternalOnly.transactionAId, - spanName: 'Span A', - duration: 100000, - spanSubtype: 'http', - spanType: 'external', - }, + it('returns producer-internal-only Span A and producer-consumer Span C as parents of Transaction D', () => { + expect(transactionDLinksDetails.parentsLinks.spanLinksDetails.length).to.be(2); + expect(transactionDLinksDetails.parentsLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerInternalOnly.traceId, + spanId: ids.producerInternalOnly.spanAId, + details: { + serviceName: 'producer-internal-only', + agentName: 'go', + transactionId: ids.producerInternalOnly.transactionAId, + spanName: 'Span A', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', }, - { - traceId: ids.producerConsumer.traceId, - spanId: ids.producerConsumer.spanCId, - details: { - serviceName: 'producer-consumer', - agentName: 'ruby', - transactionId: ids.producerConsumer.transactionCId, - spanName: 'Span C', - duration: 100000, - spanSubtype: 'http', - spanType: 'external', - }, + }, + { + traceId: ids.producerConsumer.traceId, + spanId: ids.producerConsumer.spanCId, + details: { + serviceName: 'producer-consumer', + agentName: 'ruby', + transactionId: ids.producerConsumer.transactionCId, + spanName: 'Span C', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', }, - ]); - }); + }, + ]); + }); - it('returns no children on Transaction D', () => { - expect(transactionDLinksDetails.childrenLinks.spanLinksDetails.length).to.be(0); - }); + it('returns no children on Transaction D', () => { + expect(transactionDLinksDetails.childrenLinks.spanLinksDetails.length).to.be(0); + }); - it('returns producer-external-only Span B and producer-consumer Transaction C as parents of Span E', () => { - expect(spanELinksDetails.parentsLinks.spanLinksDetails.length).to.be(2); - - expect(spanELinksDetails.parentsLinks.spanLinksDetails).to.eql([ - { - traceId: ids.producerExternalOnly.traceId, - spanId: ids.producerExternalOnly.spanBId, - details: { - serviceName: 'producer-external-only', - agentName: 'java', - transactionId: ids.producerExternalOnly.transactionBId, - spanName: 'Span B', - duration: 100000, - spanSubtype: 'http', - spanType: 'external', - }, + it('returns producer-external-only Span B and producer-consumer Transaction C as parents of Span E', () => { + expect(spanELinksDetails.parentsLinks.spanLinksDetails.length).to.be(2); + + expect(spanELinksDetails.parentsLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerExternalOnly.traceId, + spanId: ids.producerExternalOnly.spanBId, + details: { + serviceName: 'producer-external-only', + agentName: 'java', + transactionId: ids.producerExternalOnly.transactionBId, + spanName: 'Span B', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', }, - { - traceId: ids.producerConsumer.traceId, - spanId: ids.producerConsumer.transactionCId, - details: { - serviceName: 'producer-consumer', - agentName: 'ruby', - transactionId: ids.producerConsumer.transactionCId, - spanName: 'Transaction C', - duration: 1000000, - }, + }, + { + traceId: ids.producerConsumer.traceId, + spanId: ids.producerConsumer.transactionCId, + details: { + serviceName: 'producer-consumer', + agentName: 'ruby', + transactionId: ids.producerConsumer.transactionCId, + spanName: 'Transaction C', + duration: 1000000, }, - ]); - }); + }, + ]); + }); - it('returns no children on Span E', () => { - expect(spanELinksDetails.childrenLinks.spanLinksDetails.length).to.be(0); - }); + it('returns no children on Span E', () => { + expect(spanELinksDetails.childrenLinks.spanLinksDetails.length).to.be(0); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts index 58be27fe311ae03..c990a7c632caa97 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts @@ -93,149 +93,145 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputValues: Awaited>; - registry.when( - 'Dependencies throughput value', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded', () => { - const GO_PROD_RATE = 75; - const JAVA_PROD_RATE = 25; + registry.when('Dependencies throughput value', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + const GO_PROD_RATE = 75; + const JAVA_PROD_RATE = 25; + before(async () => { + const serviceGoProdInstance = apm + .service('synth-go', 'production', 'go') + .instance('instance-a'); + const serviceJavaInstance = apm + .service('synth-java', 'development', 'java') + .instance('instance-c'); + + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction('GET /api/product/list') + .duration(1000) + .timestamp(timestamp) + .children( + serviceGoProdInstance + .span('GET apm-*/_search', 'db', 'elasticsearch') + .duration(1000) + .success() + .destination('elasticsearch') + .timestamp(timestamp), + serviceGoProdInstance + .span('custom_operation', 'app') + .duration(550) + .children( + serviceGoProdInstance + .span('SELECT FROM products', 'db', 'postgresql') + .duration(500) + .success() + .destination('postgresql') + .timestamp(timestamp) + ) + .success() + .timestamp(timestamp) + ) + ), + timerange(start, end) + .interval('1m') + .rate(JAVA_PROD_RATE) + .generator((timestamp) => + serviceJavaInstance + .transaction('POST /api/product/buy') + .duration(1000) + .timestamp(timestamp) + .children( + serviceJavaInstance + .span('GET apm-*/_search', 'db', 'elasticsearch') + .duration(1000) + .success() + .destination('elasticsearch') + .timestamp(timestamp), + serviceJavaInstance + .span('custom_operation', 'app') + .duration(50) + .success() + .timestamp(timestamp) + ) + ), + ]); + }); + + after(() => synthtraceEsClient.clean()); + + describe('verify top dependencies', () => { before(async () => { - const serviceGoProdInstance = apm - .service('synth-go', 'production', 'go') - .instance('instance-a'); - const serviceJavaInstance = apm - .service('synth-java', 'development', 'java') - .instance('instance-c'); - - await synthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction('GET /api/product/list') - .duration(1000) - .timestamp(timestamp) - .children( - serviceGoProdInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') - .duration(1000) - .success() - .destination('elasticsearch') - .timestamp(timestamp), - serviceGoProdInstance - .span('custom_operation', 'app') - .duration(550) - .children( - serviceGoProdInstance - .span('SELECT FROM products', 'db', 'postgresql') - .duration(500) - .success() - .destination('postgresql') - .timestamp(timestamp) - ) - .success() - .timestamp(timestamp) - ) - ), - timerange(start, end) - .interval('1m') - .rate(JAVA_PROD_RATE) - .generator((timestamp) => - serviceJavaInstance - .transaction('POST /api/product/buy') - .duration(1000) - .timestamp(timestamp) - .children( - serviceJavaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') - .duration(1000) - .success() - .destination('elasticsearch') - .timestamp(timestamp), - serviceJavaInstance - .span('custom_operation', 'app') - .duration(50) - .success() - .timestamp(timestamp) - ) - ), - ]); + throughputValues = await getThroughputValues(); }); - after(() => synthtraceEsClient.clean()); + it('returns elasticsearch and postgresql as dependencies', () => { + const { topDependencies } = throughputValues; + const topDependenciesAsObj = Object.fromEntries(topDependencies); + expect(topDependenciesAsObj.elasticsearch).to.equal( + roundNumber(JAVA_PROD_RATE + GO_PROD_RATE) + ); + expect(topDependenciesAsObj.postgresql).to.equal(roundNumber(GO_PROD_RATE)); + }); + }); - describe('verify top dependencies', () => { + describe('compare throughput value between top backends, backend throughput chart and upstream services apis', () => { + describe('elasticsearch dependency', () => { before(async () => { - throughputValues = await getThroughputValues(); + throughputValues = await getThroughputValues({ dependencyName: 'elasticsearch' }); + }); + + it('matches throughput values between throughput chart and top dependency', () => { + const { topDependencies, dependencyThroughputChartMean } = throughputValues; + const topDependenciesAsObj = Object.fromEntries(topDependencies); + const elasticsearchDependency = topDependenciesAsObj.elasticsearch; + [elasticsearchDependency, dependencyThroughputChartMean].forEach((value) => + expect(value).to.be.equal(roundNumber(JAVA_PROD_RATE + GO_PROD_RATE)) + ); }); - it('returns elasticsearch and postgresql as dependencies', () => { - const { topDependencies } = throughputValues; + it('matches throughput values between upstream services and top dependency', () => { + const { topDependencies, upstreamServicesThroughput } = throughputValues; const topDependenciesAsObj = Object.fromEntries(topDependencies); - expect(topDependenciesAsObj.elasticsearch).to.equal( - roundNumber(JAVA_PROD_RATE + GO_PROD_RATE) + const elasticsearchDependency = topDependenciesAsObj.elasticsearch; + const upstreamServiceThroughputSum = roundNumber( + sumBy(upstreamServicesThroughput, 'throughput') + ); + [elasticsearchDependency, upstreamServiceThroughputSum].forEach((value) => + expect(value).to.be.equal(roundNumber(JAVA_PROD_RATE + GO_PROD_RATE)) ); - expect(topDependenciesAsObj.postgresql).to.equal(roundNumber(GO_PROD_RATE)); }); }); + describe('postgresql dependency', () => { + before(async () => { + throughputValues = await getThroughputValues({ dependencyName: 'postgresql' }); + }); - describe('compare throughput value between top backends, backend throughput chart and upstream services apis', () => { - describe('elasticsearch dependency', () => { - before(async () => { - throughputValues = await getThroughputValues({ dependencyName: 'elasticsearch' }); - }); - - it('matches throughput values between throughput chart and top dependency', () => { - const { topDependencies, dependencyThroughputChartMean } = throughputValues; - const topDependenciesAsObj = Object.fromEntries(topDependencies); - const elasticsearchDependency = topDependenciesAsObj.elasticsearch; - [elasticsearchDependency, dependencyThroughputChartMean].forEach((value) => - expect(value).to.be.equal(roundNumber(JAVA_PROD_RATE + GO_PROD_RATE)) - ); - }); - - it('matches throughput values between upstream services and top dependency', () => { - const { topDependencies, upstreamServicesThroughput } = throughputValues; - const topDependenciesAsObj = Object.fromEntries(topDependencies); - const elasticsearchDependency = topDependenciesAsObj.elasticsearch; - const upstreamServiceThroughputSum = roundNumber( - sumBy(upstreamServicesThroughput, 'throughput') - ); - [elasticsearchDependency, upstreamServiceThroughputSum].forEach((value) => - expect(value).to.be.equal(roundNumber(JAVA_PROD_RATE + GO_PROD_RATE)) - ); - }); + it('matches throughput values between throughput chart and top dependency', () => { + const { topDependencies, dependencyThroughputChartMean } = throughputValues; + const topDependenciesAsObj = Object.fromEntries(topDependencies); + const postgresqlDependency = topDependenciesAsObj.postgresql; + [postgresqlDependency, dependencyThroughputChartMean].forEach((value) => + expect(value).to.be.equal(roundNumber(GO_PROD_RATE)) + ); }); - describe('postgresql dependency', () => { - before(async () => { - throughputValues = await getThroughputValues({ dependencyName: 'postgresql' }); - }); - - it('matches throughput values between throughput chart and top dependency', () => { - const { topDependencies, dependencyThroughputChartMean } = throughputValues; - const topDependenciesAsObj = Object.fromEntries(topDependencies); - const postgresqlDependency = topDependenciesAsObj.postgresql; - [postgresqlDependency, dependencyThroughputChartMean].forEach((value) => - expect(value).to.be.equal(roundNumber(GO_PROD_RATE)) - ); - }); - - it('matches throughput values between upstream services and top dependency', () => { - const { topDependencies, upstreamServicesThroughput } = throughputValues; - const topDependenciesAsObj = Object.fromEntries(topDependencies); - const postgresqlDependency = topDependenciesAsObj.postgresql; - const upstreamServiceThroughputSum = roundNumber( - sumBy(upstreamServicesThroughput, 'throughput') - ); - [postgresqlDependency, upstreamServiceThroughputSum].forEach((value) => - expect(value).to.be.equal(roundNumber(GO_PROD_RATE)) - ); - }); + + it('matches throughput values between upstream services and top dependency', () => { + const { topDependencies, upstreamServicesThroughput } = throughputValues; + const topDependenciesAsObj = Object.fromEntries(topDependencies); + const postgresqlDependency = topDependenciesAsObj.postgresql; + const upstreamServiceThroughputSum = roundNumber( + sumBy(upstreamServicesThroughput, 'throughput') + ); + [postgresqlDependency, upstreamServiceThroughputSum].forEach((value) => + expect(value).to.be.equal(roundNumber(GO_PROD_RATE)) + ); }); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts index 41124abf10c5f45..ef091dc83a429e9 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts @@ -104,7 +104,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: Awaited>; let throughputTransactionValues: Awaited>; - registry.when('Services APIs', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { + registry.when('Services APIs', { config: 'basic', archives: [] }, () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts index c3dc9adcaef1958..fd775ec9af2a9e3 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts @@ -69,74 +69,67 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: Awaited>; let throughputTransactionValues: Awaited>; - registry.when( - 'Service maps APIs', - { config: 'trial', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('when data is loaded ', () => { - const GO_PROD_RATE = 80; - const GO_DEV_RATE = 20; + registry.when('Service maps APIs', { config: 'trial', archives: [] }, () => { + describe('when data is loaded ', () => { + const GO_PROD_RATE = 80; + const GO_DEV_RATE = 20; + before(async () => { + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); + const serviceGoDevInstance = apm + .service(serviceName, 'development', 'go') + .instance('instance-b'); + + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction('GET /apple 🍎 ', 'Worker') + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .generator((timestamp) => + serviceGoDevInstance.transaction('GET /apple 🍎 ').duration(1000).timestamp(timestamp) + ), + ]); + }); + + after(() => synthtraceEsClient.clean()); + + describe('compare throughput value between service inventory and service maps', () => { before(async () => { - const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') - .instance('instance-a'); - const serviceGoDevInstance = apm - .service(serviceName, 'development', 'go') - .instance('instance-b'); - - await synthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction('GET /apple 🍎 ', 'Worker') - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(GO_DEV_RATE) - .generator((timestamp) => - serviceGoDevInstance - .transaction('GET /apple 🍎 ') - .duration(1000) - .timestamp(timestamp) - ), + [throughputTransactionValues, throughputMetricValues] = await Promise.all([ + getThroughputValues('transaction'), + getThroughputValues('metric'), ]); }); - after(() => synthtraceEsClient.clean()); - - describe('compare throughput value between service inventory and service maps', () => { - before(async () => { - [throughputTransactionValues, throughputMetricValues] = await Promise.all([ - getThroughputValues('transaction'), - getThroughputValues('metric'), - ]); - }); - - it('returns same throughput value for Transaction-based and Metric-based data', () => { - [ - ...Object.values(throughputTransactionValues), - ...Object.values(throughputMetricValues), - ].forEach((value) => expect(roundNumber(value)).to.be.equal(GO_DEV_RATE)); - }); + it('returns same throughput value for Transaction-based and Metric-based data', () => { + [ + ...Object.values(throughputTransactionValues), + ...Object.values(throughputMetricValues), + ].forEach((value) => expect(roundNumber(value)).to.be.equal(GO_DEV_RATE)); + }); + }); + + describe('when calling service maps transactions stats api', () => { + let serviceMapsNodeThroughput: number | null | undefined; + before(async () => { + const response = await callApi(); + serviceMapsNodeThroughput = + response.body.currentPeriod.transactionStats?.throughput?.value; }); - describe('when calling service maps transactions stats api', () => { - let serviceMapsNodeThroughput: number | null | undefined; - before(async () => { - const response = await callApi(); - serviceMapsNodeThroughput = - response.body.currentPeriod.transactionStats?.throughput?.value; - }); - - it('returns expected throughput value', () => { - expect(roundNumber(serviceMapsNodeThroughput)).to.be.equal(GO_DEV_RATE); - }); + it('returns expected throughput value', () => { + expect(roundNumber(serviceMapsNodeThroughput)).to.be.equal(GO_DEV_RATE); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts index b71beb0513af21d..0c0696f801a9528 100644 --- a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts @@ -80,185 +80,146 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); } - registry.when( - 'Find traces when traces do not exist', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - it('handles empty state', async () => { - const response = await fetchTraceSamples({ - query: '', - type: TraceSearchType.kql, - environment: ENVIRONMENT_ALL.value, - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - samples: [], - }); + registry.when('Find traces when traces do not exist', { config: 'basic', archives: [] }, () => { + it('handles empty state', async () => { + const response = await fetchTraceSamples({ + query: '', + type: TraceSearchType.kql, + environment: ENVIRONMENT_ALL.value, }); - } - ); - - registry.when( - 'Find traces when traces exist', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - before(() => { - const java = apm.service('java', 'production', 'java').instance('java'); - - const node = apm.service('node', 'development', 'nodejs').instance('node'); - - const python = apm.service('python', 'production', 'python').instance('python'); - - function generateTrace( - timestamp: number, - order: Instance[], - db?: 'elasticsearch' | 'redis' - ) { - return order - .concat() - .reverse() - .reduce((prev, instance, index) => { - const invertedIndex = order.length - index - 1; - - const duration = 50; - const time = timestamp + invertedIndex * 10; - - const transaction: Transaction = instance - .transaction(`GET /${instance.fields['service.name']!}/api`) - .timestamp(time) - .duration(duration); - - if (prev) { - const next = order[invertedIndex + 1].fields['service.name']!; - transaction.children( - instance - .span(`GET ${next}/api`, 'external', 'http') - .destination(next) - .duration(duration) - .timestamp(time + 1) - .children(prev) - ); - } else if (db) { - transaction.children( - instance - .span(db, 'db', db) - .destination(db) - .duration(duration) - .timestamp(time + 1) - ); - } - return transaction; - }, undefined)!; - } - - return synthtraceEsClient.index( - timerange(start, end) - .interval('15m') - .rate(1) - .generator((timestamp) => { - return [ - generateTrace(timestamp, [java, node]), - generateTrace(timestamp, [node, java], 'redis'), - generateTrace(timestamp, [python], 'redis'), - generateTrace(timestamp, [python, node, java], 'elasticsearch'), - generateTrace(timestamp, [java, python, node]), - ]; - }) - ); + expect(response.status).to.be(200); + expect(response.body).to.eql({ + samples: [], }); + }); + }); + + registry.when('Find traces when traces exist', { config: 'basic', archives: [] }, () => { + before(() => { + const java = apm.service('java', 'production', 'java').instance('java'); + + const node = apm.service('node', 'development', 'nodejs').instance('node'); + + const python = apm.service('python', 'production', 'python').instance('python'); + + function generateTrace(timestamp: number, order: Instance[], db?: 'elasticsearch' | 'redis') { + return order + .concat() + .reverse() + .reduce((prev, instance, index) => { + const invertedIndex = order.length - index - 1; + + const duration = 50; + const time = timestamp + invertedIndex * 10; + + const transaction: Transaction = instance + .transaction(`GET /${instance.fields['service.name']!}/api`) + .timestamp(time) + .duration(duration); + + if (prev) { + const next = order[invertedIndex + 1].fields['service.name']!; + transaction.children( + instance + .span(`GET ${next}/api`, 'external', 'http') + .destination(next) + .duration(duration) + .timestamp(time + 1) + .children(prev) + ); + } else if (db) { + transaction.children( + instance + .span(db, 'db', db) + .destination(db) + .duration(duration) + .timestamp(time + 1) + ); + } - describe('when using KQL', () => { - describe('and the query is empty', () => { - it('returns all trace samples', async () => { - const { - body: { samples }, - } = await fetchTraceSamples({ - query: '', - type: TraceSearchType.kql, - environment: 'ENVIRONMENT_ALL', - }); + return transaction; + }, undefined)!; + } + + return synthtraceEsClient.index( + timerange(start, end) + .interval('15m') + .rate(1) + .generator((timestamp) => { + return [ + generateTrace(timestamp, [java, node]), + generateTrace(timestamp, [node, java], 'redis'), + generateTrace(timestamp, [python], 'redis'), + generateTrace(timestamp, [python, node, java], 'elasticsearch'), + generateTrace(timestamp, [java, python, node]), + ]; + }) + ); + }); - expect(samples.length).to.eql(5); + describe('when using KQL', () => { + describe('and the query is empty', () => { + it('returns all trace samples', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: '', + type: TraceSearchType.kql, + environment: 'ENVIRONMENT_ALL', }); - }); - - describe('and query is set', () => { - it('returns the relevant traces', async () => { - const { - body: { samples }, - } = await fetchTraceSamples({ - query: 'span.destination.service.resource:elasticsearch', - type: TraceSearchType.kql, - environment: 'ENVIRONMENT_ALL', - }); - expect(samples.length).to.eql(1); - }); + expect(samples.length).to.eql(5); }); }); - describe('when using EQL', () => { - describe('and the query is invalid', () => { - it.skip('returns a 400', async function () { - try { - await fetchTraceSamples({ - query: '', - type: TraceSearchType.eql, - environment: 'ENVIRONMENT_ALL', - }); - this.fail(); - } catch (error: unknown) { - const apiError = error as ApmApiError; - expect(apiError.res.status).to.eql(400); - } + describe('and query is set', () => { + it('returns the relevant traces', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: 'span.destination.service.resource:elasticsearch', + type: TraceSearchType.kql, + environment: 'ENVIRONMENT_ALL', }); + + expect(samples.length).to.eql(1); }); + }); + }); - describe('and the query is set', () => { - it('returns the correct trace samples for transaction sequences', async () => { - const { - body: { samples }, - } = await fetchTraceSamples({ - query: `sequence by trace.id - [ transaction where service.name == "java" ] - [ transaction where service.name == "node" ]`, + describe('when using EQL', () => { + describe('and the query is invalid', () => { + it.skip('returns a 400', async function () { + try { + await fetchTraceSamples({ + query: '', type: TraceSearchType.eql, environment: 'ENVIRONMENT_ALL', }); - - const traces = await fetchTraces(samples); - - expect(traces.length).to.eql(2); - - const mapped = traces.map((traceDocs) => { - return sortBy(traceDocs, '@timestamp') - .filter((doc) => doc.processor.event === 'transaction') - .map((doc) => doc.service.name); - }); - - expect(mapped).to.eql([ - ['java', 'node'], - ['java', 'python', 'node'], - ]); - }); + this.fail(); + } catch (error: unknown) { + const apiError = error as ApmApiError; + expect(apiError.res.status).to.eql(400); + } }); + }); - it('returns the correct trace samples for join sequences', async () => { + describe('and the query is set', () => { + it('returns the correct trace samples for transaction sequences', async () => { const { body: { samples }, } = await fetchTraceSamples({ query: `sequence by trace.id - [ span where service.name == "java" ] by span.id - [ transaction where service.name == "python" ] by parent.id`, + [ transaction where service.name == "java" ] + [ transaction where service.name == "node" ]`, type: TraceSearchType.eql, environment: 'ENVIRONMENT_ALL', }); const traces = await fetchTraces(samples); - expect(traces.length).to.eql(1); + expect(traces.length).to.eql(2); const mapped = traces.map((traceDocs) => { return sortBy(traceDocs, '@timestamp') @@ -266,42 +227,69 @@ export default function ApiTest({ getService }: FtrProviderContext) { .map((doc) => doc.service.name); }); - expect(mapped).to.eql([['java', 'python', 'node']]); + expect(mapped).to.eql([ + ['java', 'node'], + ['java', 'python', 'node'], + ]); }); + }); - it('returns the correct trace samples for exit spans', async () => { - const { - body: { samples }, - } = await fetchTraceSamples({ - query: `sequence by trace.id + it('returns the correct trace samples for join sequences', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: `sequence by trace.id + [ span where service.name == "java" ] by span.id + [ transaction where service.name == "python" ] by parent.id`, + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); + + const traces = await fetchTraces(samples); + + expect(traces.length).to.eql(1); + + const mapped = traces.map((traceDocs) => { + return sortBy(traceDocs, '@timestamp') + .filter((doc) => doc.processor.event === 'transaction') + .map((doc) => doc.service.name); + }); + + expect(mapped).to.eql([['java', 'python', 'node']]); + }); + + it('returns the correct trace samples for exit spans', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: `sequence by trace.id [ transaction where service.name == "python" ] [ span where span.destination.service.resource == "redis" ]`, - type: TraceSearchType.eql, - environment: 'ENVIRONMENT_ALL', - }); + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); - const traces = await fetchTraces(samples); + const traces = await fetchTraces(samples); - expect(traces.length).to.eql(1); + expect(traces.length).to.eql(1); - const mapped = traces.map((traceDocs) => { - return sortBy(traceDocs, '@timestamp') - .filter( - (doc) => doc.processor.event === 'transaction' || doc.processor.event === 'span' - ) - .map((doc) => { - if (doc.span && 'destination' in doc.span) { - return doc.span.destination!.service.resource; - } - return doc.service.name; - }); - }); - - expect(mapped).to.eql([['python', 'redis']]); + const mapped = traces.map((traceDocs) => { + return sortBy(traceDocs, '@timestamp') + .filter( + (doc) => doc.processor.event === 'transaction' || doc.processor.event === 'span' + ) + .map((doc) => { + if (doc.span && 'destination' in doc.span) { + return doc.span.destination!.service.resource; + } + return doc.service.name; + }); }); + + expect(mapped).to.eql([['python', 'redis']]); }); + }); - after(() => synthtraceEsClient.clean()); - } - ); + after(() => synthtraceEsClient.clean()); + }); } diff --git a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts index 108772a57bc26e1..509d70caf529167 100644 --- a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts @@ -52,7 +52,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when('Trace exists', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { + registry.when('Trace exists', { config: 'basic', archives: [] }, () => { let serviceATraceId: string; before(async () => { const instanceJava = apm.service('synth-apple', 'production', 'java').instance('instance-b'); diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts index 4bbc2490dcd4b4e..350830abcbba3a4 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts @@ -75,178 +75,172 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'data is loaded', - { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, - () => { - describe('transactions groups detailed stats', () => { - const GO_PROD_RATE = 75; - const GO_PROD_ERROR_RATE = 25; + registry.when('data is loaded', { config: 'basic', archives: [] }, () => { + describe('transactions groups detailed stats', () => { + const GO_PROD_RATE = 75; + const GO_PROD_ERROR_RATE = 25; + before(async () => { + const serviceGoProdInstance = apm + .service(serviceName, 'production', 'go') + .instance('instance-a'); + + const transactionName = 'GET /api/product/list'; + + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction(transactionName) + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(GO_PROD_ERROR_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction(transactionName) + .duration(1000) + .timestamp(timestamp) + .failure() + ), + ]); + }); + + after(() => synthtraceEsClient.clean()); + + describe('without comparisons', () => { + let transactionsStatistics: TransactionsGroupsDetailedStatistics; + let metricsStatistics: TransactionsGroupsDetailedStatistics; before(async () => { - const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') - .instance('instance-a'); - - const transactionName = 'GET /api/product/list'; - - await synthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction(transactionName) - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(GO_PROD_ERROR_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction(transactionName) - .duration(1000) - .timestamp(timestamp) - .failure() - ), + [metricsStatistics, transactionsStatistics] = await Promise.all([ + callApi({ query: { kuery: 'processor.event : "metric"' } }), + callApi({ query: { kuery: 'processor.event : "transaction"' } }), ]); }); - after(() => synthtraceEsClient.clean()); - - describe('without comparisons', () => { - let transactionsStatistics: TransactionsGroupsDetailedStatistics; - let metricsStatistics: TransactionsGroupsDetailedStatistics; - before(async () => { - [metricsStatistics, transactionsStatistics] = await Promise.all([ - callApi({ query: { kuery: 'processor.event : "metric"' } }), - callApi({ query: { kuery: 'processor.event : "transaction"' } }), - ]); - }); - - it('returns some transactions data', () => { - expect(isEmpty(transactionsStatistics.currentPeriod)).to.be.equal(false); - }); + it('returns some transactions data', () => { + expect(isEmpty(transactionsStatistics.currentPeriod)).to.be.equal(false); + }); - it('returns some metrics data', () => { - expect(isEmpty(metricsStatistics.currentPeriod)).to.be.equal(false); - }); + it('returns some metrics data', () => { + expect(isEmpty(metricsStatistics.currentPeriod)).to.be.equal(false); + }); - it('has same latency mean value for metrics and transactions data', () => { - const transactionsCurrentPeriod = - transactionsStatistics.currentPeriod[transactionNames[0]]; - const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; - const transactionsLatencyMean = meanBy(transactionsCurrentPeriod.latency, 'y'); - const metricsLatencyMean = meanBy(metricsCurrentPeriod.latency, 'y'); - [transactionsLatencyMean, metricsLatencyMean].forEach((value) => - expect(value).to.be.equal(1000000) - ); - }); + it('has same latency mean value for metrics and transactions data', () => { + const transactionsCurrentPeriod = + transactionsStatistics.currentPeriod[transactionNames[0]]; + const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + const transactionsLatencyMean = meanBy(transactionsCurrentPeriod.latency, 'y'); + const metricsLatencyMean = meanBy(metricsCurrentPeriod.latency, 'y'); + [transactionsLatencyMean, metricsLatencyMean].forEach((value) => + expect(value).to.be.equal(1000000) + ); + }); - it('has same error rate mean value for metrics and transactions data', () => { - const transactionsCurrentPeriod = - transactionsStatistics.currentPeriod[transactionNames[0]]; - const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + it('has same error rate mean value for metrics and transactions data', () => { + const transactionsCurrentPeriod = + transactionsStatistics.currentPeriod[transactionNames[0]]; + const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; - const transactionsErrorRateMean = meanBy(transactionsCurrentPeriod.errorRate, 'y'); - const metricsErrorRateMean = meanBy(metricsCurrentPeriod.errorRate, 'y'); - [transactionsErrorRateMean, metricsErrorRateMean].forEach((value) => - expect(asPercent(value, 1)).to.be.equal(`${GO_PROD_ERROR_RATE}%`) - ); - }); + const transactionsErrorRateMean = meanBy(transactionsCurrentPeriod.errorRate, 'y'); + const metricsErrorRateMean = meanBy(metricsCurrentPeriod.errorRate, 'y'); + [transactionsErrorRateMean, metricsErrorRateMean].forEach((value) => + expect(asPercent(value, 1)).to.be.equal(`${GO_PROD_ERROR_RATE}%`) + ); + }); - it('has same throughput mean value for metrics and transactions data', () => { - const transactionsCurrentPeriod = - transactionsStatistics.currentPeriod[transactionNames[0]]; - const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; - const transactionsThroughputMean = roundNumber( - meanBy(transactionsCurrentPeriod.throughput, 'y') - ); - const metricsThroughputMean = roundNumber(meanBy(metricsCurrentPeriod.throughput, 'y')); - [transactionsThroughputMean, metricsThroughputMean].forEach((value) => - expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_PROD_ERROR_RATE)) - ); - }); + it('has same throughput mean value for metrics and transactions data', () => { + const transactionsCurrentPeriod = + transactionsStatistics.currentPeriod[transactionNames[0]]; + const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + const transactionsThroughputMean = roundNumber( + meanBy(transactionsCurrentPeriod.throughput, 'y') + ); + const metricsThroughputMean = roundNumber(meanBy(metricsCurrentPeriod.throughput, 'y')); + [transactionsThroughputMean, metricsThroughputMean].forEach((value) => + expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_PROD_ERROR_RATE)) + ); + }); - it('has same impact value for metrics and transactions data', () => { - const transactionsCurrentPeriod = - transactionsStatistics.currentPeriod[transactionNames[0]]; - const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + it('has same impact value for metrics and transactions data', () => { + const transactionsCurrentPeriod = + transactionsStatistics.currentPeriod[transactionNames[0]]; + const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; - const transactionsImpact = transactionsCurrentPeriod.impact; - const metricsImpact = metricsCurrentPeriod.impact; - [transactionsImpact, metricsImpact].forEach((value) => expect(value).to.be.equal(100)); - }); + const transactionsImpact = transactionsCurrentPeriod.impact; + const metricsImpact = metricsCurrentPeriod.impact; + [transactionsImpact, metricsImpact].forEach((value) => expect(value).to.be.equal(100)); }); + }); - describe('with comparisons', () => { - let transactionsStatistics: TransactionsGroupsDetailedStatistics; - before(async () => { - transactionsStatistics = await callApi({ - query: { - start: moment(end).subtract(7, 'minutes').toISOString(), - end: new Date(end).toISOString(), - offset: '8m', - }, - }); + describe('with comparisons', () => { + let transactionsStatistics: TransactionsGroupsDetailedStatistics; + before(async () => { + transactionsStatistics = await callApi({ + query: { + start: moment(end).subtract(7, 'minutes').toISOString(), + end: new Date(end).toISOString(), + offset: '8m', + }, }); + }); - it('returns some data for both periods', () => { - expect(isEmpty(transactionsStatistics.currentPeriod)).to.be.equal(false); - expect(isEmpty(transactionsStatistics.previousPeriod)).to.be.equal(false); - }); + it('returns some data for both periods', () => { + expect(isEmpty(transactionsStatistics.currentPeriod)).to.be.equal(false); + expect(isEmpty(transactionsStatistics.previousPeriod)).to.be.equal(false); + }); - it('has same start time for both periods', () => { - const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; - const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; - [ - [currentPeriod.latency, previousPeriod.latency], - [currentPeriod.errorRate, previousPeriod.errorRate], - [currentPeriod.throughput, previousPeriod.throughput], - ].forEach(([currentTimeseries, previousTimeseries]) => { - const firstCurrentPeriodDate = new Date( - first(currentTimeseries)?.x ?? NaN - ).toISOString(); - const firstPreviousPeriodDate = new Date( - first(previousPeriod.latency)?.x ?? NaN - ).toISOString(); - - expect(firstCurrentPeriodDate).to.equal(firstPreviousPeriodDate); - }); + it('has same start time for both periods', () => { + const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; + const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; + [ + [currentPeriod.latency, previousPeriod.latency], + [currentPeriod.errorRate, previousPeriod.errorRate], + [currentPeriod.throughput, previousPeriod.throughput], + ].forEach(([currentTimeseries, previousTimeseries]) => { + const firstCurrentPeriodDate = new Date( + first(currentTimeseries)?.x ?? NaN + ).toISOString(); + const firstPreviousPeriodDate = new Date( + first(previousPeriod.latency)?.x ?? NaN + ).toISOString(); + + expect(firstCurrentPeriodDate).to.equal(firstPreviousPeriodDate); }); - it('has same end time for both periods', () => { - const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; - const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; - [ - [currentPeriod.latency, previousPeriod.latency], - [currentPeriod.errorRate, previousPeriod.errorRate], - [currentPeriod.throughput, previousPeriod.throughput], - ].forEach(([currentTimeseries, previousTimeseries]) => { - const lastCurrentPeriodDate = new Date( - last(currentTimeseries)?.x ?? NaN - ).toISOString(); - const lastPreviousPeriodDate = new Date( - last(previousPeriod.latency)?.x ?? NaN - ).toISOString(); - - expect(lastCurrentPeriodDate).to.equal(lastPreviousPeriodDate); - }); + }); + it('has same end time for both periods', () => { + const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; + const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; + [ + [currentPeriod.latency, previousPeriod.latency], + [currentPeriod.errorRate, previousPeriod.errorRate], + [currentPeriod.throughput, previousPeriod.throughput], + ].forEach(([currentTimeseries, previousTimeseries]) => { + const lastCurrentPeriodDate = new Date(last(currentTimeseries)?.x ?? NaN).toISOString(); + const lastPreviousPeriodDate = new Date( + last(previousPeriod.latency)?.x ?? NaN + ).toISOString(); + + expect(lastCurrentPeriodDate).to.equal(lastPreviousPeriodDate); }); + }); - it('returns same number of buckets for both periods', () => { - const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; - const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; - [ - [currentPeriod.latency, previousPeriod.latency], - [currentPeriod.errorRate, previousPeriod.errorRate], - [currentPeriod.throughput, previousPeriod.throughput], - ].forEach(([currentTimeseries, previousTimeseries]) => { - expect(currentTimeseries.length).to.equal(previousTimeseries.length); - }); + it('returns same number of buckets for both periods', () => { + const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; + const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; + [ + [currentPeriod.latency, previousPeriod.latency], + [currentPeriod.errorRate, previousPeriod.errorRate], + [currentPeriod.throughput, previousPeriod.throughput], + ].forEach(([currentTimeseries, previousTimeseries]) => { + expect(currentTimeseries.length).to.equal(previousTimeseries.length); }); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/common/services/spaces.ts b/x-pack/test/common/services/spaces.ts index 24554e151135ced..c023d8da26ba580 100644 --- a/x-pack/test/common/services/spaces.ts +++ b/x-pack/test/common/services/spaces.ts @@ -24,7 +24,7 @@ export function SpacesServiceProvider({ getService }: FtrProviderContext) { return new (class SpacesService { public async create(space: any) { - log.debug('creating space'); + log.debug(`creating space ${space.name}`); const { data, status, statusText } = await axios.post('/api/spaces/space', space); if (status !== 200) { @@ -32,19 +32,19 @@ export function SpacesServiceProvider({ getService }: FtrProviderContext) { `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` ); } - log.debug('created space'); + log.debug(`created space ${space}`); } public async delete(spaceId: string) { - log.debug(`deleting space: ${spaceId}`); + log.debug(`deleting space id: ${spaceId}`); const { data, status, statusText } = await axios.delete(`/api/spaces/space/${spaceId}`); if (status !== 204) { - throw new Error( + log.debug( `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` ); } - log.debug(`deleted space: ${spaceId}`); + log.debug(`deleted space id: ${spaceId}`); } })(); } diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 6641867cadf2207..e71c14a8054c874 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -23,7 +23,7 @@ export default function (providerContext: FtrProviderContext) { let systemPkgVersion: string; before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); setupFleetAndAgents(providerContext); let packagePoliciesToDeleteIds: string[] = []; @@ -41,7 +41,7 @@ export default function (providerContext: FtrProviderContext) { } await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); it('should work with valid minimum required values', async () => { const { diff --git a/x-pack/test/fleet_api_integration/apis/download_sources/crud.ts b/x-pack/test/fleet_api_integration/apis/download_sources/crud.ts index 4e5d2c60ae9323f..c48f5602f5b5207 100644 --- a/x-pack/test/fleet_api_integration/apis/download_sources/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/download_sources/crud.ts @@ -14,11 +14,12 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('fleet_download_sources_crud', async function () { skipIfNoDockerRegistry(providerContext); before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); setupFleetAndAgents(providerContext); @@ -38,7 +39,7 @@ export default function (providerContext: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts b/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts index 9a49958d31d34c4..8906a7fa3ea37cf 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts @@ -13,10 +13,10 @@ import { setupFleetAndAgents } from '../agents/services'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const pkgName = 'error_handling'; const goodPackageVersion = '0.1.0'; const badPackageVersion = '0.2.0'; + const kibanaServer = getService('kibanaServer'); const installPackage = async (pkg: string, version: string) => { await supertest @@ -33,10 +33,10 @@ export default function (providerContext: FtrProviderContext) { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); beforeEach(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); afterEach(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); it('on a fresh install, it should uninstall a broken package during rollback', async function () { diff --git a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts index 88368e784dfa66d..d1e7cd8d999c689 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts @@ -14,20 +14,21 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); let agentCount = 0; let pkgVersion: string; describe('fleet_telemetry', () => { skipIfNoDockerRegistry(providerContext); before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); setupFleetAndAgents(providerContext); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); if (pkgVersion) { await supertest.delete(`/api/fleet/epm/packages/fleet_server/${pkgVersion}`); diff --git a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts index 43a3707a2196333..f48e17a70450429 100644 --- a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts @@ -14,11 +14,12 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('fleet_output_crud', async function () { skipIfNoDockerRegistry(providerContext); before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); setupFleetAndAgents(providerContext); @@ -37,7 +38,7 @@ export default function (providerContext: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index c16815eee3168cb..46f716e1322460a 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -14,6 +14,7 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const es: Client = getService('es'); const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); const getPackagePolicyById = async (id: string) => { const { body } = await supertest.get(`/api/fleet/package_policies/${id}`); @@ -27,13 +28,13 @@ export default function (providerContext: FtrProviderContext) { skipIfNoDockerRegistry(providerContext); let agentPolicyId: string; before(async () => { - await getService('esArchiver').load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await getService('esArchiver').load( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); }); after(async () => { - await getService('esArchiver').unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await getService('esArchiver').unload( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/get.ts b/x-pack/test/fleet_api_integration/apis/package_policy/get.ts index ef9eee75428381b..dbfb8109a2c1984 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/get.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/get.ts @@ -23,7 +23,8 @@ export default function (providerContext: FtrProviderContext) { skipIfNoDockerRegistry(providerContext); before(async () => { - await getService('esArchiver').load('x-pack/test/functional/es_archives/empty_kibana'); + await getService('kibanaServer').savedObjects.cleanStandardList(); + await getService('esArchiver').load( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); @@ -33,7 +34,7 @@ export default function (providerContext: FtrProviderContext) { await getService('esArchiver').unload( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); - await getService('esArchiver').unload('x-pack/test/functional/es_archives/empty_kibana'); + await getService('kibanaServer').savedObjects.cleanStandardList(); }); describe('get by id', async function () { diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index 6486579f41c7323..4a113714dc32457 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -17,6 +17,7 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); function withTestPackageVersion(version: string) { before(async function () { @@ -42,12 +43,12 @@ export default function (providerContext: FtrProviderContext) { let packagePolicyId: string; before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); after(async () => { - await getService('esArchiver').unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await getService('esArchiver').unload( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); diff --git a/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts b/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts index ee7679d2cef6565..83d2913e7aa5933 100644 --- a/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts +++ b/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts @@ -21,7 +21,8 @@ export default function (providerContext: FtrProviderContext) { describe('Preconfiguration', async () => { skipIfNoDockerRegistry(providerContext); before(async () => { - await getService('esArchiver').load('x-pack/test/functional/es_archives/empty_kibana'); + await getService('kibanaServer').savedObjects.cleanStandardList(); + await getService('esArchiver').load( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); @@ -31,7 +32,7 @@ export default function (providerContext: FtrProviderContext) { await getService('esArchiver').unload( 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' ); - await getService('esArchiver').unload('x-pack/test/functional/es_archives/empty_kibana'); + await getService('kibanaServer').savedObjects.cleanStandardList(); }); // Basic health check for the API; functionality is covered by the unit tests diff --git a/x-pack/test/fleet_api_integration/apis/service_tokens.ts b/x-pack/test/fleet_api_integration/apis/service_tokens.ts index e668caab8e98f28..bd13c3f5b8ca8ae 100644 --- a/x-pack/test/fleet_api_integration/apis/service_tokens.ts +++ b/x-pack/test/fleet_api_integration/apis/service_tokens.ts @@ -11,16 +11,16 @@ import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const esClient = getService('es'); describe('fleet_service_tokens', async () => { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); describe('POST /api/fleet/service_tokens', () => { diff --git a/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js index b03c2191cd13b4d..aa54a54196f95e0 100644 --- a/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js +++ b/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js @@ -16,8 +16,7 @@ export default function ({ getPageObjects, getService }) { const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); - // FLAKY: https://github.com/elastic/kibana/issues/136218 - describe.skip('maps in embeddable library', () => { + describe('maps in embeddable library', () => { before(async () => { await security.testUser.setRoles( [ diff --git a/x-pack/test/functional/es_archives/session_view/io_events/data.json b/x-pack/test/functional/es_archives/session_view/io_events/data.json index 0cf008aa6834c68..a9553169850f1ad 100644 --- a/x-pack/test/functional/es_archives/session_view/io_events/data.json +++ b/x-pack/test/functional/es_archives/session_view/io_events/data.json @@ -45,6 +45,14 @@ "total_bytes_skipped": 0, "bytes_skipped": [], "text": "256\n,\n Some host somewhere in the cloud\n | | | CentOS Stream release 8 on x86_64\n .Load average: 1.23, 1.01, 0.63\n\nHostname ********\nType xyz\n o Datacenter ********\n Cluster ********\n\n\n\n\n,0 loaded units listed. Pass --all to see loaded but inactive units, too.\nTo show all installed unit files use 'systemctl list-unit-files'.\n" + }, + "tty": { + "char_device": { + "major": 4, + "minor": 1 + }, + "rows": 66, + "columns": 280 } } } @@ -74,7 +82,15 @@ "total_bytes_captured": 1024, "total_bytes_skipped": 0, "bytes_skipped": [], - "text": ",\u001b[?2004h\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?2004h\u001b[1;59r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[59;1H\"/usr/local/bin/galera_traffic_start.sh\" [readonly] 14L, 397C\u001b[1;1H#!/bin/env bash\n# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\n# Script for setting the reject of queries in Galera\n\nmysql -h127.0.0.1 -P6033 -uroot -e \"set global wsrep_reject_queries='NONE'\" 2>&1\nRC=$?\n\nif [[ $RC != 0 ]]; then\n >&2 echo \"Failed to unset the reject of queries on Galera node, exiting.\"\n exit $RC\nelse\n echo \"Successfully unset the reject of queries.\"\nfi\n\u001b[94m~ \u001b[16;1H~ \u001b[17;1H~ \u001b[18;1H~ \u001b[19;1H~ \u001b[20;1H~ \u001b[21;1H~ \u001b[22;1H~ \u001b[23;1H~ \u001b[24;1H~ \u001b[25;1H~ \u001b[26;1H~ \u001b[27;1H~ \u001b[28;1H~ \u001b[29;1H~ \u001b[30;1H~ \u001b[31;1H~ \u001b[32;1H~ \u001b[33;1H~ \u001b[34;1H~ \u001b[35;1H~ \u001b[36;1H~ \u001b[37;1H~ \u001b[38;1H~ \u001b[39;1H~ \u001b[40;1H~ \u001b[41;1H~ \u001b[42;1H~ \u001b[43;1H~ \u001b[44;1H~ \u001b[45;1H~ \u001b[46;1H~ \u001b[47;1H~ \u001b[48;1H~ \u001b[49;1H~ \u001b[50;1H~ \u001b[51;1H~ \u001b[52;1H~ \u001b[53;1H~ \u001b[54;1H~ \u001b[55;1H~ \u001b[56;1H~ \u001b[57;1H~ \u001b[58;1H~ \u001b[1;1H\u001b[?25h\u0007\u001b[?25l\u001b[m\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hq\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001b[?2004h\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?2004h\u001b[1;59r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[59;1H\"/usr/local/bin/galera_traffic_stop.sh\" [readonly] 115L, 3570C\u001b[1;1H#!/bin/env bash\n# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\n# Script for rejecting connection on Galera cluster node, either gracefully or not,\n# depending on supplied arguments.\n\nfunction usage() {\n echo \"\n This script disables DB connections to Galera node.\n The default is to stop them gracefully.\n\n Usage: $0 [-h] [-w ] [-s ] [-x]\n\n Options:\n -h Prints this help.\n -w Number of seconds for waiting to close the connections.\u001b[17;11HDefault value is to wait for mysql-wait_timeout.\n -s Sleep interval between connections checks.\n -x Kills all connections immediately. Other options are ignored.\"\n exit\n}\n" + "text": ",\u001b[?2004h\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?2004h\u001b[1;59r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[59;1H\"/usr/local/bin/script_one.sh\" [readonly] 14L, 397C\u001b[1;1H#!/bin/env bash\n# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\n# Script for setting the reject of queries in MySQL\n\nmysql -h127.0.0.1 -P6033 -uroot -e \"set global wsrep_reject_queries='NONE'\" 2>&1\nRC=$?\n\nif [[ $RC != 0 ]]; then\n >&2 echo \"Failed to unset the reject of queries on MySQL node, exiting.\"\n exit $RC\nelse\n echo \"Successfully unset the reject of queries.\"\nfi\n\u001b[94m~ \u001b[16;1H~ \u001b[17;1H~ \u001b[18;1H~ \u001b[19;1H~ \u001b[20;1H~ \u001b[21;1H~ \u001b[22;1H~ \u001b[23;1H~ \u001b[24;1H~ \u001b[25;1H~ \u001b[26;1H~ \u001b[27;1H~ \u001b[28;1H~ \u001b[29;1H~ \u001b[30;1H~ \u001b[31;1H~ \u001b[32;1H~ \u001b[33;1H~ \u001b[34;1H~ \u001b[35;1H~ \u001b[36;1H~ \u001b[37;1H~ \u001b[38;1H~ \u001b[39;1H~ \u001b[40;1H~ \u001b[41;1H~ \u001b[42;1H~ \u001b[43;1H~ \u001b[44;1H~ \u001b[45;1H~ \u001b[46;1H~ \u001b[47;1H~ \u001b[48;1H~ \u001b[49;1H~ \u001b[50;1H~ \u001b[51;1H~ \u001b[52;1H~ \u001b[53;1H~ \u001b[54;1H~ \u001b[55;1H~ \u001b[56;1H~ \u001b[57;1H~ \u001b[58;1H~ \u001b[1;1H\u001b[?25h\u0007\u001b[?25l\u001b[m\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hq\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001b[?2004h\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?2004h\u001b[1;59r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[59;1H\"/usr/local/bin/galera_traffic_stop.sh\" [readonly] 115L, 3570C\u001b[1;1H#!/bin/env bash\n# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\n# Script for rejecting connection on MySQL cluster node, either gracefully or not,\n# depending on supplied arguments.\n\nfunction usage() {\n echo \"\n This script disables DB connections to MySQL node.\n The default is to stop them gracefully.\n\n Usage: $0 [-h] [-w ] [-s ] [-x]\n\n Options:\n -h Prints this help.\n -w Number of seconds for waiting to close the connections.\u001b[17;11HDefault value is to wait for mysql-wait_timeout.\n -s Sleep interval between connections checks.\n -x Kills all connections immediately. Other options are ignored.\"\n exit\n}\n" + }, + "tty": { + "char_device": { + "major": 4, + "minor": 1 + }, + "rows": 66, + "columns": 280 } } } @@ -104,7 +120,15 @@ "total_bytes_captured": 1024, "total_bytes_skipped": 0, "bytes_skipped": [], - "text": "\nfunction get_number_db_connections() {\n # count current\n DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select count(1) from stats_mysql_processlist where user = '$DB_USER' and db like 'db\\_%' escapee\u001b[26;1H '\\'\")\n}\n\nfunction set_number_grace_seconds() {\n local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select variable_value from global_variables where variable_name = 'mysql-wait_timeout'\")\n GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n}\n\nfunction wait_for_connections() {\n local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\u001b[37;5Hecho \"Waiting for connections to close for up to $GRACE_PERIOD seconds\"\u001b[39;5Hfor i in $(seq 0 $number_of_loops); do\u001b[40;9Hget_number_db_connections\u001b[41;9Hif [[ $DB_CONNECTIONS_NUMBER -eq 0 ]]; then\u001b[42;13Hecho \"No connection found for user $DB_USER to this node\"\u001b[43;13Hbreak\u001b[44;9Helse\u001b[45;13Hecho \"$DB_CONNECTIONS_NUMBER connection(s) found, waiting for ${SLEEP_INTERVAL}s, round $i\"\u001b[46;13Hsleep $SLEEP_INTERVAL\u001b[47;9Hfi\n done\n}\n\nfunction parse_args() {\n while getopts 'hs:w:x' opt; do\u001b[53;9Hcase \"$opt\" in\u001b[54;9Hh)\u001b[55;13Husage\u001b[56;13H;;\u001b[57;9Hs)\u001b[58;13Hif ! [[ $OPTARG =~ ^[0-9]+$ ]]; then\u001b[1;1H\u001b[?25h\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hset number\r\u001b[?25l\u001b[1;1H\u001b[38;5;130m 1 \u001b[m#!/bin/env bash\n\u001b[38;5;130m 2 \u001b[m# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\u001b[38;5;130m 3 \n 4 \u001b[m# Script for rejecting connection on Galera cluster node, either gracefully or not,\n\u001b[38;5;130m 5 \u001b[m# depending on supplied arguments.\n\u001b[38;5;130m 6 \n 7 \u001b[mfunction usage() {\n\u001b[38;5;130m 8 \u001b[m echo \"\n\u001b[38;5;130m 9 \u001b[m This script disables DB connections to Galera node.\n\u001b[38;5;130m 10 \u001b[m The default is to stop them gracefully.\n\u001b[38;5;130m 11 \n 12 \u001b[m Usage: $0 [-h] [-w ] [-s ] [-x]\n\u001b[38;5;130m 13 \n 14 \u001b[m Options:\n\u001b[38;5;130m 15 \u001b[m -h Prints this help.\n\u001b[38;5;130m 16 \u001b[m -w Number of seconds for waiting to close the connections.\n\u001b[38;5;130m 17 \u001b[m Default value is to wait for mysql-wait_timeout.\n\u001b[38;5;130m 18 \u001b[m -s Sleep interval between connections checks.\n\u001b[38;5;130m 19 \u001b[m -x Kills all connections immediately. Other options are ignored.\"\n\u001b[38;5;130m 20 \u001b[m exit\n\u001b[38;5;130m 21 \u001b[m}\n\u001b[38;5;130m 22 \n 23 \u001b[mfunction get_number_db_connections() {\n\u001b[38;5;130m 24 \u001b[m # count current\n\u001b[38;5;130m 25 \u001b[m DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select count(1) from stats_mysql_processlist where user = '$DB_USER' and db like 'db\\_%%\u001b[26;1H\u001b[38;5;130m \u001b[m' escape '\\'\")\n\u001b[38;5;130m 26 \u001b[m}\n\u001b[38;5;130m 27 \n 28 \u001b[mfunction set_number_grace_seconds() {\n\u001b[38;5;130m 29 \u001b[m local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select variable_value from global_variables where variable_name = 'mysql-wait_timm\u001b[31;1H\u001b[38;5;130m \u001b[meout'\")\u001b[31;16H\u001b[K\u001b[32;1H\u001b[38;5;130m 30 \u001b[m GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n\u001b[38;5;130m 31 \u001b[m}\n\u001b[38;5;130m 32 \u001b[m\u001b[34;10H\u001b[K\u001b[35;1H\u001b[38;5;130m 33 \u001b[mfunction wait_for_connections() {\u001b[35;42H\u001b[K\u001b[36;1H\u001b[38;5;130m 34 \u001b[m local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\n\u001b[38;5;130m 35 \u001b[m\u001b[37;10H\u001b[K\u001b[38;1H\u001b[38;5;130m 36 \u001b[m echo \"Waiting for connections to close for up to $GRACE_PERIOD seconds\"\n\u001b[38;5;130m 37 \u001b[m\u001b[39;9H\u001b[K\u001b[40;1H\u001b[38;5;130m 38 \u001b[m for i in $(seq 0 $number_of_loops); do\n" + "text": "\nfunction get_number_db_connections() {\n # count current\n DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select count(1) from stats_mysql_processlist where user = '$DB_USER' and db like 'db\\_%' escapee\u001b[26;1H '\\'\")\n}\n\nfunction set_number_grace_seconds() {\n local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select variable_value from global_variables where variable_name = 'mysql-wait_timeout'\")\n GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n}\n\nfunction wait_for_connections() {\n local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\u001b[37;5Hecho \"Waiting for connections to close for up to $GRACE_PERIOD seconds\"\u001b[39;5Hfor i in $(seq 0 $number_of_loops); do\u001b[40;9Hget_number_db_connections\u001b[41;9Hif [[ $DB_CONNECTIONS_NUMBER -eq 0 ]]; then\u001b[42;13Hecho \"No connection found for user $DB_USER to this node\"\u001b[43;13Hbreak\u001b[44;9Helse\u001b[45;13Hecho \"$DB_CONNECTIONS_NUMBER connection(s) found, waiting for ${SLEEP_INTERVAL}s, round $i\"\u001b[46;13Hsleep $SLEEP_INTERVAL\u001b[47;9Hfi\n done\n}\n\nfunction parse_args() {\n while getopts 'hs:w:x' opt; do\u001b[53;9Hcase \"$opt\" in\u001b[54;9Hh)\u001b[55;13Husage\u001b[56;13H;;\u001b[57;9Hs)\u001b[58;13Hif ! [[ $OPTARG =~ ^[0-9]+$ ]]; then\u001b[1;1H\u001b[?25h\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hset number\r\u001b[?25l\u001b[1;1H\u001b[38;5;130m 1 \u001b[m#!/bin/env bash\n\u001b[38;5;130m 2 \u001b[m# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\u001b[38;5;130m 3 \n 4 \u001b[m# Script for rejecting connection on MySQL cluster node, either gracefully or not,\n\u001b[38;5;130m 5 \u001b[m# depending on supplied arguments.\n\u001b[38;5;130m 6 \n 7 \u001b[mfunction usage() {\n\u001b[38;5;130m 8 \u001b[m echo \"\n\u001b[38;5;130m 9 \u001b[m This script disables DB connections to MySQL node.\n\u001b[38;5;130m 10 \u001b[m The default is to stop them gracefully.\n\u001b[38;5;130m 11 \n 12 \u001b[m Usage: $0 [-h] [-w ] [-s ] [-x]\n\u001b[38;5;130m 13 \n 14 \u001b[m Options:\n\u001b[38;5;130m 15 \u001b[m -h Prints this help.\n\u001b[38;5;130m 16 \u001b[m -w Number of seconds for waiting to close the connections.\n\u001b[38;5;130m 17 \u001b[m Default value is to wait for mysql-wait_timeout.\n\u001b[38;5;130m 18 \u001b[m -s Sleep interval between connections checks.\n\u001b[38;5;130m 19 \u001b[m -x Kills all connections immediately. Other options are ignored.\"\n\u001b[38;5;130m 20 \u001b[m exit\n\u001b[38;5;130m 21 \u001b[m}\n\u001b[38;5;130m 22 \n 23 \u001b[mfunction get_number_db_connections() {\n\u001b[38;5;130m 24 \u001b[m # count current\n\u001b[38;5;130m 25 \u001b[m DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select count(1) from stats_mysql_processlist where user = '$DB_USER' and db like 'db\\_%%\u001b[26;1H\u001b[38;5;130m \u001b[m' escape '\\'\")\n\u001b[38;5;130m 26 \u001b[m}\n\u001b[38;5;130m 27 \n 28 \u001b[mfunction set_number_grace_seconds() {\n\u001b[38;5;130m 29 \u001b[m local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select variable_value from global_variables where variable_name = 'mysql-wait_timm\u001b[31;1H\u001b[38;5;130m \u001b[meout'\")\u001b[31;16H\u001b[K\u001b[32;1H\u001b[38;5;130m 30 \u001b[m GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n\u001b[38;5;130m 31 \u001b[m}\n\u001b[38;5;130m 32 \u001b[m\u001b[34;10H\u001b[K\u001b[35;1H\u001b[38;5;130m 33 \u001b[mfunction wait_for_connections() {\u001b[35;42H\u001b[K\u001b[36;1H\u001b[38;5;130m 34 \u001b[m local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\n\u001b[38;5;130m 35 \u001b[m\u001b[37;10H\u001b[K\u001b[38;1H\u001b[38;5;130m 36 \u001b[m echo \"Waiting for connections to close for up to $GRACE_PERIOD seconds\"\n\u001b[38;5;130m 37 \u001b[m\u001b[39;9H\u001b[K\u001b[40;1H\u001b[38;5;130m 38 \u001b[m for i in $(seq 0 $number_of_loops); do\n" + }, + "tty": { + "char_device": { + "major": 4, + "minor": 1 + }, + "rows": 66, + "columns": 280 } } } @@ -135,6 +159,14 @@ "total_bytes_skipped": 0, "bytes_skipped": [], "text": "\u001b[38;5;130m 39 \u001b[m get_number_db_connections\u001b[41;42H\u001b[K\u001b[42;1H\u001b[38;5;130m 40 \u001b[m if [[ $DB_CONNECTIONS_NUMBER -eq 0 ]]; then\u001b[42;60H\u001b[K\u001b[43;1H\u001b[38;5;130m 41 \u001b[m echo \"No connection found for user $DB_USER to this node\"\n\u001b[38;5;130m 42 \u001b[m \u001b[8Cbreak\n\u001b[38;5;130m 43 \u001b[m else\u001b[45;21H\u001b[K\u001b[46;1H\u001b[38;5;130m 44 \u001b[m echo \"$DB_CONNECTIONS_NUMBER connection(s) found, waiting for ${SLEEP_INTERVAL}s, round $i\"\n\u001b[38;5;130m 45 \u001b[m \u001b[10Csleep $SLEEP_INTERVAL\n\u001b[38;5;130m 46 \u001b[m\u001b[8Cfi\n\u001b[38;5;130m 47 \u001b[m done\n\u001b[38;5;130m 48 \u001b[m}\n\u001b[38;5;130m 49 \u001b[m\u001b[51;10H\u001b[K\u001b[52;1H\u001b[38;5;130m 50 \u001b[mfunction parse_args() {\u001b[52;33H\u001b[K\u001b[53;1H\u001b[38;5;130m 51 \u001b[m while getopts 'hs:w:x' opt; do\n\u001b[38;5;130m 52 \u001b[m case \"$opt\" in\n\u001b[38;5;130m 53 \u001b[m h)\n\u001b[38;5;130m 54 \u001b[m usage\n\u001b[38;5;130m 55 \u001b[m \u001b[10C;;\n\u001b[38;5;130m 56 \u001b[m s)\u001b[58;19H\u001b[K\u001b[1;9H\u001b[?25h\u001b[?25l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[1;1H\u001b[38;5;130m 58 \u001b[m\u001b[16C>&2 echo \"Sleep interval (-s) must be a number\"\n\u001b[38;5;130m 59 \u001b[m\u001b[16Cexit 1\n\u001b[38;5;130m 60 \u001b[m\u001b[12Cfi\n\u001b[38;5;130m 61 \u001b[m\u001b[12CARG_SLEEP_INTERVAL=\"$OPTARG\"\n\u001b[38;5;130m 62 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 63 \u001b[m\u001b[8Cw)\n\u001b[38;5;130m 64 \u001b[m\u001b[12Cif ! [[ $OPTARG =~ ^[0-9]+$ ]]; then\n\u001b[38;5;130m 65 \u001b[m\u001b[16C>&2 echo \"Wait timeout (-w) must be a number\"\n\u001b[38;5;130m 66 \u001b[m\u001b[16Cexit 1\n\u001b[38;5;130m 67 \u001b[m\u001b[12Cfi\n\u001b[38;5;130m 68 \u001b[m\u001b[12CARG_GRACE_PERIOD=\"$OPTARG\"\n\u001b[38;5;130m 69 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 70 \u001b[m\u001b[8Cx)\n\u001b[38;5;130m 71 \u001b[m\u001b[12CARG_KILL_IMMEDIATELY=1\n\u001b[38;5;130m 72 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 73 \u001b[m\u001b[8Cesac\n\u001b[38;5;130m 74 \u001b[m done\n\u001b[38;5;130m 75 \n 76 \u001b[m GRACE_PERIOD=${ARG_GRACE_PERIOD:--1}\n\u001b[38;5;130m 77 \u001b[m SLEEP_INTERVAL=${ARG_SLEEP_INTERVAL:-30}\n\u001b[38;5;130m 78 \u001b[m KILL_IMMEDIATELY=${ARG_KILL_IMMEDIATELY:-0}\n\u001b[38;5;130m 79 \u001b[m}\n\u001b[38;5;130m 80 \n 81 \u001b[mDB_USER=\"rolap01\"\n\u001b[38;5;130m 82 \n 83 \u001b[mparse_args $@\n\u001b[38;5;130m 84 \n 85 \u001b[mif [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 86 \u001b[m echo \"WARNING: Not waiting for connections to close gracefully\"\n\u001b[38;5;130m 87 \u001b[m echo \"Press any key to continue... wsrep_reject_queries will be set to 'ALL_KILL'\"\n\u001b[38;5;130m 88 \u001b[m read a\n\u001b[38;5;130m 89 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e \"set global wsrep_reject_queries='ALL_KILL'\"\n\u001b[38;5;130m 90 \u001b[melse\n\u001b[38;5;130m 91 \u001b[m # Stop accepting queries in mariadb, do not kill opened connections\n\u001b[38;5;130m 92 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e \"set global wsrep_reject_queries='ALL'\"\n\u001b[38;5;130m 93 \u001b[mfi\n\u001b[38;5;130m 94 \n 95 \u001b[mexit_code=$?\n" + }, + "tty": { + "char_device": { + "major": 4, + "minor": 1 + }, + "rows": 66, + "columns": 280 } } } @@ -164,7 +196,15 @@ "total_bytes_captured": 1024, "total_bytes_skipped": 0, "bytes_skipped": [], - "text": "\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo \"Failed to set the reject of queries on Galera node, exiting.\"\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo \"Successfully stopped accepting queries.\"\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo \"ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections.\"\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[1;1H\u001b[38;5;130m 1 \u001b[m#!/bin/env bash\n\u001b[38;5;130m 2 \u001b[m# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\u001b[38;5;130m 3 \n 4 \u001b[m# Script for rejecting connection on Galera cluster node, either gracefully or not,\n\u001b[38;5;130m 5 \u001b[m# depending on supplied arguments.\n\u001b[38;5;130m 6 \n 7 \u001b[mfunction usage() {\n\u001b[38;5;130m 8 \u001b[m echo \"\n\u001b[38;5;130m 9 \u001b[m This script disables DB connections to Galera node.\n\u001b[38;5;130m 10 \u001b[m The default is to stop them gracefully.\n\u001b[38;5;130m 11 \n 12 \u001b[m Usage: $0 [-h] [-w ] [-s ] [-x]\n\u001b[38;5;130m 13 \n 14 \u001b[m Options:\n\u001b[38;5;130m 15 \u001b[m -h Prints this help.\n\u001b[38;5;130m 16 \u001b[m -w Number of seconds for waiting to close the connections.\n\u001b[38;5;130m 17 \u001b[m\u001b[10CDefault value is to wait for mysql-wait_timeout.\n\u001b[38;5;130m 18 \u001b[m -s Sleep interval between connections checks.\n\u001b[38;5;130m 19 \u001b[m -x Kills all connections immediately. Other options are ignored.\"\n\u001b[38;5;130m 20 \u001b[m exit\n\u001b[38;5;130m 21 \u001b[m}\n\u001b[38;5;130m 22 \n 23 \u001b[mfunction get_number_db_connections() {\n\u001b[38;5;130m 24 \u001b[m # count current\n\u001b[38;5;130m 25 \u001b[m DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select count(1) from stats_mysql_processlist where user = '$DB_USER' and db like 'db\\_%%\u001b[26;1H\u001b[38;5;130m \u001b[m' escape '\\'\")\n\u001b[38;5;130m 26 \u001b[m}\n\u001b[38;5;130m 27 \n 28 \u001b[mfunction set_number_grace_seconds() {\n\u001b[38;5;130m 29 \u001b[m local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select variable_value from global_variables where variable_name = 'mysql-wait_timm\u001b[31;1H\u001b[38;5;130m \u001b[meout'\")\n\u001b[38;5;130m 30 \u001b[m GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n\u001b[38;5;130m 31 \u001b[m}\n\u001b[38;5;130m 32 \n" + "text": "\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo \"Failed to set the reject of queries on MySQL node, exiting.\"\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo \"Successfully stopped accepting queries.\"\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo \"ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections.\"\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[1;1H\u001b[38;5;130m 1 \u001b[m#!/bin/env bash\n\u001b[38;5;130m 2 \u001b[m# Copyright (C) 2022, ********(R) Corporation. All rights reserved.\n\u001b[38;5;130m 3 \n 4 \u001b[m# Script for rejecting connection on MySQL cluster node, either gracefully or not,\n\u001b[38;5;130m 5 \u001b[m# depending on supplied arguments.\n\u001b[38;5;130m 6 \n 7 \u001b[mfunction usage() {\n\u001b[38;5;130m 8 \u001b[m echo \"\n\u001b[38;5;130m 9 \u001b[m This script disables DB connections to MySQL node.\n\u001b[38;5;130m 10 \u001b[m The default is to stop them gracefully.\n\u001b[38;5;130m 11 \n 12 \u001b[m Usage: $0 [-h] [-w ] [-s ] [-x]\n\u001b[38;5;130m 13 \n 14 \u001b[m Options:\n\u001b[38;5;130m 15 \u001b[m -h Prints this help.\n\u001b[38;5;130m 16 \u001b[m -w Number of seconds for waiting to close the connections.\n\u001b[38;5;130m 17 \u001b[m\u001b[10CDefault value is to wait for mysql-wait_timeout.\n\u001b[38;5;130m 18 \u001b[m -s Sleep interval between connections checks.\n\u001b[38;5;130m 19 \u001b[m -x Kills all connections immediately. Other options are ignored.\"\n\u001b[38;5;130m 20 \u001b[m exit\n\u001b[38;5;130m 21 \u001b[m}\n\u001b[38;5;130m 22 \n 23 \u001b[mfunction get_number_db_connections() {\n\u001b[38;5;130m 24 \u001b[m # count current\n\u001b[38;5;130m 25 \u001b[m DB_CONNECTIONS_NUMBER=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select count(1) from stats_mysql_processlist where user = '$DB_USER' and db like 'db\\_%%\u001b[26;1H\u001b[38;5;130m \u001b[m' escape '\\'\")\n\u001b[38;5;130m 26 \u001b[m}\n\u001b[38;5;130m 27 \n 28 \u001b[mfunction set_number_grace_seconds() {\n\u001b[38;5;130m 29 \u001b[m local mysql_wait_timeout_ms=$(mysql -h127.0.0.1 -P6032 -uadmin -N --silent -e \"select variable_value from global_variables where variable_name = 'mysql-wait_timm\u001b[31;1H\u001b[38;5;130m \u001b[meout'\")\n\u001b[38;5;130m 30 \u001b[m GRACE_PERIOD=$((($mysql_wait_timeout_ms+1000-1)/1000))\n\u001b[38;5;130m 31 \u001b[m}\n\u001b[38;5;130m 32 \n" + }, + "tty": { + "char_device": { + "major": 4, + "minor": 1 + }, + "rows": 66, + "columns": 280 } } } @@ -195,6 +235,14 @@ "total_bytes_skipped": 0, "bytes_skipped": [], "text": " 33 \u001b[mfunction wait_for_connections() {\n\u001b[38;5;130m 34 \u001b[m local number_of_loops=$(((($GRACE_PERIOD+$SLEEP_INTERVAL-1)/$SLEEP_INTERVAL)))\n\u001b[38;5;130m 35 \n 36 \u001b[m echo \"Waiting for connections to close for up to $GRACE_PERIOD seconds\"\n\u001b[38;5;130m 37 \n 38 \u001b[m for i in $(seq 0 $number_of_loops); do\n\u001b[38;5;130m 39 \u001b[m\u001b[8Cget_number_db_connections\n\u001b[38;5;130m 40 \u001b[m\u001b[8Cif [[ $DB_CONNECTIONS_NUMBER -eq 0 ]]; then\n\u001b[38;5;130m 41 \u001b[m\u001b[12Cecho \"No connection found for user $DB_USER to this node\"\n\u001b[38;5;130m 42 \u001b[m\u001b[12Cbreak\n\u001b[38;5;130m 43 \u001b[m\u001b[8Celse\n\u001b[38;5;130m 44 \u001b[m\u001b[12Cecho \"$DB_CONNECTIONS_NUMBER connection(s) found, waiting for ${SLEEP_INTERVAL}s, round $i\"\n\u001b[38;5;130m 45 \u001b[m\u001b[12Csleep $SLEEP_INTERVAL\n\u001b[38;5;130m 46 \u001b[m\u001b[8Cfi\n\u001b[38;5;130m 47 \u001b[m done\n\u001b[38;5;130m 48 \u001b[m}\n\u001b[38;5;130m 49 \n 50 \u001b[mfunction parse_args() {\n\u001b[38;5;130m 51 \u001b[m while getopts 'hs:w:x' opt; do\n\u001b[38;5;130m 52 \u001b[m\u001b[8Ccase \"$opt\" in\n\u001b[38;5;130m 53 \u001b[m\u001b[8Ch)\n\u001b[38;5;130m 54 \u001b[m\u001b[12Cusage\n\u001b[38;5;130m 55 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 56 \u001b[m\u001b[8Cs)\u001b[1;9H\u001b[?25h\u001b[?25l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[1;1H\u001b[38;5;130m 58 \u001b[m\u001b[16C>&2 echo \"Sleep interval (-s) must be a number\"\n\u001b[38;5;130m 59 \u001b[m\u001b[16Cexit 1\n\u001b[38;5;130m 60 \u001b[m\u001b[12Cfi\n\u001b[38;5;130m 61 \u001b[m\u001b[12CARG_SLEEP_INTERVAL=\"$OPTARG\"\n\u001b[38;5;130m 62 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 63 \u001b[m\u001b[8Cw)\n\u001b[38;5;130m 64 \u001b[m\u001b[12Cif ! [[ $OPTARG =~ ^[0-9]+$ ]]; then\n\u001b[38;5;130m 65 \u001b[m\u001b[16C>&2 echo \"Wait timeout (-w) must be a number\"\n\u001b[38;5;130m 66 \u001b[m\u001b[16Cexit 1\n\u001b[38;5;130m 67 \u001b[m\u001b[12Cfi\n\u001b[38;5;130m 68 \u001b[m\u001b[12CARG_GRACE_PERIOD=\"$OPTARG\"\n\u001b[38;5;130m 69 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 70 \u001b[m\u001b[8Cx)\n\u001b[38;5;130m 71 \u001b[m\u001b[12CARG_KILL_IMMEDIATELY=1\n\u001b[38;5;130m 72 \u001b[m\u001b[12C;;\n\u001b[38;5;130m 73 \u001b[m\u001b[8Cesac\n\u001b[38;5;130m 74 \u001b[m done\n\u001b[38;5;130m 75 \n 76 \u001b[m GRACE_PERIOD=${ARG_GRACE_PERIOD:--1}\n\u001b[38;5;130m 77 \u001b[m SLEEP_INTERVAL=${ARG_SLEEP_INTERVAL:-30}\n\u001b[38;5;130m 78 \u001b[m KILL_IMMEDIATELY=${ARG_KILL_IMMEDIATELY:-0}\n\u001b[38;5;130m 79 \u001b[m}\n\u001b[38;5;130m 80 \n 81 \u001b[mDB_USER=\"rolap01\"\n\u001b[38;5;130m 82 \n 83 \u001b[mparse_args $@\n" + }, + "tty": { + "char_device": { + "major": 4, + "minor": 1 + }, + "rows": 66, + "columns": 280 } } } @@ -224,7 +272,15 @@ "total_bytes_captured": 1024, "total_bytes_skipped": 0, "bytes_skipped": [], - "text": "\u001b[38;5;130m 84 \n 85 \u001b[mif [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 86 \u001b[m echo \"WARNING: Not waiting for connections to close gracefully\"\n\u001b[38;5;130m 87 \u001b[m echo \"Press any key to continue... wsrep_reject_queries will be set to 'ALL_KILL'\"\n\u001b[38;5;130m 88 \u001b[m read a\n\u001b[38;5;130m 89 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e \"set global wsrep_reject_queries='ALL_KILL'\"\n\u001b[38;5;130m 90 \u001b[melse\n\u001b[38;5;130m 91 \u001b[m # Stop accepting queries in mariadb, do not kill opened connections\n\u001b[38;5;130m 92 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e \"set global wsrep_reject_queries='ALL'\"\n\u001b[38;5;130m 93 \u001b[mfi\n\u001b[38;5;130m 94 \n 95 \u001b[mexit_code=$?\n\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo \"Failed to set the reject of queries on Galera node, exiting.\"\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo \"Successfully stopped accepting queries.\"\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo \"ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections.\"\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\nType :qa! and press to abandon all changes and exit Vim\u0007\u001b[58;9H\u001b[?25h\u0007\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hqa!\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001bkroot@staging-host:~\u001b\\\n" + "text": "\u001b[38;5;130m 84 \n 85 \u001b[mif [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 86 \u001b[m echo \"WARNING: Not waiting for connections to close gracefully\"\n\u001b[38;5;130m 87 \u001b[m echo \"Press any key to continue... wsrep_reject_queries will be set to 'ALL_KILL'\"\n\u001b[38;5;130m 88 \u001b[m read a\n\u001b[38;5;130m 89 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e \"set global wsrep_reject_queries='ALL_KILL'\"\n\u001b[38;5;130m 90 \u001b[melse\n\u001b[38;5;130m 91 \u001b[m # Stop accepting queries in mariadb, do not kill opened connections\n\u001b[38;5;130m 92 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e \"set global wsrep_reject_queries='ALL'\"\n\u001b[38;5;130m 93 \u001b[mfi\n\u001b[38;5;130m 94 \n 95 \u001b[mexit_code=$?\n\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo \"Failed to set the reject of queries on MySQL node, exiting.\"\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo \"Successfully stopped accepting queries.\"\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo \"ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections.\"\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\nType :qa! and press to abandon all changes and exit Vim\u0007\u001b[58;9H\u001b[?25h\u0007\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hqa!\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001bkroot@staging-host:~\u001b\\\n" + }, + "tty": { + "char_device": { + "major": 4, + "minor": 1 + }, + "rows": 66, + "columns": 280 } } } @@ -254,7 +310,15 @@ "total_bytes_captured": 1024, "total_bytes_skipped": 0, "bytes_skipped": [], - "text": "\u001bkroot@staging-host:~\u001b\\\b\b\b\b\u001b[1P\b\b\b\b\u001b[1P\b\b\b\b\u001b[1P\b\b\b\b\b\b\b\b\b\n\u001bkroot@staging-host:~\u001b\\\b\u001b[K\b\u001b[K\b\u001b[K\n,\n22/05/26 09:24:09 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] vi -R /usr/local/bin/galera_traffic_start.sh\u0007\n22/05/26 09:25:32 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] vi -R /usr/local/bin/galera_traffic_start.sh.sh.sh.sho.shp.sh\n22/05/26 09:30:08 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] exi\u0007\u0007\u0007exitlogout\n,\u001bec2-user@staging-host:~\u001b\\\n\u001bec2-user@staging-host:~\u001b\\\n,\n22/05/26 09:24:01 rack-na/cl_md (md), Cluster ********\n[ec2-user@staging-host:~] sudo -i\n22/05/26 10:11:37 rack-na/cl_md (md), Cluster ********\n[ec2-user@staging-host:~] exitlogout\n\n" + "text": "\u001bkroot@staging-host:~\u001b\\\b\b\b\b\u001b[1P\b\b\b\b\u001b[1P\b\b\b\b\u001b[1P\b\b\b\b\b\b\b\b\b\n\u001bkroot@staging-host:~\u001b\\\b\u001b[K\b\u001b[K\b\u001b[K\n,\n22/05/26 09:24:09 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] vi -R /usr/local/bin/script_one.sh\u0007\n22/05/26 09:25:32 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] vi -R /usr/local/bin/script_one.sh.sh.sh.sho.shp.sh\n22/05/26 09:30:08 rack-na/cl_md (md), Cluster ********\n[root@staging-host:~] exi\u0007\u0007\u0007exitlogout\n,\u001bec2-user@staging-host:~\u001b\\\n\u001bec2-user@staging-host:~\u001b\\\n,\n22/05/26 09:24:01 rack-na/cl_md (md), Cluster ********\n[ec2-user@staging-host:~] sudo -i\n22/05/26 10:11:37 rack-na/cl_md (md), Cluster ********\n[ec2-user@staging-host:~] exitlogout\n\n" + }, + "tty": { + "char_device": { + "major": 4, + "minor": 1 + }, + "rows": 66, + "columns": 280 } } } diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts index 142f6af98499409..5b10de7b3e036b9 100644 --- a/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { EsArchiver } from '@kbn/es-archiver'; import { AppSearchService, IEngine } from '../../../../services/app_search_service'; import { Browser } from '../../../../../../../test/functional/services/common'; import { FtrProviderContext } from '../../../../ftr_provider_context'; @@ -15,11 +14,10 @@ export default function enterpriseSearchSetupEnginesTests({ getService, getPageObjects, }: FtrProviderContext) { - const esArchiver = getService('esArchiver') as EsArchiver; const browser = getService('browser') as Browser; const retry = getService('retry'); const appSearch = getService('appSearch') as AppSearchService; - + const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['appSearch', 'security']); describe('Engines Overview', function () { @@ -28,14 +26,14 @@ export default function enterpriseSearchSetupEnginesTests({ let metaEngine: IEngine; before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); engine1 = await appSearch.createEngine(); engine2 = await appSearch.createEngine(); metaEngine = await appSearch.createMetaEngine([engine1.name, engine2.name]); }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); appSearch.destroyEngine(engine1.name); appSearch.destroyEngine(engine2.name); appSearch.destroyEngine(metaEngine.name); diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/app_search/setup_guide.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/app_search/setup_guide.ts index e7bec22936d1289..2995150d73d07c7 100644 --- a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/app_search/setup_guide.ts +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/app_search/setup_guide.ts @@ -12,16 +12,17 @@ export default function enterpriseSearchSetupGuideTests({ getService, getPageObjects, }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const browser = getService('browser'); const retry = getService('retry'); - + const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['appSearch']); describe('Setup Guide', function () { - before(async () => await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana')); + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); describe('when no enterpriseSearch.host is configured', () => { diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/workplace_search/setup_guide.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/workplace_search/setup_guide.ts index e1e78885e64014e..0be3156d29c0211 100644 --- a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/workplace_search/setup_guide.ts +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/workplace_search/setup_guide.ts @@ -12,16 +12,17 @@ export default function enterpriseSearchSetupGuideTests({ getService, getPageObjects, }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const browser = getService('browser'); const retry = getService('retry'); - + const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['workplaceSearch']); describe('Setup Guide', function () { - before(async () => await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana')); + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); describe('when no enterpriseSearch.host is configured', () => { diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts index 72ce196c1ae620a..fcab559466a741c 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts @@ -107,7 +107,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); - describe('Persistable state attachments', () => { + // FLAKY: https://github.com/elastic/kibana/issues/139300 + describe.skip('Persistable state attachments', () => { const getLensState = (dataViewId: string) => ({ title: '', visualizationType: 'lnsXY', diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index a7bcdf2ddc52cdd..961efb82a90d1a3 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -21,6 +21,7 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const spacesService = getService('spaces'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('Event Log public API', () => { before(async () => { @@ -32,7 +33,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); for (const namespace of [undefined, 'namespace-a']) { diff --git a/x-pack/test/reporting_functional/reporting_without_security/management.ts b/x-pack/test/reporting_functional/reporting_without_security/management.ts index 36589d375b0e03d..f9444c7e4c72980 100644 --- a/x-pack/test/reporting_functional/reporting_without_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_without_security/management.ts @@ -40,7 +40,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - const esArchiver = getService('esArchiver'); const reportingApi = getService('reportingAPI'); const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; @@ -55,12 +54,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Polling for jobs', () => { beforeEach(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.importExport.load(ecommerceSOPath); }); afterEach(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.importExport.unload(ecommerceSOPath); await reportingApi.deleteAllReports(); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts index efc87330eb82286..3dc4f9fb3f3a43e 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts @@ -11,17 +11,18 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const telemetryTestResources = getService('telemetryTestResources'); + const kibanaServer = getService('kibanaServer'); // The source of the data for these tests have changed and need to be updated // There are currently tests in the security_solution application being maintained describe.skip('security solution endpoint telemetry', () => { after(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); describe('when no agents are connected', () => { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); it('reports no endpoints or policies', async () => { diff --git a/x-pack/test/visual_regression/tests/login_page.ts b/x-pack/test/visual_regression/tests/login_page.ts index 34e1132134744c4..3e2b41002403619 100644 --- a/x-pack/test/visual_regression/tests/login_page.ts +++ b/x-pack/test/visual_regression/tests/login_page.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const visualTesting = getService('visualTesting'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); @@ -17,12 +17,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe.skip('Security', () => { describe('Login Page', () => { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); await PageObjects.security.forceLogout(); }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); afterEach(async () => {