diff --git a/.ci/Dockerfile b/.ci/Dockerfile index b2254c8fb1e05..ec7befe05f0d4 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=12.19.1 +ARG NODE_VERSION=14.15.1 FROM node:${NODE_VERSION} AS base diff --git a/.node-version b/.node-version index e9f788b12771f..2f5ee741e0d77 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -12.19.1 +14.15.1 diff --git a/.nvmrc b/.nvmrc index e9f788b12771f..2f5ee741e0d77 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.19.1 +14.15.1 diff --git a/docs/api/logstash-configuration-management/create-logstash.asciidoc b/docs/api/logstash-configuration-management/create-logstash.asciidoc index b608f4ee698f7..9bd5a9028ee9a 100644 --- a/docs/api/logstash-configuration-management/create-logstash.asciidoc +++ b/docs/api/logstash-configuration-management/create-logstash.asciidoc @@ -20,9 +20,6 @@ experimental[] Create a centrally-managed Logstash pipeline, or update an existi [[logstash-configuration-management-api-create-request-body]] ==== Request body -`id`:: - (Required, string) The pipeline ID. - `description`:: (Optional, string) The pipeline description. diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc index c796aac3d6b27..d66718be4074a 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -61,7 +61,7 @@ For all APIs, you must use a request header. The {kib} APIs support the `kbn-xsr By default, you must use `kbn-xsrf` for all API calls, except in the following scenarios: * The API endpoint uses the `GET` or `HEAD` operations -* The path is whitelisted using the <> setting +* The path is allowed using the <> setting * XSRF protections are disabled using the <> setting `Content-Type: application/json`:: diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index 01ba084b9e9e7..d9a8d0558714f 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -40,7 +40,7 @@ users interacting with APM APIs must have <> setting +* The path is allowed using the <> setting * XSRF protections are disabled using the <> setting `Content-Type: application/json`:: diff --git a/docs/developer/contributing/development-unit-tests.asciidoc b/docs/developer/contributing/development-unit-tests.asciidoc index 5322106b17ac1..d5f5bc76b3302 100644 --- a/docs/developer/contributing/development-unit-tests.asciidoc +++ b/docs/developer/contributing/development-unit-tests.asciidoc @@ -20,11 +20,13 @@ yarn test:mocha == Jest Jest tests are stored in the same directory as source code files with the `.test.{js,mjs,ts,tsx}` suffix. -*Running Jest Unit Tests* +Each plugin and package contains it's own `jest.config.js` file to define its root, and any overrides +to the jest-preset provided by `@kbn/test`. When working on a single plugin or package, you will find +it's more efficient to supply the Jest configuration file when running. ["source","shell"] ----------- -yarn test:jest +yarn jest --config src/plugins/discover/jest.config.js ----------- [discrete] diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 5ee7131610584..e515abee6014c 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -507,8 +507,8 @@ Kibana. |or -|{kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/spaces/README.md[spaces] +|See Configuring Kibana Spaces. |{kib-repo}blob/{branch}/x-pack/plugins/stack_alerts/README.md[stackAlerts] diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md index 5a1ab83551d34..fd6ade88479af 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md @@ -11,5 +11,5 @@ Signature: ```typescript -readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; +readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md index 5b0b3eea01cb1..d540de7637441 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; +readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md index 2dda422046318..0a9b674a45de2 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerActions: (triggerId: T) => Action[]; +readonly getTriggerActions: (triggerId: T) => Action[]; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md index e087753726a8a..faed81236342d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; +readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md index f9eb693b492f7..e3c5dbb92ae90 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md @@ -21,19 +21,19 @@ export declare class UiActionsService | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | ActionRegistry | | -| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | +| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | | [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void | | | [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | () => void | Removes all registered triggers and actions. | | [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: TriggerId, actionId: string) => void | | | [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void> | | | [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | UiActionsExecutionService | | | [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | () => UiActionsService | "Fork" a separate instance of UiActionsService that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of UiActionsService are only available within this instance. | -| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION"> | | +| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV"> | | | [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T> | | -| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[] | | -| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]> | | +| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[] | | +| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]> | | | [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | (actionId: string) => boolean | | -| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION"> | | +| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV"> | | | [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | (trigger: Trigger) => void | | | [triggers](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggers.md) | | TriggerRegistry | | | [triggerToActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggertoactions.md) | | TriggerToActionsRegistry | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md index bd340eb76fbac..6f03777e14552 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; +readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; ``` diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index bda5f00f762cd..3b643f76f0c09 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -16,6 +16,7 @@ roles when Security is enabled. |=== | `xpack.spaces.enabled` | Set to `true` (default) to enable Spaces in {kib}. + This setting is deprecated. Starting in 8.0, it will not be possible to disable this plugin. | `xpack.spaces.maxSpaces` | The maximum amount of Spaces that can be used with this instance of {kib}. Some operations diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index c22d4466ee09e..3786cbc7d83b6 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -575,10 +575,10 @@ all http requests to https over the port configured as <> setting requires the following format: +The <> setting requires the following format: |=== diff --git a/docs/user/dashboard/download-underlying-data.asciidoc b/docs/user/dashboard/download-underlying-data.asciidoc new file mode 100644 index 0000000000000..78403ba797d78 --- /dev/null +++ b/docs/user/dashboard/download-underlying-data.asciidoc @@ -0,0 +1,15 @@ +[float] +[role="xpack"] +[[download_csv]] +=== Download CSV + +To download the underlying data of the Lens panels on your dashboard, you can use the *Download as CSV* option. + +TIP: The *Download as CSV* option supports multiple CSV file downloads from the same Lens visualization out of the box, if configured: for instance with multiple layers on a bar chart. + +To use the *Download as CSV* option: + +* Click the from the panel menu, then click *Download as CSV*. ++ +[role="screenshot"] +image::images/download_csv_context_menu.png[Download as CSV from panel context menu] \ No newline at end of file diff --git a/docs/user/dashboard/explore-dashboard-data.asciidoc b/docs/user/dashboard/explore-dashboard-data.asciidoc index 238dfb79e900b..66f91dc2bc18c 100644 --- a/docs/user/dashboard/explore-dashboard-data.asciidoc +++ b/docs/user/dashboard/explore-dashboard-data.asciidoc @@ -16,3 +16,4 @@ The data that displays depends on the element that you inspect. image:images/Dashboard_inspect.png[Inspect in dashboard] include::explore-underlying-data.asciidoc[] +include::download-underlying-data.asciidoc[] diff --git a/docs/user/dashboard/images/download_csv_context_menu.png b/docs/user/dashboard/images/download_csv_context_menu.png new file mode 100644 index 0000000000000..09f82b7812495 Binary files /dev/null and b/docs/user/dashboard/images/download_csv_context_menu.png differ diff --git a/src/dev/jest/config.integration.js b/jest.config.integration.js similarity index 82% rename from src/dev/jest/config.integration.js rename to jest.config.integration.js index 9e7bbc34ac711..3dacb107f94c0 100644 --- a/src/dev/jest/config.integration.js +++ b/jest.config.integration.js @@ -17,16 +17,14 @@ * under the License. */ -import preset from '@kbn/test/jest-preset'; -import config from './config'; +const preset = require('@kbn/test/jest-preset'); -export default { - ...config, - testMatch: [ - '**/integration_tests/**/*.test.js', - '**/integration_tests/**/*.test.ts', - '**/integration_tests/**/*.test.tsx', - ], +module.exports = { + preset: '@kbn/test', + rootDir: '.', + roots: ['/src', '/packages'], + testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'], + testRunner: 'jasmine2', testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000000..c190556700b81 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + rootDir: '.', + projects: [...require('./jest.config.oss').projects, ...require('./x-pack/jest.config').projects], +}; diff --git a/jest.config.oss.js b/jest.config.oss.js new file mode 100644 index 0000000000000..e9235069687e0 --- /dev/null +++ b/jest.config.oss.js @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + rootDir: '.', + projects: [ + '/packages/*/jest.config.js', + '/src/*/jest.config.js', + '/src/legacy/*/jest.config.js', + '/src/plugins/*/jest.config.js', + '/test/*/jest.config.js', + ], + reporters: ['default', '/packages/kbn-test/target/jest/junit_reporter'], +}; diff --git a/package.json b/package.json index 7028b3093dc4f..e50b8516d9c29 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "**/@types/hapi__boom": "^7.4.1", "**/@types/hapi__hapi": "^18.2.6", "**/@types/hapi__mimos": "4.1.0", - "**/@types/node": "12.19.4", + "**/@types/node": "14.14.7", "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", @@ -98,18 +98,18 @@ "**/typescript": "4.1.2" }, "engines": { - "node": "12.19.1", + "node": "14.15.1", "yarn": "^1.21.1" }, "dependencies": { "@babel/core": "^7.11.6", "@babel/runtime": "^7.11.2", "@elastic/datemath": "link:packages/elastic-datemath", - "@elastic/elasticsearch": "7.10.0-rc.1", + "@elastic/elasticsearch": "7.10.0", "@elastic/ems-client": "7.11.0", "@elastic/eui": "30.2.0", "@elastic/filesaver": "1.1.2", - "@elastic/good": "8.1.1-kibana2", + "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", @@ -496,7 +496,7 @@ "@types/mustache": "^0.8.31", "@types/ncp": "^2.0.1", "@types/nock": "^10.0.3", - "@types/node": "12.19.4", + "@types/node": "14.14.7", "@types/node-fetch": "^2.5.7", "@types/node-forge": "^0.9.5", "@types/nodemailer": "^6.4.0", @@ -525,7 +525,6 @@ "@types/react-resize-detector": "^4.0.1", "@types/react-router": "^5.1.7", "@types/react-router-dom": "^5.1.5", - "@types/react-sticky": "^6.0.3", "@types/react-test-renderer": "^16.9.1", "@types/react-virtualized": "^9.18.7", "@types/read-pkg": "^4.0.0", @@ -722,7 +721,7 @@ "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^0.8.15", + "lmdb-store": "^0.9.0", "load-grunt-config": "^3.0.1", "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", @@ -782,7 +781,6 @@ "react-router-redux": "^4.0.8", "react-shortcuts": "^2.0.0", "react-sizeme": "^2.3.6", - "react-sticky": "^6.0.3", "react-syntax-highlighter": "^5.7.0", "react-test-renderer": "^16.12.0", "react-tiny-virtual-list": "^2.2.0", @@ -805,7 +803,7 @@ "sass-resources-loader": "^2.0.1", "selenium-webdriver": "^4.0.0-alpha.7", "serve-static": "1.14.1", - "shelljs": "^0.8.3", + "shelljs": "^0.8.4", "simple-git": "1.116.0", "sinon": "^7.4.2", "spawn-sync": "^1.0.15", diff --git a/packages/README.md b/packages/README.md index 8ff05f4e8ff89..9d9cd4ed7b6e5 100644 --- a/packages/README.md +++ b/packages/README.md @@ -60,7 +60,7 @@ A package can also follow the pattern of having `.test.js` files as siblings of A package using the `.test.js` naming convention will have those tests automatically picked up by Jest and run by the unit test runner, currently mapped to the Kibana `test` script in the root `package.json`. * `yarn test` or `yarn grunt test` runs all unit tests. -* `node scripts/jest` runs all Jest tests in Kibana. +* `yarn jest` runs all Jest tests in Kibana. ---- Each package can also specify its own `test` script in the package's `package.json`, for cases where you'd prefer to run the tests from the local package directory. diff --git a/packages/kbn-apm-config-loader/jest.config.js b/packages/kbn-apm-config-loader/jest.config.js new file mode 100644 index 0000000000000..2b88679a57e72 --- /dev/null +++ b/packages/kbn-apm-config-loader/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-apm-config-loader'], +}; diff --git a/packages/kbn-babel-code-parser/jest.config.js b/packages/kbn-babel-code-parser/jest.config.js new file mode 100644 index 0000000000000..60fce8897723e --- /dev/null +++ b/packages/kbn-babel-code-parser/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-babel-code-parser'], +}; diff --git a/packages/kbn-config-schema/jest.config.js b/packages/kbn-config-schema/jest.config.js new file mode 100644 index 0000000000000..35de02838aa1c --- /dev/null +++ b/packages/kbn-config-schema/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-config-schema'], +}; diff --git a/packages/kbn-config/jest.config.js b/packages/kbn-config/jest.config.js new file mode 100644 index 0000000000000..b4c84eef4675c --- /dev/null +++ b/packages/kbn-config/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-config'], +}; diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 4a6d86a0dfba6..5d8fb1e28beb6 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -25,8 +25,8 @@ Object { }, "uuid": undefined, "xsrf": Object { + "allowlist": Array [], "disableProtection": false, - "whitelist": Array [], }, } `; @@ -56,8 +56,8 @@ Object { }, "uuid": undefined, "xsrf": Object { + "allowlist": Array [], "disableProtection": false, - "whitelist": Array [], }, } `; diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts index 1c51564187442..036ff5e80b3ec 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts @@ -96,7 +96,7 @@ describe('#get', () => { someNotSupportedValue: 'val', xsrf: { disableProtection: false, - whitelist: [], + allowlist: [], }, }, }); @@ -119,7 +119,7 @@ describe('#get', () => { someNotSupportedValue: 'val', xsrf: { disableProtection: false, - whitelist: [], + allowlist: [], }, }, }); diff --git a/packages/kbn-dev-utils/jest.config.js b/packages/kbn-dev-utils/jest.config.js new file mode 100644 index 0000000000000..2b0cefe5e741f --- /dev/null +++ b/packages/kbn-dev-utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-dev-utils'], +}; diff --git a/packages/kbn-es-archiver/jest.config.js b/packages/kbn-es-archiver/jest.config.js new file mode 100644 index 0000000000000..e5df757f6637e --- /dev/null +++ b/packages/kbn-es-archiver/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-es-archiver'], +}; diff --git a/packages/kbn-es/jest.config.js b/packages/kbn-es/jest.config.js new file mode 100644 index 0000000000000..2c09b5400369d --- /dev/null +++ b/packages/kbn-es/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-es'], +}; diff --git a/packages/kbn-i18n/jest.config.js b/packages/kbn-i18n/jest.config.js new file mode 100644 index 0000000000000..dff8b872bdfe0 --- /dev/null +++ b/packages/kbn-i18n/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-i18n'], + testRunner: 'jasmine2', +}; diff --git a/packages/kbn-interpreter/jest.config.js b/packages/kbn-interpreter/jest.config.js new file mode 100644 index 0000000000000..d2f6127ccff79 --- /dev/null +++ b/packages/kbn-interpreter/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-interpreter'], +}; diff --git a/packages/kbn-legacy-logging/jest.config.js b/packages/kbn-legacy-logging/jest.config.js new file mode 100644 index 0000000000000..f33205439e134 --- /dev/null +++ b/packages/kbn-legacy-logging/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-legacy-logging'], +}; diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts index 1b13eda44fff2..1533bde4fc17b 100644 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -117,11 +117,18 @@ export class LegacyLoggingServer { public log({ level, context, message, error, timestamp, meta = {} }: LogRecord) { const { tags = [], ...metadata } = meta; - this.events.emit('log', { - data: getDataToLog(error, metadata, message), - tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags], - timestamp: timestamp.getTime(), - }); + this.events + .emit('log', { + data: getDataToLog(error, metadata, message), + tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags], + timestamp: timestamp.getTime(), + }) + // @ts-expect-error @hapi/podium emit is actually an async function + .catch((err) => { + // eslint-disable-next-line no-console + console.error('An unexpected error occurred while writing to the log:', err.stack); + process.exit(1); + }); } public stop() { diff --git a/packages/kbn-legacy-logging/src/log_reporter.test.ts b/packages/kbn-legacy-logging/src/log_reporter.test.ts new file mode 100644 index 0000000000000..4fa2922c7824e --- /dev/null +++ b/packages/kbn-legacy-logging/src/log_reporter.test.ts @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import os from 'os'; +import path from 'path'; +import fs from 'fs'; + +import stripAnsi from 'strip-ansi'; + +import { getLogReporter } from './log_reporter'; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('getLogReporter', () => { + it('should log to stdout (not json)', async () => { + const lines: string[] = []; + const origWrite = process.stdout.write; + process.stdout.write = (buffer: string | Uint8Array): boolean => { + lines.push(stripAnsi(buffer.toString()).trim()); + return true; + }; + + const loggerStream = getLogReporter({ + config: { + json: false, + dest: 'stdout', + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); + + await sleep(500); + + process.stdout.write = origWrite; + expect(lines.length).toBe(1); + expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] hello world$/); + }); + + it('should log to stdout (as json)', async () => { + const lines: string[] = []; + const origWrite = process.stdout.write; + process.stdout.write = (buffer: string | Uint8Array): boolean => { + lines.push(JSON.parse(buffer.toString().trim())); + return true; + }; + + const loggerStream = getLogReporter({ + config: { + json: true, + dest: 'stdout', + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); + + await sleep(500); + + process.stdout.write = origWrite; + expect(lines.length).toBe(1); + expect(lines[0]).toMatchObject({ + type: 'log', + tags: ['foo'], + message: 'hello world', + }); + }); + + it('should log to custom file (not json)', async () => { + const dir = os.tmpdir(); + const logfile = `dest-${Date.now()}.log`; + const dest = path.join(dir, logfile); + + const loggerStream = getLogReporter({ + config: { + json: false, + dest, + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); + + await sleep(500); + + const lines = stripAnsi(fs.readFileSync(dest, { encoding: 'utf8' })) + .trim() + .split(os.EOL); + expect(lines.length).toBe(1); + expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] hello world$/); + }); + + it('should log to custom file (as json)', async () => { + const dir = os.tmpdir(); + const logfile = `dest-${Date.now()}.log`; + const dest = path.join(dir, logfile); + + const loggerStream = getLogReporter({ + config: { + json: true, + dest, + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); + + await sleep(500); + + const lines = fs + .readFileSync(dest, { encoding: 'utf8' }) + .trim() + .split(os.EOL) + .map((data) => JSON.parse(data)); + expect(lines.length).toBe(1); + expect(lines[0]).toMatchObject({ + type: 'log', + tags: ['foo'], + message: 'hello world', + }); + }); +}); diff --git a/packages/kbn-legacy-logging/src/log_reporter.ts b/packages/kbn-legacy-logging/src/log_reporter.ts index 8ecaf348bac04..f0075b431b83d 100644 --- a/packages/kbn-legacy-logging/src/log_reporter.ts +++ b/packages/kbn-legacy-logging/src/log_reporter.ts @@ -17,9 +17,11 @@ * under the License. */ +import { createWriteStream } from 'fs'; +import { pipeline } from 'stream'; + // @ts-expect-error missing type def import { Squeeze } from '@hapi/good-squeeze'; -import { createWriteStream as writeStr, WriteStream } from 'fs'; import { KbnLoggerJsonFormat } from './log_format_json'; import { KbnLoggerStringFormat } from './log_format_string'; @@ -31,21 +33,28 @@ export function getLogReporter({ events, config }: { events: any; config: LogFor const format = config.json ? new KbnLoggerJsonFormat(config) : new KbnLoggerStringFormat(config); const logInterceptor = new LogInterceptor(); - let dest: WriteStream | NodeJS.WritableStream; if (config.dest === 'stdout') { - dest = process.stdout; + pipeline(logInterceptor, squeeze, format, onFinished); + // The `pipeline` function is used to properly close all streams in the + // pipeline in case one of them ends or fails. Since stdout obviously + // shouldn't be closed in case of a failure in one of the other streams, + // we're not including that in the call to `pipeline`, but rely on the old + // `pipe` function instead. + format.pipe(process.stdout); } else { - dest = writeStr(config.dest, { + const dest = createWriteStream(config.dest, { flags: 'a', encoding: 'utf8', }); - - logInterceptor.on('end', () => { - dest.end(); - }); + pipeline(logInterceptor, squeeze, format, dest, onFinished); } - logInterceptor.pipe(squeeze).pipe(format).pipe(dest); - return logInterceptor; } + +function onFinished(err: NodeJS.ErrnoException | null) { + if (err) { + // eslint-disable-next-line no-console + console.error('An unexpected error occurred in the logging pipeline:', err.stack); + } +} diff --git a/packages/kbn-legacy-logging/src/setup_logging.ts b/packages/kbn-legacy-logging/src/setup_logging.ts index 3b8b4b167c63d..153e7a0f207c1 100644 --- a/packages/kbn-legacy-logging/src/setup_logging.ts +++ b/packages/kbn-legacy-logging/src/setup_logging.ts @@ -18,7 +18,7 @@ */ // @ts-expect-error missing typedef -import good from '@elastic/good'; +import { plugin as good } from '@elastic/good'; import { Server } from '@hapi/hapi'; import { LegacyLoggingConfig } from './schema'; import { getLoggingConfiguration } from './get_logging_config'; diff --git a/packages/kbn-logging/jest.config.js b/packages/kbn-logging/jest.config.js new file mode 100644 index 0000000000000..74ff8fd14f56a --- /dev/null +++ b/packages/kbn-logging/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-logging'], +}; diff --git a/packages/kbn-monaco/jest.config.js b/packages/kbn-monaco/jest.config.js new file mode 100644 index 0000000000000..03f879f6d0bef --- /dev/null +++ b/packages/kbn-monaco/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-monaco'], +}; diff --git a/packages/kbn-optimizer/jest.config.js b/packages/kbn-optimizer/jest.config.js new file mode 100644 index 0000000000000..6e313aaad3c82 --- /dev/null +++ b/packages/kbn-optimizer/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-optimizer'], +}; diff --git a/packages/kbn-plugin-generator/jest.config.js b/packages/kbn-plugin-generator/jest.config.js new file mode 100644 index 0000000000000..1d81a72128afd --- /dev/null +++ b/packages/kbn-plugin-generator/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-plugin-generator'], +}; diff --git a/packages/kbn-pm/.babelrc b/packages/kbn-pm/.babelrc index 1ca768097a7ee..9ea6ecafe7287 100644 --- a/packages/kbn-pm/.babelrc +++ b/packages/kbn-pm/.babelrc @@ -9,6 +9,7 @@ ], "plugins": [ "@babel/proposal-class-properties", - "@babel/proposal-object-rest-spread" + "@babel/proposal-object-rest-spread", + "@babel/proposal-optional-chaining" ] } diff --git a/packages/kbn-pm/jest.config.js b/packages/kbn-pm/jest.config.js new file mode 100644 index 0000000000000..ba0624f5f6ccd --- /dev/null +++ b/packages/kbn-pm/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-pm'], +}; diff --git a/packages/kbn-release-notes/jest.config.js b/packages/kbn-release-notes/jest.config.js new file mode 100644 index 0000000000000..44390a8c98162 --- /dev/null +++ b/packages/kbn-release-notes/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-release-notes'], +}; diff --git a/packages/kbn-spec-to-console/jest.config.js b/packages/kbn-spec-to-console/jest.config.js new file mode 100644 index 0000000000000..cef82f4d76f73 --- /dev/null +++ b/packages/kbn-spec-to-console/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-spec-to-console'], +}; diff --git a/packages/kbn-std/jest.config.js b/packages/kbn-std/jest.config.js new file mode 100644 index 0000000000000..0615e33e41af8 --- /dev/null +++ b/packages/kbn-std/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-std'], +}; diff --git a/packages/kbn-telemetry-tools/jest.config.js b/packages/kbn-telemetry-tools/jest.config.js new file mode 100644 index 0000000000000..b7b101beccf32 --- /dev/null +++ b/packages/kbn-telemetry-tools/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-telemetry-tools'], +}; diff --git a/packages/kbn-test/jest.config.js b/packages/kbn-test/jest.config.js new file mode 100644 index 0000000000000..9400d402a1a33 --- /dev/null +++ b/packages/kbn-test/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-test'], +}; diff --git a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts index 35b4b85e4d22a..a1e5b2a363a9d 100644 --- a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts +++ b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts @@ -23,7 +23,7 @@ * tries to mock out simple versions of the Mocha types */ -import EventEmitter from 'events'; +import { EventEmitter } from 'events'; export interface Suite { suites: Suite[]; diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 9e6ba67a421ac..54b064f5cd49e 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -60,3 +60,5 @@ export { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; export * from './functional_test_runner'; export { getUrl } from './jest/utils/get_url'; + +export { runCheckJestConfigsCli } from './jest/run_check_jest_configs_cli'; diff --git a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts new file mode 100644 index 0000000000000..385fb453697ef --- /dev/null +++ b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { relative, resolve, sep } from 'path'; +import { writeFileSync } from 'fs'; + +import execa from 'execa'; +import globby from 'globby'; +import Mustache from 'mustache'; + +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; + +// @ts-ignore +import { testMatch } from '../../jest-preset'; + +const template: string = `module.exports = { + preset: '@kbn/test', + rootDir: '{{{relToRoot}}}', + roots: ['/{{{modulePath}}}'], +}; +`; + +const roots: string[] = ['x-pack/plugins', 'packages', 'src/legacy', 'src/plugins', 'test', 'src']; + +export async function runCheckJestConfigsCli() { + run( + async ({ flags: { fix = false }, log }) => { + const { stdout: coveredFiles } = await execa( + 'yarn', + ['--silent', 'jest', '--listTests', '--json'], + { + cwd: REPO_ROOT, + } + ); + + const allFiles = new Set( + await globby(testMatch.concat(['!**/integration_tests/**']), { + gitignore: true, + }) + ); + + JSON.parse(coveredFiles).forEach((file: string) => { + const pathFromRoot = relative(REPO_ROOT, file); + allFiles.delete(pathFromRoot); + }); + + if (allFiles.size) { + log.error( + `The following files do not belong to a jest.config.js file, or that config is not included from the root jest.config.js\n${[ + ...allFiles, + ] + .map((file) => ` - ${file}`) + .join('\n')}` + ); + } else { + log.success('All test files are included by a Jest configuration'); + return; + } + + if (fix) { + allFiles.forEach((file) => { + const root = roots.find((r) => file.startsWith(r)); + + if (root) { + const name = relative(root, file).split(sep)[0]; + const modulePath = [root, name].join('/'); + + const content = Mustache.render(template, { + relToRoot: relative(modulePath, '.'), + modulePath, + }); + + writeFileSync(resolve(root, name, 'jest.config.js'), content); + } else { + log.warning(`Unable to determind where to place jest.config.js for ${file}`); + } + }); + } else { + log.info( + `Run 'node scripts/check_jest_configs --fix' to attempt to create the missing config files` + ); + } + + process.exit(1); + }, + { + description: 'Check that all test files are covered by a jest.config.js', + flags: { + boolean: ['fix'], + help: ` + --fix Attempt to create missing config files + `, + }, + } + ); +} diff --git a/packages/kbn-ui-framework/jest.config.js b/packages/kbn-ui-framework/jest.config.js new file mode 100644 index 0000000000000..d9cb93d7c069d --- /dev/null +++ b/packages/kbn-ui-framework/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-ui-framework'], +}; diff --git a/packages/kbn-utils/jest.config.js b/packages/kbn-utils/jest.config.js new file mode 100644 index 0000000000000..39fb0a8ff1a8c --- /dev/null +++ b/packages/kbn-utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-utils'], +}; diff --git a/packages/kbn-utils/src/streams/reduce_stream.test.ts b/packages/kbn-utils/src/streams/reduce_stream.test.ts index e4a7dc1cef491..7d823bb8fe113 100644 --- a/packages/kbn-utils/src/streams/reduce_stream.test.ts +++ b/packages/kbn-utils/src/streams/reduce_stream.test.ts @@ -70,7 +70,7 @@ describe('reduceStream', () => { const errorStub = jest.fn(); reduce$.on('data', dataStub); reduce$.on('error', errorStub); - const endEvent = promiseFromEvent('end', reduce$); + const closeEvent = promiseFromEvent('close', reduce$); reduce$.write(1); reduce$.write(2); @@ -79,7 +79,7 @@ describe('reduceStream', () => { reduce$.write(1000); reduce$.end(); - await endEvent; + await closeEvent; expect(reducer).toHaveBeenCalledTimes(3); expect(dataStub).toHaveBeenCalledTimes(0); expect(errorStub).toHaveBeenCalledTimes(1); diff --git a/src/dev/jest/cli.js b/scripts/check_jest_configs.js similarity index 90% rename from src/dev/jest/cli.js rename to scripts/check_jest_configs.js index 40627c4bece74..a7a520f433bf9 100644 --- a/src/dev/jest/cli.js +++ b/scripts/check_jest_configs.js @@ -17,6 +17,5 @@ * under the License. */ -import { run } from 'jest'; - -run(process.argv.slice(2)); +require('../src/setup_node_env'); +require('@kbn/test').runCheckJestConfigsCli(); diff --git a/scripts/jest.js b/scripts/jest.js index c252056de766b..90f8da10f4c90 100755 --- a/scripts/jest.js +++ b/scripts/jest.js @@ -29,8 +29,15 @@ // // See all cli options in https://facebook.github.io/jest/docs/cli.html -var resolve = require('path').resolve; -process.argv.push('--config', resolve(__dirname, '../src/dev/jest/config.js')); +if (process.argv.indexOf('--config') === -1) { + // append correct jest.config if none is provided + var configPath = require('path').resolve(__dirname, '../jest.config.oss.js'); + process.argv.push('--config', configPath); + console.log('Running Jest with --config', configPath); +} -require('../src/setup_node_env'); -require('../src/dev/jest/cli'); +if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; +} + +require('jest').run(); diff --git a/scripts/jest_integration.js b/scripts/jest_integration.js index 7da1436f5583c..f07d28939ef0c 100755 --- a/scripts/jest_integration.js +++ b/scripts/jest_integration.js @@ -29,9 +29,17 @@ // // See all cli options in https://facebook.github.io/jest/docs/cli.html -var resolve = require('path').resolve; -process.argv.push('--config', resolve(__dirname, '../src/dev/jest/config.integration.js')); process.argv.push('--runInBand'); -require('../src/setup_node_env'); -require('../src/dev/jest/cli'); +if (process.argv.indexOf('--config') === -1) { + // append correct jest.config if none is provided + var configPath = require('path').resolve(__dirname, '../jest.config.integration.js'); + process.argv.push('--config', configPath); + console.log('Running Jest with --config', configPath); +} + +if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; +} + +require('jest').run(); diff --git a/src/cli/cluster/cluster.mock.ts b/src/cli/cluster/cluster.mock.ts index 332f8aad53ba1..85d16a79a467c 100644 --- a/src/cli/cluster/cluster.mock.ts +++ b/src/cli/cluster/cluster.mock.ts @@ -19,7 +19,7 @@ /* eslint-env jest */ // eslint-disable-next-line max-classes-per-file -import EventEmitter from 'events'; +import { EventEmitter } from 'events'; import { assign, random } from 'lodash'; import { delay } from 'bluebird'; diff --git a/src/cli/jest.config.js b/src/cli/jest.config.js new file mode 100644 index 0000000000000..6a1055ca864c8 --- /dev/null +++ b/src/cli/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/cli'], +}; diff --git a/src/cli/repl/__snapshots__/repl.test.js.snap b/src/cli/repl/__snapshots__/repl.test.js.snap index c7751b5797f49..804898284491d 100644 --- a/src/cli/repl/__snapshots__/repl.test.js.snap +++ b/src/cli/repl/__snapshots__/repl.test.js.snap @@ -1,17 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`repl it allows print depth to be specified 1`] = `"{ '0': { '1': { '2': [Object] } }, whoops: [Circular] }"`; +exports[`repl it allows print depth to be specified 1`] = ` +" { + '0': { '1': { '2': [Object] } }, + whoops: [Circular *1] +}" +`; exports[`repl it colorizes raw values 1`] = `"{ meaning: 42 }"`; exports[`repl it handles deep and recursive objects 1`] = ` -"{ +" { '0': { '1': { '2': { '3': { '4': { '5': [Object] } } } } }, - whoops: [Circular] + whoops: [Circular *1] }" `; @@ -51,13 +56,13 @@ Array [ Array [ "Promise Rejected: ", - "{ + " { '0': { '1': { '2': { '3': { '4': { '5': [Object] } } } } }, - whoops: [Circular] + whoops: [Circular *1] }", ], ] @@ -71,13 +76,13 @@ Array [ Array [ "Promise Resolved: ", - "{ + " { '0': { '1': { '2': { '3': { '4': { '5': [Object] } } } } }, - whoops: [Circular] + whoops: [Circular *1] }", ], ] diff --git a/src/cli_encryption_keys/jest.config.js b/src/cli_encryption_keys/jest.config.js new file mode 100644 index 0000000000000..f3be28f7898f5 --- /dev/null +++ b/src/cli_encryption_keys/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/cli_encryption_keys'], +}; diff --git a/src/cli_keystore/jest.config.js b/src/cli_keystore/jest.config.js new file mode 100644 index 0000000000000..787cd7ccd84be --- /dev/null +++ b/src/cli_keystore/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/cli_keystore'], +}; diff --git a/src/cli_plugin/jest.config.js b/src/cli_plugin/jest.config.js new file mode 100644 index 0000000000000..cbd226f5df887 --- /dev/null +++ b/src/cli_plugin/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/cli_plugin'], +}; diff --git a/src/core/jest.config.js b/src/core/jest.config.js new file mode 100644 index 0000000000000..bdb65b3817507 --- /dev/null +++ b/src/core/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/core'], + testRunner: 'jasmine2', +}; diff --git a/src/core/public/utils/crypto/sha256.ts b/src/core/public/utils/crypto/sha256.ts index 13e0d405a706b..add93cb75b92a 100644 --- a/src/core/public/utils/crypto/sha256.ts +++ b/src/core/public/utils/crypto/sha256.ts @@ -200,7 +200,7 @@ export class Sha256 { return this; } - digest(encoding: string): string { + digest(encoding: BufferEncoding): string { // Suppose the length of the message M, in bits, is l const l = this._len * 8; diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 7a69dc2fa726e..c645629fa5653 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -82,12 +82,13 @@ describe('core deprecations', () => { describe('xsrfDeprecation', () => { it('logs a warning if server.xsrf.whitelist is set', () => { - const { messages } = applyCoreDeprecations({ + const { migrated, messages } = applyCoreDeprecations({ server: { xsrf: { whitelist: ['/path'] } }, }); + expect(migrated.server.xsrf.allowlist).toEqual(['/path']); expect(messages).toMatchInlineSnapshot(` Array [ - "It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. It will be removed in 8.0 release. Instead, supply the \\"kbn-xsrf\\" header.", + "\\"server.xsrf.whitelist\\" is deprecated and has been replaced by \\"server.xsrf.allowlist\\"", ] `); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 6c85cfbed8e82..3dde7cfb6c1cb 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -38,16 +38,6 @@ const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; -const xsrfDeprecation: ConfigDeprecation = (settings, fromPath, log) => { - if ((settings.server?.xsrf?.whitelist ?? []).length > 0) { - log( - 'It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. ' + - 'It will be removed in 8.0 release. Instead, supply the "kbn-xsrf" header.' - ); - } - return settings; -}; - const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { log( @@ -140,10 +130,10 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unu unusedFromRoot('elasticsearch.startupTimeout'), rename('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'), rename('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'), + rename('server.xsrf.whitelist', 'server.xsrf.allowlist'), configPathDeprecation, dataPathDeprecation, rewriteBasePathDeprecation, cspRulesDeprecation, mapManifestServiceUrlDeprecation, - xsrfDeprecation, ]; diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index 523256129333f..b1c731e8ba534 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -99,7 +99,7 @@ const createStartContractMock = () => { }, xsrf: { disableProtection: false, - whitelistConfigured: false, + allowlistConfigured: false, }, }, logging: { diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index e1c78edb902a9..6686a778ee8a5 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -182,8 +182,8 @@ describe('CoreUsageDataService', () => { "truststoreConfigured": false, }, "xsrf": Object { + "allowlistConfigured": false, "disableProtection": false, - "whitelistConfigured": false, }, }, "logging": Object { diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index f729e23cb68bc..490c411ecb852 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -180,7 +180,7 @@ export class CoreUsageDataService implements CoreService { expect(validated.name).toEqual('kibana-hostname'); }); -test('throws if xsrf.whitelist element does not start with a slash', () => { +test('throws if xsrf.allowlist element does not start with a slash', () => { const httpSchema = config.schema; const obj = { xsrf: { - whitelist: ['/valid-path', 'invalid-path'], + allowlist: ['/valid-path', 'invalid-path'], }, }; expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"[xsrf.whitelist.1]: must start with a slash"` + `"[xsrf.allowlist.1]: must start with a slash"` ); }); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 7d41b4ea9e915..be64def294625 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -82,7 +82,7 @@ export const config = { ), xsrf: schema.object({ disableProtection: schema.boolean({ defaultValue: false }), - whitelist: schema.arrayOf( + allowlist: schema.arrayOf( schema.string({ validate: match(/^\//, 'must start with a slash') }), { defaultValue: [] } ), @@ -142,7 +142,7 @@ export class HttpConfig { public ssl: SslConfig; public compression: { enabled: boolean; referrerWhitelist?: string[] }; public csp: ICspConfig; - public xsrf: { disableProtection: boolean; whitelist: string[] }; + public xsrf: { disableProtection: boolean; allowlist: string[] }; public requestId: { allowFromAnyIp: boolean; ipAllowlist: string[] }; /** diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index a964130550bf5..7df35b04c66cf 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -36,7 +36,7 @@ const actualVersion = pkg.version; const versionHeader = 'kbn-version'; const xsrfHeader = 'kbn-xsrf'; const nameHeader = 'kbn-name'; -const whitelistedTestPath = '/xsrf/test/route/whitelisted'; +const allowlistedTestPath = '/xsrf/test/route/whitelisted'; const xsrfDisabledTestPath = '/xsrf/test/route/disabled'; const kibanaName = 'my-kibana-name'; const setupDeps = { @@ -63,7 +63,7 @@ describe('core lifecycle handlers', () => { customResponseHeaders: { 'some-header': 'some-value', }, - xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] }, + xsrf: { disableProtection: false, allowlist: [allowlistedTestPath] }, requestId: { allowFromAnyIp: true, ipAllowlist: [], @@ -179,7 +179,7 @@ describe('core lifecycle handlers', () => { } ); ((router as any)[method.toLowerCase()] as RouteRegistrar)( - { path: whitelistedTestPath, validate: false }, + { path: allowlistedTestPath, validate: false }, (context, req, res) => { return res.ok({ body: 'ok' }); } @@ -235,7 +235,7 @@ describe('core lifecycle handlers', () => { }); it('accepts whitelisted requests without either an xsrf or version header', async () => { - await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok'); + await getSupertest(method.toLowerCase(), allowlistedTestPath).expect(200, 'ok'); }); it('accepts requests on a route with disabled xsrf protection', async () => { diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts index fdcf2a173b906..8ad823b3a6944 100644 --- a/src/core/server/http/lifecycle_handlers.test.ts +++ b/src/core/server/http/lifecycle_handlers.test.ts @@ -58,7 +58,7 @@ describe('xsrf post-auth handler', () => { describe('non destructive methods', () => { it('accepts requests without version or xsrf header', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'get', headers: {} }); @@ -74,7 +74,7 @@ describe('xsrf post-auth handler', () => { describe('destructive methods', () => { it('accepts requests with xsrf header', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post', headers: { 'kbn-xsrf': 'xsrf' } }); @@ -88,7 +88,7 @@ describe('xsrf post-auth handler', () => { }); it('accepts requests with version header', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post', headers: { 'kbn-version': 'some-version' } }); @@ -102,7 +102,7 @@ describe('xsrf post-auth handler', () => { }); it('returns a bad request if called without xsrf or version header', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post' }); @@ -121,7 +121,7 @@ describe('xsrf post-auth handler', () => { }); it('accepts requests if protection is disabled', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: true } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: true } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post', headers: {} }); @@ -134,9 +134,9 @@ describe('xsrf post-auth handler', () => { expect(result).toEqual('next'); }); - it('accepts requests if path is whitelisted', () => { + it('accepts requests if path is allowlisted', () => { const config = createConfig({ - xsrf: { whitelist: ['/some-path'], disableProtection: false }, + xsrf: { allowlist: ['/some-path'], disableProtection: false }, }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post', headers: {}, path: '/some-path' }); @@ -152,7 +152,7 @@ describe('xsrf post-auth handler', () => { it('accepts requests if xsrf protection on a route is disabled', () => { const config = createConfig({ - xsrf: { whitelist: [], disableProtection: false }, + xsrf: { allowlist: [], disableProtection: false }, }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ diff --git a/src/core/server/http/lifecycle_handlers.ts b/src/core/server/http/lifecycle_handlers.ts index 7ef7e86326039..4060284b5b56a 100644 --- a/src/core/server/http/lifecycle_handlers.ts +++ b/src/core/server/http/lifecycle_handlers.ts @@ -29,12 +29,12 @@ const XSRF_HEADER = 'kbn-xsrf'; const KIBANA_NAME_HEADER = 'kbn-name'; export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler => { - const { whitelist, disableProtection } = config.xsrf; + const { allowlist, disableProtection } = config.xsrf; return (request, response, toolkit) => { if ( disableProtection || - whitelist.includes(request.route.path) || + allowlist.includes(request.route.path) || request.route.options.xsrfRequired === false ) { return toolkit.next(); diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index 412396644648e..cdcbe513e1224 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -43,7 +43,7 @@ configService.atPath.mockReturnValue( compression: { enabled: true }, xsrf: { disableProtection: true, - whitelist: [], + allowlist: [], }, customResponseHeaders: {}, requestId: { diff --git a/src/core/server/metrics/collectors/process.test.ts b/src/core/server/metrics/collectors/process.test.ts index a437d799371f1..0ce1b9e8e350e 100644 --- a/src/core/server/metrics/collectors/process.test.ts +++ b/src/core/server/metrics/collectors/process.test.ts @@ -62,6 +62,7 @@ describe('ProcessMetricsCollector', () => { heapTotal, heapUsed, external: 0, + arrayBuffers: 0, })); jest.spyOn(v8, 'getHeapStatistics').mockImplementation( diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 36a8d9a52fd52..59f9c4f9ff38c 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -409,7 +409,7 @@ export interface CoreConfigUsageData { }; xsrf: { disableProtection: boolean; - whitelistConfigured: boolean; + allowlistConfigured: boolean; }; requestId: { allowFromAnyIp: boolean; diff --git a/src/core/server/ui_settings/cache.test.ts b/src/core/server/ui_settings/cache.test.ts new file mode 100644 index 0000000000000..ea375751fe437 --- /dev/null +++ b/src/core/server/ui_settings/cache.test.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Cache } from './cache'; + +describe('Cache', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + it('stores value for maxAge ms', async () => { + const cache = new Cache(500); + cache.set(42); + expect(cache.get()).toBe(42); + jest.advanceTimersByTime(100); + expect(cache.get()).toBe(42); + }); + it('invalidates cache after maxAge ms', async () => { + const cache = new Cache(500); + cache.set(42); + expect(cache.get()).toBe(42); + jest.advanceTimersByTime(1000); + expect(cache.get()).toBe(null); + }); + it('del invalidates cache immediately', async () => { + const cache = new Cache(10); + cache.set(42); + expect(cache.get()).toBe(42); + cache.del(); + expect(cache.get()).toBe(null); + }); +}); diff --git a/src/core/server/ui_settings/cache.ts b/src/core/server/ui_settings/cache.ts new file mode 100644 index 0000000000000..697cf2b284c78 --- /dev/null +++ b/src/core/server/ui_settings/cache.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +const oneSec = 1000; +const defMaxAge = 5 * oneSec; +/** + * @internal + */ +export class Cache> { + private value: T | null; + private timer?: NodeJS.Timeout; + + /** + * Delete cached value after maxAge ms. + */ + constructor(private readonly maxAge: number = defMaxAge) { + this.value = null; + } + get() { + return this.value; + } + set(value: T) { + this.del(); + this.value = value; + this.timer = setTimeout(() => this.del(), this.maxAge); + } + del() { + if (this.timer) { + clearTimeout(this.timer); + } + this.value = null; + } +} diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index a38fb2ab7e06c..8238511e27ed9 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -676,4 +676,111 @@ describe('ui settings', () => { expect(uiSettings.isOverridden('bar')).toBe(true); }); }); + + describe('caching', () => { + describe('read operations cache user config', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('get', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(10000); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('getAll', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.getAll(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.getAll(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(10000); + await uiSettings.getAll(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('getUserProvided', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.getUserProvided(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.getUserProvided(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(10000); + await uiSettings.getUserProvided(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + }); + + describe('write operations invalidate user config cache', () => { + it('set', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.set('foo', 'bar'); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('setMany', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.setMany({ foo: 'bar' }); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('remove', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.remove('foo'); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('removeMany', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.removeMany(['foo', 'bar']); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + }); + }); }); diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index f168784a93330..ab5fca9f81031 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -24,6 +24,7 @@ import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; import { IUiSettingsClient, UiSettingsParams, PublicUiSettingsParams } from './types'; import { CannotOverrideError } from './ui_settings_errors'; +import { Cache } from './cache'; export interface UiSettingsServiceOptions { type: string; @@ -36,7 +37,6 @@ export interface UiSettingsServiceOptions { } interface ReadOptions { - ignore401Errors?: boolean; autoCreateOrUpgradeIfMissing?: boolean; } @@ -58,6 +58,7 @@ export class UiSettingsClient implements IUiSettingsClient { private readonly overrides: NonNullable; private readonly defaults: NonNullable; private readonly log: Logger; + private readonly cache: Cache; constructor(options: UiSettingsServiceOptions) { const { type, id, buildNum, savedObjectsClient, log, defaults = {}, overrides = {} } = options; @@ -69,6 +70,7 @@ export class UiSettingsClient implements IUiSettingsClient { this.defaults = defaults; this.overrides = overrides; this.log = log; + this.cache = new Cache(); } getRegistered() { @@ -95,7 +97,12 @@ export class UiSettingsClient implements IUiSettingsClient { } async getUserProvided(): Promise> { - const userProvided: UserProvided = this.onReadHook(await this.read()); + const cachedValue = this.cache.get(); + if (cachedValue) { + return cachedValue; + } + + const userProvided: UserProvided = this.onReadHook(await this.read()); // write all overridden keys, dropping the userValue is override is null and // adding keys for overrides that are not in saved object @@ -104,10 +111,13 @@ export class UiSettingsClient implements IUiSettingsClient { value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; } + this.cache.set(userProvided); + return userProvided; } async setMany(changes: Record) { + this.cache.del(); this.onWriteHook(changes); await this.write({ changes }); } @@ -140,7 +150,7 @@ export class UiSettingsClient implements IUiSettingsClient { private async getRaw(): Promise { const userProvided = await this.getUserProvided(); - return defaultsDeep(userProvided, this.defaults); + return defaultsDeep({}, userProvided, this.defaults); } private validateKey(key: string, value: unknown) { @@ -209,10 +219,9 @@ export class UiSettingsClient implements IUiSettingsClient { } } - private async read({ - ignore401Errors = false, - autoCreateOrUpgradeIfMissing = true, - }: ReadOptions = {}): Promise> { + private async read({ autoCreateOrUpgradeIfMissing = true }: ReadOptions = {}): Promise< + Record + > { try { const resp = await this.savedObjectsClient.get>(this.type, this.id); return resp.attributes; @@ -227,16 +236,13 @@ export class UiSettingsClient implements IUiSettingsClient { }); if (!failedUpgradeAttributes) { - return await this.read({ - ignore401Errors, - autoCreateOrUpgradeIfMissing: false, - }); + return await this.read({ autoCreateOrUpgradeIfMissing: false }); } return failedUpgradeAttributes; } - if (this.isIgnorableError(error, ignore401Errors)) { + if (this.isIgnorableError(error)) { return {}; } @@ -244,17 +250,9 @@ export class UiSettingsClient implements IUiSettingsClient { } } - private isIgnorableError(error: Error, ignore401Errors: boolean) { - const { - isForbiddenError, - isEsUnavailableError, - isNotAuthorizedError, - } = this.savedObjectsClient.errors; - - return ( - isForbiddenError(error) || - isEsUnavailableError(error) || - (ignore401Errors && isNotAuthorizedError(error)) - ); + private isIgnorableError(error: Error) { + const { isForbiddenError, isEsUnavailableError } = this.savedObjectsClient.errors; + + return isForbiddenError(error) || isEsUnavailableError(error); } } diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.ts index ad42ea11436f5..93ad599e41e40 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.ts @@ -25,7 +25,7 @@ export const DownloadNodeBuilds: GlobalTask = { global: true, description: 'Downloading node.js builds for all platforms', async run(config, log) { - const shasums = await getNodeShasums(config.getNodeVersion()); + const shasums = await getNodeShasums(log, config.getNodeVersion()); await Promise.all( config.getNodePlatforms().map(async (platform) => { const { url, downloadPath, downloadName } = getNodeDownloadInfo(config, platform); diff --git a/src/dev/build/tasks/nodejs/node_shasums.test.ts b/src/dev/build/tasks/nodejs/node_shasums.test.ts index 08ac823c7ebf0..53d073afd6499 100644 --- a/src/dev/build/tasks/nodejs/node_shasums.test.ts +++ b/src/dev/build/tasks/nodejs/node_shasums.test.ts @@ -70,11 +70,12 @@ jest.mock('axios', () => ({ }, })); +import { ToolingLog } from '@kbn/dev-utils'; import { getNodeShasums } from './node_shasums'; describe('src/dev/build/tasks/nodejs/node_shasums', () => { it('resolves to an object with shasums for node downloads for version', async () => { - const shasums = await getNodeShasums('8.9.4'); + const shasums = await getNodeShasums(new ToolingLog(), '8.9.4'); expect(shasums).toEqual( expect.objectContaining({ 'node-v8.9.4.tar.gz': '729b44b32b2f82ecd5befac4f7518de0c4e3add34e8fe878f745740a66cbbc01', diff --git a/src/dev/build/tasks/nodejs/node_shasums.ts b/src/dev/build/tasks/nodejs/node_shasums.ts index e0926aa3e49e4..0f506dff4fd88 100644 --- a/src/dev/build/tasks/nodejs/node_shasums.ts +++ b/src/dev/build/tasks/nodejs/node_shasums.ts @@ -18,10 +18,13 @@ */ import axios from 'axios'; +import { ToolingLog } from '@kbn/dev-utils'; -export async function getNodeShasums(nodeVersion: string) { +export async function getNodeShasums(log: ToolingLog, nodeVersion: string) { const url = `https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v${nodeVersion}/SHASUMS256.txt`; + log.debug('Downloading shasum values for node version', nodeVersion, 'from', url); + const { status, data } = await axios.get(url); if (status !== 200) { diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts index 9b03dcd828cf9..5b3c1bad74930 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts @@ -45,6 +45,7 @@ const testWriter = new ToolingLogCollectingWriter(); log.setWriters([testWriter]); expect.addSnapshotSerializer(createAnyInstanceSerializer(Config)); +expect.addSnapshotSerializer(createAnyInstanceSerializer(ToolingLog)); const nodeVersion = Fs.readFileSync(Path.resolve(REPO_ROOT, '.node-version'), 'utf8').trim(); expect.addSnapshotSerializer( @@ -100,6 +101,7 @@ it('checks shasums for each downloaded node build', async () => { [MockFunction] { "calls": Array [ Array [ + , "", ], ], diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts index 9ce0778d2d1f0..50684d866cbf5 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts @@ -25,7 +25,7 @@ export const VerifyExistingNodeBuilds: GlobalTask = { global: true, description: 'Verifying previously downloaded node.js build for all platforms', async run(config, log) { - const shasums = await getNodeShasums(config.getNodeVersion()); + const shasums = await getNodeShasums(log, config.getNodeVersion()); await Promise.all( config.getNodePlatforms().map(async (platform) => { diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index b6eda2dbfd560..0819123138d0f 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -47,12 +47,12 @@ const packages: Package[] = [ extractMethod: 'gunzip', archives: { 'darwin-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/darwin-x64-72.gz', - sha256: '983106049bb86e21b7f823144b2b83e3f1408217401879b3cde0312c803512c9', + url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/darwin-x64-83.gz', + sha256: 'b45cd8296fd6eb2a091399c20111af43093ba30c99ed9e5d969278f5ff69ba8f', }, 'linux-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/linux-x64-72.gz', - sha256: '8b6692037f7b0df24dabc9c9b039038d1c3a3110f62121616b406c482169710a', + url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/linux-x64-83.gz', + sha256: '1bbc3f90f0ba105772b37c04e3a718f69544b4df01dda00435c2b8e50b2ad0d9', }, // ARM build is currently done manually as Github Actions used in upstream project @@ -62,16 +62,16 @@ const packages: Package[] = [ // * checkout the node-re2 project, // * install Node using the same minor used by Kibana // * npm install, which will also create a build - // * gzip -c build/Release/re2.node > linux-arm64-72.gz + // * gzip -c build/Release/re2.node > linux-arm64-83.gz // * upload to kibana-ci-proxy-cache bucket 'linux-arm64': { url: - 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.15.4/linux-arm64-72.gz', - sha256: '5942353ec9cf46a39199818d474f7af137cfbb1bc5727047fe22f31f36602a7e', + 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.15.4/linux-arm64-83.gz', + sha256: '4eb524ca9a79dea9c07342e487fbe91591166fdbc022ae987104840df948a4e9', }, 'win32-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/win32-x64-72.gz', - sha256: '0a6991e693577160c3e9a3f196bd2518368c52d920af331a1a183313e0175604', + url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/win32-x64-83.gz', + sha256: 'efe939d3cda1d64ee3ee3e60a20613b95166d55632e702c670763ea7e69fca06', }, }, }, diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js index c666581ddb08c..177439c56a115 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js @@ -49,7 +49,8 @@ describe('Team Assignment', () => { describe(`when the codeowners file contains #CC#`, () => { it(`should strip the prefix and still drill down through the fs`, async () => { const { stdout } = await execa('grep', ['tre', teamAssignmentsPath], { cwd: ROOT_DIR }); - expect(stdout).to.be(`x-pack/plugins/code/server/config.ts kibana-tre + expect(stdout).to.be(`x-pack/plugins/code/jest.config.js kibana-tre +x-pack/plugins/code/server/config.ts kibana-tre x-pack/plugins/code/server/index.ts kibana-tre x-pack/plugins/code/server/plugin.test.ts kibana-tre x-pack/plugins/code/server/plugin.ts kibana-tre`); diff --git a/src/dev/jest.config.js b/src/dev/jest.config.js new file mode 100644 index 0000000000000..bdb51372e2c26 --- /dev/null +++ b/src/dev/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/dev'], +}; diff --git a/src/legacy/server/jest.config.js b/src/legacy/server/jest.config.js new file mode 100644 index 0000000000000..f971e823765ac --- /dev/null +++ b/src/legacy/server/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/legacy/server'], +}; diff --git a/src/legacy/ui/jest.config.js b/src/legacy/ui/jest.config.js new file mode 100644 index 0000000000000..45809f8797129 --- /dev/null +++ b/src/legacy/ui/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/legacy/ui'], +}; diff --git a/src/legacy/utils/jest.config.js b/src/legacy/utils/jest.config.js new file mode 100644 index 0000000000000..7ce73fa367613 --- /dev/null +++ b/src/legacy/utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/legacy/utils'], +}; diff --git a/src/optimize/jest.config.js b/src/optimize/jest.config.js new file mode 100644 index 0000000000000..419f4f97098b3 --- /dev/null +++ b/src/optimize/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/optimize'], +}; diff --git a/src/plugins/advanced_settings/jest.config.js b/src/plugins/advanced_settings/jest.config.js new file mode 100644 index 0000000000000..94fd65aae4464 --- /dev/null +++ b/src/plugins/advanced_settings/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/advanced_settings'], +}; diff --git a/src/plugins/bfetch/jest.config.js b/src/plugins/bfetch/jest.config.js new file mode 100644 index 0000000000000..5976a994be7e5 --- /dev/null +++ b/src/plugins/bfetch/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/bfetch'], +}; diff --git a/src/plugins/charts/jest.config.js b/src/plugins/charts/jest.config.js new file mode 100644 index 0000000000000..168ccde71a667 --- /dev/null +++ b/src/plugins/charts/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/charts'], +}; diff --git a/src/dev/jest/config.js b/src/plugins/console/jest.config.js similarity index 59% rename from src/dev/jest/config.js rename to src/plugins/console/jest.config.js index c04ef9480d0b7..f08613f91e1f1 100644 --- a/src/dev/jest/config.js +++ b/src/plugins/console/jest.config.js @@ -17,26 +17,9 @@ * under the License. */ -export default { +module.exports = { preset: '@kbn/test', rootDir: '../../..', - roots: [ - '/src/plugins', - '/src/legacy/ui', - '/src/core', - '/src/legacy/server', - '/src/cli', - '/src/cli_keystore', - '/src/cli_encryption_keys', - '/src/cli_plugin', - '/packages/kbn-test/target/functional_test_runner', - '/src/dev', - '/src/optimize', - '/src/legacy/utils', - '/src/setup_node_env', - '/packages', - '/test/functional/services/remote', - '/src/dev/code_coverage/ingest_coverage', - ], + roots: ['/src/plugins/console'], testRunner: 'jasmine2', }; diff --git a/src/plugins/dashboard/jest.config.js b/src/plugins/dashboard/jest.config.js new file mode 100644 index 0000000000000..b9f6f66159b30 --- /dev/null +++ b/src/plugins/dashboard/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/dashboard'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index bd19a9f0d9cd3..b5451203e2365 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -9,9 +9,10 @@ "urlForwarding", "navigation", "uiActions", - "savedObjects" + "savedObjects", + "share" ], - "optionalPlugins": ["home", "share", "usageCollection", "savedObjectsTaggingOss"], + "optionalPlugins": ["home", "usageCollection", "savedObjectsTaggingOss"], "server": true, "ui": true, "requiredBundles": ["kibanaUtils", "kibanaReact", "home"] diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx new file mode 100644 index 0000000000000..770e01d6190cb --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart } from 'kibana/public'; + +import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '../../embeddable_plugin'; +import { DashboardContainer } from '../../application/embeddable'; +import { getSampleDashboardInput, getSampleDashboardPanel } from '../../application/test_helpers'; +import { + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardExportableEmbeddableFactory, + CONTACT_CARD_EXPORTABLE_EMBEDDABLE, +} from '../../embeddable_plugin_test_samples'; +import { coreMock } from '../../../../../core/public/mocks'; +import { ExportCSVAction } from './export_csv_action'; +import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; +import { DataPublicPluginStart } from '../../../../data/public/types'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { LINE_FEED_CHARACTER } from 'src/plugins/data/common/exports/export_csv'; + +describe('Export CSV action', () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( + CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + new ContactCardExportableEmbeddableFactory((() => null) as any, {} as any) + ); + const start = doStart(); + + let container: DashboardContainer; + let embeddable: ContactCardEmbeddable; + let coreStart: CoreStart; + let dataMock: jest.Mocked; + + beforeEach(async () => { + coreStart = coreMock.createStart(); + coreStart.savedObjects.client = { + ...coreStart.savedObjects.client, + get: jest.fn().mockImplementation(() => ({ attributes: { title: 'Holy moly' } })), + find: jest.fn().mockImplementation(() => ({ total: 15 })), + create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })), + }; + + const options = { + ExitFullScreenButton: () => null, + SavedObjectFinder: () => null, + application: {} as any, + embeddable: start, + inspector: {} as any, + notifications: {} as any, + overlays: coreStart.overlays, + savedObjectMetaData: {} as any, + uiActions: {} as any, + }; + const input = getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Kibanana', id: '123' }, + type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + }), + }, + }); + container = new DashboardContainer(input, options); + dataMock = dataPluginMock.createStartContract(); + + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EXPORTABLE_EMBEDDABLE, { + firstName: 'Kibana', + }); + + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } else { + embeddable = contactCardEmbeddable; + } + }); + + test('Download is incompatible with embeddables without getInspectorAdapters implementation', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); + }); + + test('Should download a compatible Embeddable', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const result = ((await action.execute({ embeddable, asString: true })) as unknown) as + | undefined + | Record; + expect(result).toEqual({ + 'Hello Kibana.csv': { + content: `First Name,Last Name${LINE_FEED_CHARACTER}Kibana,undefined${LINE_FEED_CHARACTER}`, + type: 'text/plain;charset=utf-8', + }, + }); + }); + + test('Should not download incompatible Embeddable', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + const result = ((await action.execute({ + embeddable: errorEmbeddable, + asString: true, + })) as unknown) as undefined | Record; + expect(result).toBeUndefined(); + }); +}); diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx new file mode 100644 index 0000000000000..48a7877f9383e --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -0,0 +1,138 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Datatable } from 'src/plugins/expressions/public'; +import { FormatFactory } from '../../../../data/common/field_formats/utils'; +import { DataPublicPluginStart, exporters } from '../../../../data/public'; +import { downloadMultipleAs } from '../../../../share/public'; +import { Adapters, IEmbeddable } from '../../../../embeddable/public'; +import { ActionByType } from '../../../../ui_actions/public'; +import { CoreStart } from '../../../../../core/public'; + +export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; + +export interface Params { + core: CoreStart; + data: DataPublicPluginStart; +} + +export interface ExportContext { + embeddable?: IEmbeddable; + // used for testing + asString?: boolean; +} + +/** + * This is "Export CSV" action which appears in the context + * menu of a dashboard panel. + */ +export class ExportCSVAction implements ActionByType { + public readonly id = ACTION_EXPORT_CSV; + + public readonly type = ACTION_EXPORT_CSV; + + public readonly order = 5; + + constructor(protected readonly params: Params) {} + + public getIconType() { + return 'exportAction'; + } + + public readonly getDisplayName = (context: ExportContext): string => + i18n.translate('dashboard.actions.DownloadCreateDrilldownAction.displayName', { + defaultMessage: 'Download as CSV', + }); + + public async isCompatible(context: ExportContext): Promise { + return !!this.hasDatatableContent(context.embeddable?.getInspectorAdapters?.()); + } + + private hasDatatableContent = (adapters: Adapters | undefined) => { + return Object.keys(adapters?.tables || {}).length > 0; + }; + + private getFormatter = (): FormatFactory | undefined => { + if (this.params.data) { + return this.params.data.fieldFormats.deserialize; + } + }; + + private getDataTableContent = (adapters: Adapters | undefined) => { + if (this.hasDatatableContent(adapters)) { + return adapters?.tables; + } + return; + }; + + private exportCSV = async (context: ExportContext) => { + const formatFactory = this.getFormatter(); + // early exit if not formatter is available + if (!formatFactory) { + return; + } + const tableAdapters = this.getDataTableContent( + context?.embeddable?.getInspectorAdapters() + ) as Record; + + if (tableAdapters) { + const datatables = Object.values(tableAdapters); + const content = datatables.reduce>( + (memo, datatable, i) => { + // skip empty datatables + if (datatable) { + const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + const untitledFilename = i18n.translate( + 'dashboard.actions.downloadOptionsUnsavedFilename', + { + defaultMessage: 'unsaved', + } + ); + + memo[`${context!.embeddable!.getTitle() || untitledFilename}${postFix}.csv`] = { + content: exporters.datatableToCSV(datatable, { + csvSeparator: this.params.core.uiSettings.get('csv:separator', ','), + quoteValues: this.params.core.uiSettings.get('csv:quoteValues', true), + formatFactory, + }), + type: exporters.CSV_MIME_TYPE, + }; + } + return memo; + }, + {} + ); + + // useful for testing + if (context.asString) { + return (content as unknown) as Promise; + } + + if (content) { + return downloadMultipleAs(content); + } + } + }; + + public async execute(context: ExportContext): Promise { + // make it testable: type here will be forced + return await this.exportCSV(context); + } +} diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index cd32c2025456f..3d7ebe76cb66a 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -47,3 +47,4 @@ export { LibraryNotificationAction, ACTION_LIBRARY_NOTIFICATION, } from './library_notification_action'; +export { ExportContext, ExportCSVAction, ACTION_EXPORT_CSV } from './export_csv_action'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index c47a4c2d21b11..76b1ccc037e89 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -101,6 +101,11 @@ import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; +import { + ACTION_EXPORT_CSV, + ExportContext, + ExportCSVAction, +} from './application/actions/export_csv_action'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -160,6 +165,7 @@ declare module '../../../plugins/ui_actions/public' { [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext; [ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext; [ACTION_LIBRARY_NOTIFICATION]: LibraryNotificationActionContext; + [ACTION_EXPORT_CSV]: ExportContext; } } @@ -414,7 +420,7 @@ export class DashboardPlugin public start(core: CoreStart, plugins: StartDependencies): DashboardStart { const { notifications } = core; - const { uiActions } = plugins; + const { uiActions, data, share } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -431,6 +437,11 @@ export class DashboardPlugin uiActions.registerAction(clonePanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); + if (share) { + const ExportCSVPlugin = new ExportCSVAction({ core, data }); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, ExportCSVPlugin); + } + if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(addToLibraryAction); diff --git a/src/plugins/data/common/exports/export_csv.tsx b/src/plugins/data/common/exports/export_csv.tsx index 1e1420c245eb4..116586c5b66e8 100644 --- a/src/plugins/data/common/exports/export_csv.tsx +++ b/src/plugins/data/common/exports/export_csv.tsx @@ -22,7 +22,7 @@ import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { Datatable } from 'src/plugins/expressions'; -const LINE_FEED_CHARACTER = '\r\n'; +export const LINE_FEED_CHARACTER = '\r\n'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; const allDoubleQuoteRE = /"/g; export const CSV_MIME_TYPE = 'text/plain;charset=utf-8'; diff --git a/src/plugins/data/jest.config.js b/src/plugins/data/jest.config.js new file mode 100644 index 0000000000000..3c6e854a53d7b --- /dev/null +++ b/src/plugins/data/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/data'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/discover/jest.config.js b/src/plugins/discover/jest.config.js new file mode 100644 index 0000000000000..0723569db357d --- /dev/null +++ b/src/plugins/discover/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/discover'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index 17f3199b75b15..e45f18606e3fc 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -18,20 +18,15 @@ */ import { find, template } from 'lodash'; -import { stringify } from 'query-string'; import $ from 'jquery'; -import rison from 'rison-node'; -import '../../doc_viewer'; - import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; - -import { dispatchRenderComplete, url } from '../../../../../../kibana_utils/public'; +import { dispatchRenderComplete } from '../../../../../../kibana_utils/public'; import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; import cellTemplateHtml from '../components/table_row/cell.html'; import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html'; -import { esFilters } from '../../../../../../data/public'; import { getServices } from '../../../../kibana_services'; +import { getContextUrl } from '../../../helpers/get_context_url'; const TAGS_WITH_WS = />\s+ { - const globalFilters: any = getServices().filterManager.getGlobalFilters(); - const appFilters: any = getServices().filterManager.getAppFilters(); - - const hash = stringify( - url.encodeQuery({ - _g: rison.encode({ - filters: globalFilters || [], - }), - _a: rison.encode({ - columns: $scope.columns, - filters: (appFilters || []).map(esFilters.disableFilter), - }), - }), - { encode: false, sort: false } + return getContextUrl( + $scope.row._id, + $scope.indexPattern.id, + $scope.columns, + getServices().filterManager ); - - return `#/context/${encodeURIComponent($scope.indexPattern.id)}/${encodeURIComponent( - $scope.row._id - )}?${hash}`; }; // create a tr element that lists the value for each *column* diff --git a/src/plugins/discover/public/application/helpers/get_context_url.test.ts b/src/plugins/discover/public/application/helpers/get_context_url.test.ts new file mode 100644 index 0000000000000..481ea6b1a5b4f --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_context_url.test.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getContextUrl } from './get_context_url'; +import { FilterManager } from '../../../../data/public/query/filter_manager'; +const filterManager = ({ + getGlobalFilters: () => [], + getAppFilters: () => [], +} as unknown) as FilterManager; + +describe('Get context url', () => { + test('returning a valid context url', async () => { + const url = await getContextUrl('docId', 'ipId', ['test1', 'test2'], filterManager); + expect(url).toMatchInlineSnapshot( + `"#/context/ipId/docId?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` + ); + }); + + test('returning a valid context url when docId contains whitespace', async () => { + const url = await getContextUrl('doc Id', 'ipId', ['test1', 'test2'], filterManager); + expect(url).toMatchInlineSnapshot( + `"#/context/ipId/doc%20Id?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` + ); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/get_context_url.tsx b/src/plugins/discover/public/application/helpers/get_context_url.tsx new file mode 100644 index 0000000000000..b159341cbe28d --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_context_url.tsx @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { stringify } from 'query-string'; +import rison from 'rison-node'; +import { url } from '../../../../kibana_utils/common'; +import { esFilters, FilterManager } from '../../../../data/public'; + +/** + * Helper function to generate an URL to a document in Discover's context view + */ +export function getContextUrl( + documentId: string, + indexPatternId: string, + columns: string[], + filterManager: FilterManager +) { + const globalFilters = filterManager.getGlobalFilters(); + const appFilters = filterManager.getAppFilters(); + + const hash = stringify( + url.encodeQuery({ + _g: rison.encode({ + filters: globalFilters || [], + }), + _a: rison.encode({ + columns, + filters: (appFilters || []).map(esFilters.disableFilter), + }), + }), + { encode: false, sort: false } + ); + + return `#/context/${encodeURIComponent(indexPatternId)}/${encodeURIComponent( + documentId + )}?${hash}`; +} diff --git a/src/plugins/embeddable/jest.config.js b/src/plugins/embeddable/jest.config.js new file mode 100644 index 0000000000000..a079791092549 --- /dev/null +++ b/src/plugins/embeddable/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/embeddable'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx new file mode 100644 index 0000000000000..338eb4877a50a --- /dev/null +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ContactCardEmbeddable } from './contact_card_embeddable'; + +export class ContactCardExportableEmbeddable extends ContactCardEmbeddable { + public getInspectorAdapters = () => { + return { + tables: { + layer1: { + type: 'datatable', + columns: [ + { id: 'firstName', name: 'First Name' }, + { id: 'originalLastName', name: 'Last Name' }, + ], + rows: [ + { + firstName: this.getInput().firstName, + orignialLastName: this.getInput().lastName, + }, + ], + }, + }, + }; + }; +} + +export const CONTACT_EXPORTABLE_USER_TRIGGER = 'CONTACT_EXPORTABLE_USER_TRIGGER'; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx new file mode 100644 index 0000000000000..5b8827ac6fc2a --- /dev/null +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; + +import { CoreStart } from 'src/core/public'; +import { toMountPoint } from '../../../../../../kibana_react/public'; +import { EmbeddableFactoryDefinition } from '../../../embeddables'; +import { Container } from '../../../containers'; +import { ContactCardEmbeddableInput } from './contact_card_embeddable'; +import { ContactCardExportableEmbeddable } from './contact_card_exportable_embeddable'; +import { ContactCardInitializer } from './contact_card_initializer'; + +export const CONTACT_CARD_EXPORTABLE_EMBEDDABLE = 'CONTACT_CARD_EXPORTABLE_EMBEDDABLE'; + +export class ContactCardExportableEmbeddableFactory + implements EmbeddableFactoryDefinition { + public readonly type = CONTACT_CARD_EXPORTABLE_EMBEDDABLE; + + constructor( + private readonly execTrigger: UiActionsStart['executeTriggerActions'], + private readonly overlays: CoreStart['overlays'] + ) {} + + public async isEditable() { + return true; + } + + public getDisplayName() { + return i18n.translate('embeddableApi.samples.contactCard.displayName', { + defaultMessage: 'contact card', + }); + } + + public getExplicitInput = (): Promise> => { + return new Promise((resolve) => { + const modalSession = this.overlays.openModal( + toMountPoint( + { + modalSession.close(); + // @ts-expect-error + resolve(undefined); + }} + onCreate={(input: { firstName: string; lastName?: string }) => { + modalSession.close(); + resolve(input); + }} + /> + ), + { + 'data-test-subj': 'createContactCardEmbeddable', + } + ); + }); + }; + + public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => { + return new ContactCardExportableEmbeddable( + initialInput, + { + execAction: this.execTrigger, + }, + parent + ); + }; +} diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts index c79a4f517916e..a9006cdc7b477 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts @@ -20,5 +20,7 @@ export * from './contact_card'; export * from './contact_card_embeddable'; export * from './contact_card_embeddable_factory'; +export * from './contact_card_exportable_embeddable'; +export * from './contact_card_exportable_embeddable_factory'; export * from './contact_card_initializer'; export * from './slow_contact_card_embeddable_factory'; diff --git a/src/plugins/es_ui_shared/jest.config.js b/src/plugins/es_ui_shared/jest.config.js new file mode 100644 index 0000000000000..5b8b34692800d --- /dev/null +++ b/src/plugins/es_ui_shared/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/es_ui_shared'], +}; diff --git a/src/plugins/expressions/jest.config.js b/src/plugins/expressions/jest.config.js new file mode 100644 index 0000000000000..b4e3e10b3fc70 --- /dev/null +++ b/src/plugins/expressions/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/expressions'], +}; diff --git a/src/plugins/home/jest.config.js b/src/plugins/home/jest.config.js new file mode 100644 index 0000000000000..c56c7b3eed1d6 --- /dev/null +++ b/src/plugins/home/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/home'], +}; diff --git a/src/plugins/index_pattern_management/jest.config.js b/src/plugins/index_pattern_management/jest.config.js new file mode 100644 index 0000000000000..8a499406080fd --- /dev/null +++ b/src/plugins/index_pattern_management/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/index_pattern_management'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/input_control_vis/jest.config.js b/src/plugins/input_control_vis/jest.config.js new file mode 100644 index 0000000000000..17fb6f3359bf3 --- /dev/null +++ b/src/plugins/input_control_vis/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/input_control_vis'], +}; diff --git a/src/plugins/inspector/jest.config.js b/src/plugins/inspector/jest.config.js new file mode 100644 index 0000000000000..6fc4a063970b9 --- /dev/null +++ b/src/plugins/inspector/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/inspector'], +}; diff --git a/src/plugins/kibana_legacy/jest.config.js b/src/plugins/kibana_legacy/jest.config.js new file mode 100644 index 0000000000000..69df43bc5b15f --- /dev/null +++ b/src/plugins/kibana_legacy/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/kibana_legacy'], +}; diff --git a/src/plugins/kibana_overview/jest.config.js b/src/plugins/kibana_overview/jest.config.js new file mode 100644 index 0000000000000..4a719b38e47ae --- /dev/null +++ b/src/plugins/kibana_overview/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/kibana_overview'], +}; diff --git a/src/plugins/kibana_react/jest.config.js b/src/plugins/kibana_react/jest.config.js new file mode 100644 index 0000000000000..2810331c9b667 --- /dev/null +++ b/src/plugins/kibana_react/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/kibana_react'], +}; diff --git a/src/plugins/kibana_usage_collection/jest.config.js b/src/plugins/kibana_usage_collection/jest.config.js new file mode 100644 index 0000000000000..9510fc98732b3 --- /dev/null +++ b/src/plugins/kibana_usage_collection/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/kibana_usage_collection'], +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index 3fd011b0bded2..a514f9f899e55 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -65,7 +65,7 @@ export function getCoreUsageCollector( }, xsrf: { disableProtection: { type: 'boolean' }, - whitelistConfigured: { type: 'boolean' }, + allowlistConfigured: { type: 'boolean' }, }, requestId: { allowFromAnyIp: { type: 'boolean' }, diff --git a/src/plugins/kibana_utils/jest.config.js b/src/plugins/kibana_utils/jest.config.js new file mode 100644 index 0000000000000..2ddfb7047bf2e --- /dev/null +++ b/src/plugins/kibana_utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/kibana_utils'], +}; diff --git a/src/plugins/kibana_utils/public/state_management/url/format.ts b/src/plugins/kibana_utils/public/state_management/url/format.ts index 4497e509bc86b..4a3d725de7e47 100644 --- a/src/plugins/kibana_utils/public/state_management/url/format.ts +++ b/src/plugins/kibana_utils/public/state_management/url/format.ts @@ -27,6 +27,9 @@ export function replaceUrlQuery( queryReplacer: (query: ParsedQuery) => ParsedQuery ) { const url = parseUrl(rawUrl); + // @ts-expect-error `queryReplacer` expects key/value pairs with values of type `string | string[] | null`, + // however `@types/node` says that `url.query` has values of type `string | string[] | undefined`. + // After investigating this, it seems that no matter what the values will be of type `string | string[]` const newQuery = queryReplacer(url.query || {}); const searchQueryString = stringify(urlUtils.encodeQuery(newQuery), { sort: false, @@ -45,6 +48,9 @@ export function replaceUrlHashQuery( ) { const url = parseUrl(rawUrl); const hash = parseUrlHash(rawUrl); + // @ts-expect-error `queryReplacer` expects key/value pairs with values of type `string | string[] | null`, + // however `@types/node` says that `url.query` has values of type `string | string[] | undefined`. + // After investigating this, it seems that no matter what the values will be of type `string | string[]` const newQuery = queryReplacer(hash?.query || {}); const searchQueryString = stringify(urlUtils.encodeQuery(newQuery), { sort: false, diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index cb3c9470c7abd..8ec7ad00d7926 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -252,10 +252,16 @@ export function getRelativeToHistoryPath(absoluteUrl: string, history: History): return formatUrl({ pathname: stripBasename(parsedUrl.pathname ?? null), + // @ts-expect-error `urlUtils.encodeQuery` expects key/value pairs with values of type `string | string[] | null`, + // however `@types/node` says that `url.query` has values of type `string | string[] | undefined`. + // After investigating this, it seems that no matter what the values will be of type `string | string[]` search: stringify(urlUtils.encodeQuery(parsedUrl.query), { sort: false, encode: false }), hash: parsedHash ? formatUrl({ pathname: parsedHash.pathname, + // @ts-expect-error `urlUtils.encodeQuery` expects key/value pairs with values of type `string | string[] | null`, + // however `@types/node` says that `url.query` has values of type `string | string[] | undefined`. + // After investigating this, it seems that no matter what the values will be of type `string | string[]` search: stringify(urlUtils.encodeQuery(parsedHash.query), { sort: false, encode: false }), }) : parsedUrl.hash, diff --git a/src/plugins/legacy_export/jest.config.js b/src/plugins/legacy_export/jest.config.js new file mode 100644 index 0000000000000..1480049fd8f85 --- /dev/null +++ b/src/plugins/legacy_export/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/legacy_export'], +}; diff --git a/src/plugins/management/jest.config.js b/src/plugins/management/jest.config.js new file mode 100644 index 0000000000000..287bafc4b1c11 --- /dev/null +++ b/src/plugins/management/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/management'], +}; diff --git a/src/plugins/maps_legacy/jest.config.js b/src/plugins/maps_legacy/jest.config.js new file mode 100644 index 0000000000000..849bd2957ba62 --- /dev/null +++ b/src/plugins/maps_legacy/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/maps_legacy'], +}; diff --git a/src/plugins/navigation/jest.config.js b/src/plugins/navigation/jest.config.js new file mode 100644 index 0000000000000..bc999a25854de --- /dev/null +++ b/src/plugins/navigation/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/navigation'], +}; diff --git a/src/plugins/newsfeed/jest.config.js b/src/plugins/newsfeed/jest.config.js new file mode 100644 index 0000000000000..bf530497bcbad --- /dev/null +++ b/src/plugins/newsfeed/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/newsfeed'], +}; diff --git a/src/plugins/region_map/jest.config.js b/src/plugins/region_map/jest.config.js new file mode 100644 index 0000000000000..c0d4e4d40bb3a --- /dev/null +++ b/src/plugins/region_map/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/region_map'], +}; diff --git a/src/plugins/saved_objects/jest.config.js b/src/plugins/saved_objects/jest.config.js new file mode 100644 index 0000000000000..00ab010bc61ba --- /dev/null +++ b/src/plugins/saved_objects/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/saved_objects'], +}; diff --git a/src/plugins/saved_objects_management/jest.config.js b/src/plugins/saved_objects_management/jest.config.js new file mode 100644 index 0000000000000..3cedb8c937f5e --- /dev/null +++ b/src/plugins/saved_objects_management/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/saved_objects_management'], +}; diff --git a/src/plugins/saved_objects_tagging_oss/jest.config.js b/src/plugins/saved_objects_tagging_oss/jest.config.js new file mode 100644 index 0000000000000..7e75b5c5593e7 --- /dev/null +++ b/src/plugins/saved_objects_tagging_oss/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/saved_objects_tagging_oss'], +}; diff --git a/src/plugins/security_oss/jest.config.js b/src/plugins/security_oss/jest.config.js new file mode 100644 index 0000000000000..3bf6ee33d3e48 --- /dev/null +++ b/src/plugins/security_oss/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/security_oss'], +}; diff --git a/src/plugins/share/jest.config.js b/src/plugins/share/jest.config.js new file mode 100644 index 0000000000000..39b048279e73b --- /dev/null +++ b/src/plugins/share/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/share'], +}; diff --git a/src/plugins/telemetry/jest.config.js b/src/plugins/telemetry/jest.config.js new file mode 100644 index 0000000000000..914cea68cd01b --- /dev/null +++ b/src/plugins/telemetry/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/telemetry'], +}; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 3d79d7c6cf0e1..e1078c60caf2e 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1391,7 +1391,7 @@ "disableProtection": { "type": "boolean" }, - "whitelistConfigured": { + "allowlistConfigured": { "type": "boolean" } } diff --git a/src/plugins/telemetry_collection_manager/jest.config.js b/src/plugins/telemetry_collection_manager/jest.config.js new file mode 100644 index 0000000000000..9278ca21d7bc2 --- /dev/null +++ b/src/plugins/telemetry_collection_manager/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/telemetry_collection_manager'], +}; diff --git a/src/plugins/telemetry_management_section/jest.config.js b/src/plugins/telemetry_management_section/jest.config.js new file mode 100644 index 0000000000000..a38fa84b08afc --- /dev/null +++ b/src/plugins/telemetry_management_section/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/telemetry_management_section'], +}; diff --git a/src/plugins/tile_map/jest.config.js b/src/plugins/tile_map/jest.config.js new file mode 100644 index 0000000000000..9a89247b4f782 --- /dev/null +++ b/src/plugins/tile_map/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/tile_map'], +}; diff --git a/src/plugins/ui_actions/jest.config.js b/src/plugins/ui_actions/jest.config.js new file mode 100644 index 0000000000000..3a7de575ea248 --- /dev/null +++ b/src/plugins/ui_actions/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/ui_actions'], +}; diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts index 3a598b547e343..3111a0b55084c 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts @@ -20,25 +20,31 @@ import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { buildContextMenuForActions } from './build_eui_context_menu_panels'; import { Action, createAction } from '../actions'; +import { PresentableGrouping } from '../util'; const createTestAction = ({ type, dispayName, order, + grouping = undefined, }: { type?: string; dispayName: string; order?: number; + grouping?: PresentableGrouping; }) => createAction({ type: type as any, // mapping doesn't matter for this test getDisplayName: () => dispayName, order, execute: async () => {}, + grouping, }); const resultMapper = (panel: EuiContextMenuPanelDescriptor) => ({ - items: panel.items ? panel.items.map((item) => ({ name: item.name })) : [], + items: panel.items + ? panel.items.map((item) => ({ name: item.isSeparator ? 'SEPARATOR' : item.name })) + : [], }); test('sorts items in DESC order by "order" field first, then by display name', async () => { @@ -237,3 +243,197 @@ test('hides items behind in "More" submenu if there are more than 4 actions', as ] `); }); + +test('separates grouped items from main items with a separator', async () => { + const actions = [ + createTestAction({ + dispayName: 'Foo 1', + }), + createTestAction({ + dispayName: 'Foo 2', + }), + createTestAction({ + dispayName: 'Foo 3', + }), + createTestAction({ + dispayName: 'Foo 4', + grouping: [ + { + id: 'testGroup', + getDisplayName: () => 'Test group', + }, + ], + }), + ]; + const menu = await buildContextMenuForActions({ + actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })), + }); + + expect(menu.map(resultMapper)).toMatchInlineSnapshot(` + Array [ + Object { + "items": Array [ + Object { + "name": "Foo 1", + }, + Object { + "name": "Foo 2", + }, + Object { + "name": "Foo 3", + }, + Object { + "name": "SEPARATOR", + }, + Object { + "name": "Foo 4", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 4", + }, + ], + }, + ] + `); +}); + +test('separates multiple groups each with its own separator', async () => { + const actions = [ + createTestAction({ + dispayName: 'Foo 1', + }), + createTestAction({ + dispayName: 'Foo 2', + }), + createTestAction({ + dispayName: 'Foo 3', + }), + createTestAction({ + dispayName: 'Foo 4', + grouping: [ + { + id: 'testGroup', + getDisplayName: () => 'Test group', + }, + ], + }), + createTestAction({ + dispayName: 'Foo 5', + grouping: [ + { + id: 'testGroup2', + getDisplayName: () => 'Test group 2', + }, + ], + }), + ]; + const menu = await buildContextMenuForActions({ + actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })), + }); + + expect(menu.map(resultMapper)).toMatchInlineSnapshot(` + Array [ + Object { + "items": Array [ + Object { + "name": "Foo 1", + }, + Object { + "name": "Foo 2", + }, + Object { + "name": "Foo 3", + }, + Object { + "name": "SEPARATOR", + }, + Object { + "name": "Foo 4", + }, + Object { + "name": "SEPARATOR", + }, + Object { + "name": "Foo 5", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 4", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 5", + }, + ], + }, + ] + `); +}); + +test('does not add separator for first grouping if there are no main items', async () => { + const actions = [ + createTestAction({ + dispayName: 'Foo 4', + grouping: [ + { + id: 'testGroup', + getDisplayName: () => 'Test group', + }, + ], + }), + createTestAction({ + dispayName: 'Foo 5', + grouping: [ + { + id: 'testGroup2', + getDisplayName: () => 'Test group 2', + }, + ], + }), + ]; + const menu = await buildContextMenuForActions({ + actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })), + }); + + expect(menu.map(resultMapper)).toMatchInlineSnapshot(` + Array [ + Object { + "items": Array [ + Object { + "name": "Foo 4", + }, + Object { + "name": "SEPARATOR", + }, + Object { + "name": "Foo 5", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 4", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 5", + }, + ], + }, + ] + `); +}); diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index c7efb6dad326d..63586ca3da1f7 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -201,10 +201,12 @@ export async function buildContextMenuForActions({ for (const panel of Object.values(panels)) { if (panel._level === 0) { - panels.mainMenu.items.push({ - isSeparator: true, - key: panel.id + '__separator', - }); + if (panels.mainMenu.items.length > 0) { + panels.mainMenu.items.push({ + isSeparator: true, + key: panel.id + '__separator', + }); + } if (panel.items.length > 3) { panels.mainMenu.items.push({ name: panel.title || panel.id, diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index 3a14f49169e09..ca27e19b247c2 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -234,7 +234,7 @@ export class UiActionsService { // // (undocumented) protected readonly actions: ActionRegistry; - readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; + readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; // (undocumented) readonly attachAction: (triggerId: T, actionId: string) => void; readonly clear: () => void; @@ -248,21 +248,21 @@ export class UiActionsService { readonly executionService: UiActionsExecutionService; readonly fork: () => UiActionsService; // (undocumented) - readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; + readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; // Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts // // (undocumented) readonly getTrigger: (triggerId: T) => TriggerContract; // (undocumented) - readonly getTriggerActions: (triggerId: T) => Action[]; + readonly getTriggerActions: (triggerId: T) => Action[]; // (undocumented) - readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; + readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; // (undocumented) readonly hasAction: (actionId: string) => boolean; // Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; + readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; // (undocumented) readonly registerTrigger: (trigger: Trigger) => void; // Warning: (ae-forgotten-export) The symbol "TriggerRegistry" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/url_forwarding/jest.config.js b/src/plugins/url_forwarding/jest.config.js new file mode 100644 index 0000000000000..9dcbfccfcf90a --- /dev/null +++ b/src/plugins/url_forwarding/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/url_forwarding'], +}; diff --git a/src/plugins/usage_collection/jest.config.js b/src/plugins/usage_collection/jest.config.js new file mode 100644 index 0000000000000..89b7fc70fd620 --- /dev/null +++ b/src/plugins/usage_collection/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/usage_collection'], +}; diff --git a/src/plugins/vis_default_editor/jest.config.js b/src/plugins/vis_default_editor/jest.config.js new file mode 100644 index 0000000000000..618f9734fb54c --- /dev/null +++ b/src/plugins/vis_default_editor/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_default_editor'], +}; diff --git a/src/plugins/vis_type_markdown/jest.config.js b/src/plugins/vis_type_markdown/jest.config.js new file mode 100644 index 0000000000000..bff1b12641c92 --- /dev/null +++ b/src/plugins/vis_type_markdown/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_markdown'], +}; diff --git a/src/plugins/vis_type_metric/jest.config.js b/src/plugins/vis_type_metric/jest.config.js new file mode 100644 index 0000000000000..5c50fc5f4368e --- /dev/null +++ b/src/plugins/vis_type_metric/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_metric'], +}; diff --git a/src/plugins/vis_type_table/jest.config.js b/src/plugins/vis_type_table/jest.config.js new file mode 100644 index 0000000000000..3aa02089df012 --- /dev/null +++ b/src/plugins/vis_type_table/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_table'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/vis_type_tagcloud/jest.config.js b/src/plugins/vis_type_tagcloud/jest.config.js new file mode 100644 index 0000000000000..5419ca05cca84 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_tagcloud'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/vis_type_timelion/jest.config.js b/src/plugins/vis_type_timelion/jest.config.js new file mode 100644 index 0000000000000..eae12936427f4 --- /dev/null +++ b/src/plugins/vis_type_timelion/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_timelion'], +}; diff --git a/src/plugins/vis_type_timeseries/jest.config.js b/src/plugins/vis_type_timeseries/jest.config.js new file mode 100644 index 0000000000000..16c001e598188 --- /dev/null +++ b/src/plugins/vis_type_timeseries/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_timeseries'], +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js index 904a27dcb23c2..4b5038b82f480 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js @@ -49,12 +49,12 @@ export class MarkdownEditor extends Component { } render() { - const { visData, model, dateFormat } = this.props; + const { visData, model, getConfig } = this.props; if (!visData) { return null; } - + const dateFormat = getConfig('dateFormat'); const series = _.get(visData, `${model.id}.series`, []); const variables = convertSeriesToVars(series, model, dateFormat, this.props.getConfig); const rows = []; @@ -214,6 +214,6 @@ export class MarkdownEditor extends Component { MarkdownEditor.propTypes = { onChange: PropTypes.func, model: PropTypes.object, - dateFormat: PropTypes.string, + getConfig: PropTypes.func, visData: PropTypes.object, }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js index 3b081d8eb7db9..999127a9eb556 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js @@ -90,6 +90,6 @@ PanelConfig.propTypes = { fields: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - dateFormat: PropTypes.string, visData$: PropTypes.object, + getConfig: PropTypes.func, }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js index 36d0e3a80e227..ef7aec61a2f0d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js @@ -334,7 +334,6 @@ MarkdownPanelConfigUi.propTypes = { fields: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - dateFormat: PropTypes.string, }; export const MarkdownPanelConfig = injectI18n(MarkdownPanelConfigUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index 5b5c99b970854..454f6ff855b38 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -96,7 +96,6 @@ function TimeseriesVisualization({ if (VisComponent) { return ( diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts index 56e58b4da3458..e6104ad08fe9e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts @@ -64,6 +64,5 @@ export interface TimeseriesVisProps { ) => void; uiState: PersistedState; visData: TimeseriesVisData; - dateFormat: string; getConfig: IUiSettingsClient['get']; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js index e68b9e5ed8467..ffc6bf0dda2d4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js @@ -31,9 +31,9 @@ import { isBackgroundInverted } from '../../../lib/set_is_reversed'; const getMarkdownId = (id) => `markdown-${id}`; function MarkdownVisualization(props) { - const { backgroundColor, model, visData, dateFormat } = props; + const { backgroundColor, model, visData, getConfig } = props; const series = get(visData, `${model.id}.series`, []); - const variables = convertSeriesToVars(series, model, dateFormat, props.getConfig); + const variables = convertSeriesToVars(series, model, getConfig('dateFormat'), props.getConfig); const markdownElementId = getMarkdownId(uuid.v1()); const panelBackgroundColor = model.background_color || backgroundColor; @@ -103,7 +103,6 @@ MarkdownVisualization.propTypes = { onBrush: PropTypes.func, onChange: PropTypes.func, visData: PropTypes.object, - dateFormat: PropTypes.string, getConfig: PropTypes.func, }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index b752699fa1548..41837fbfb1d21 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -39,20 +39,18 @@ class TimeseriesVisualization extends Component { model: PropTypes.object, onBrush: PropTypes.func, visData: PropTypes.object, - dateFormat: PropTypes.string, getConfig: PropTypes.func, }; - xAxisFormatter = (interval) => (val) => { - const scaledDataFormat = this.props.getConfig('dateFormat:scaled'); - const { dateFormat } = this.props; + scaledDataFormat = this.props.getConfig('dateFormat:scaled'); + dateFormat = this.props.getConfig('dateFormat'); - if (!scaledDataFormat || !dateFormat) { + xAxisFormatter = (interval) => (val) => { + if (!this.scaledDataFormat || !this.dateFormat) { return val; } - const formatter = createXaxisFormatter(interval, scaledDataFormat, dateFormat); - + const formatter = createXaxisFormatter(interval, this.scaledDataFormat, this.dateFormat); return formatter(val); }; diff --git a/src/plugins/vis_type_vega/jest.config.js b/src/plugins/vis_type_vega/jest.config.js new file mode 100644 index 0000000000000..a9ae68df0d89b --- /dev/null +++ b/src/plugins/vis_type_vega/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_vega'], +}; diff --git a/src/plugins/vis_type_vislib/jest.config.js b/src/plugins/vis_type_vislib/jest.config.js new file mode 100644 index 0000000000000..1324ec1404b3e --- /dev/null +++ b/src/plugins/vis_type_vislib/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_vislib'], +}; diff --git a/src/plugins/visualizations/jest.config.js b/src/plugins/visualizations/jest.config.js new file mode 100644 index 0000000000000..b1c5067cfe4a9 --- /dev/null +++ b/src/plugins/visualizations/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/visualizations'], +}; diff --git a/src/plugins/visualize/jest.config.js b/src/plugins/visualize/jest.config.js new file mode 100644 index 0000000000000..6657f4092068f --- /dev/null +++ b/src/plugins/visualize/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/visualize'], +}; diff --git a/src/setup_node_env/jest.config.js b/src/setup_node_env/jest.config.js new file mode 100644 index 0000000000000..61e3239905836 --- /dev/null +++ b/src/setup_node_env/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/setup_node_env'], +}; diff --git a/src/test_utils/jest.config.js b/src/test_utils/jest.config.js new file mode 100644 index 0000000000000..b7e77413598c0 --- /dev/null +++ b/src/test_utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/src/test_utils'], +}; diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 672becca614c9..e06783174e83b 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -31,8 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'dateFormat:tz': 'Europe/Berlin', }; - // FLAKY: https://github.com/elastic/kibana/issues/81576 - describe.skip('discover histogram', function describeIndexTests() { + describe('discover histogram', function describeIndexTests() { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('long_window_logstash'); diff --git a/test/functional/jest.config.js b/test/functional/jest.config.js new file mode 100644 index 0000000000000..60dce5d95773a --- /dev/null +++ b/test/functional/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/test/functional'], +}; diff --git a/test/scripts/checks/jest_configs.sh b/test/scripts/checks/jest_configs.sh new file mode 100644 index 0000000000000..28cb1386c748f --- /dev/null +++ b/test/scripts/checks/jest_configs.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +checks-reporter-with-killswitch "Check Jest Configs" node scripts/check_jest_configs diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 89302e91ad479..fd96c2bbf8e78 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -6,6 +6,7 @@ def check() { tasks([ kibanaPipeline.scriptTask('Check Telemetry Schema', 'test/scripts/checks/telemetry.sh'), kibanaPipeline.scriptTask('Check TypeScript Projects', 'test/scripts/checks/ts_projects.sh'), + kibanaPipeline.scriptTask('Check Jest Configs', 'test/scripts/checks/jest_configs.sh'), kibanaPipeline.scriptTask('Check Doc API Changes', 'test/scripts/checks/doc_api_changes.sh'), kibanaPipeline.scriptTask('Check Types', 'test/scripts/checks/type_check.sh'), kibanaPipeline.scriptTask('Check Bundle Limits', 'test/scripts/checks/bundle_limits.sh'), diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js deleted file mode 100644 index 2ddc58500d15e..0000000000000 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function createJestConfig({ kibanaDirectory, rootDir }) { - return { - preset: '@kbn/test', - rootDir: kibanaDirectory, - roots: [`${rootDir}/plugins`], - reporters: [ - 'default', - [ - `${kibanaDirectory}/packages/kbn-test/target/jest/junit_reporter`, - { - reportName: 'X-Pack Jest Tests', - }, - ], - ], - }; -} diff --git a/x-pack/dev-tools/jest/index.js b/x-pack/dev-tools/jest/index.js deleted file mode 100644 index c22f8625c5778..0000000000000 --- a/x-pack/dev-tools/jest/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { run } from 'jest'; -import { resolve } from 'path'; - -import { createJestConfig } from './create_jest_config'; - -export function runJest() { - process.env.NODE_ENV = process.env.NODE_ENV || 'test'; - const config = JSON.stringify( - createJestConfig({ - kibanaDirectory: resolve(__dirname, '../../..'), - rootDir: resolve(__dirname, '../..'), - xPackKibanaDirectory: resolve(__dirname, '../..'), - }) - ); - - const argv = [...process.argv.slice(2), '--config', config]; - - return run(argv); -} diff --git a/x-pack/jest.config.js b/x-pack/jest.config.js new file mode 100644 index 0000000000000..8b3f717b40e66 --- /dev/null +++ b/x-pack/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + rootDir: '..', + projects: ['/x-pack/plugins/*/jest.config.js'], + reporters: [ + 'default', + ['/packages/kbn-test/target/jest/junit_reporter', { reportName: 'X-Pack Jest Tests' }], + ], +}; diff --git a/x-pack/plugins/actions/jest.config.js b/x-pack/plugins/actions/jest.config.js new file mode 100644 index 0000000000000..2aaa277079ad3 --- /dev/null +++ b/x-pack/plugins/actions/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/actions'], +}; diff --git a/x-pack/plugins/alerting_builtins/jest.config.js b/x-pack/plugins/alerting_builtins/jest.config.js new file mode 100644 index 0000000000000..05fe793a157df --- /dev/null +++ b/x-pack/plugins/alerting_builtins/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/alerting_builtins'], +}; diff --git a/x-pack/plugins/alerts/jest.config.js b/x-pack/plugins/alerts/jest.config.js new file mode 100644 index 0000000000000..d5c9ce5bbf4c2 --- /dev/null +++ b/x-pack/plugins/alerts/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/alerts'], +}; diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/log_level_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/log_level_rt.ts new file mode 100644 index 0000000000000..b488faa8e8fdc --- /dev/null +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/log_level_rt.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const logLevelRt = t.union([ + t.literal('trace'), + t.literal('debug'), + t.literal('info'), + t.literal('warning'), + t.literal('error'), + t.literal('critical'), + t.literal('off'), +]); diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index 2962a5fd2df3b..fc42af5ff7724 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -64,8 +64,38 @@ Array [ }, Object { "key": "log_level", - "type": "text", - "validationName": "string", + "options": Array [ + Object { + "text": "trace", + "value": "trace", + }, + Object { + "text": "debug", + "value": "debug", + }, + Object { + "text": "info", + "value": "info", + }, + Object { + "text": "warning", + "value": "warning", + }, + Object { + "text": "error", + "value": "error", + }, + Object { + "text": "critical", + "value": "critical", + }, + Object { + "text": "off", + "value": "off", + }, + ], + "type": "select", + "validationName": "(\\"trace\\" | \\"debug\\" | \\"info\\" | \\"warning\\" | \\"error\\" | \\"critical\\" | \\"off\\")", }, Object { "key": "profiling_inferred_spans_enabled", @@ -110,6 +140,11 @@ Array [ "type": "boolean", "validationName": "(\\"true\\" | \\"false\\")", }, + Object { + "key": "sanitize_field_names", + "type": "text", + "validationName": "string", + }, Object { "key": "server_timeout", "min": "1ms", @@ -170,6 +205,11 @@ Array [ "type": "float", "validationName": "floatRt", }, + Object { + "key": "transaction_ignore_urls", + "type": "text", + "validationName": "string", + }, Object { "key": "transaction_max_spans", "max": undefined, diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index e777e1fd09d0b..59a315830aec5 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { captureBodyRt } from '../runtime_types/capture_body_rt'; +import { logLevelRt } from '../runtime_types/log_level_rt'; import { RawSettingDefinition } from './types'; export const generalSettings: RawSettingDefinition[] = [ @@ -91,7 +92,8 @@ export const generalSettings: RawSettingDefinition[] = [ // LOG_LEVEL { key: 'log_level', - type: 'text', + validation: logLevelRt, + type: 'select', defaultValue: 'info', label: i18n.translate('xpack.apm.agentConfig.logLevel.label', { defaultMessage: 'Log level', @@ -99,7 +101,16 @@ export const generalSettings: RawSettingDefinition[] = [ description: i18n.translate('xpack.apm.agentConfig.logLevel.description', { defaultMessage: 'Sets the logging level for the agent', }), - includeAgents: ['dotnet', 'ruby'], + options: [ + { text: 'trace', value: 'trace' }, + { text: 'debug', value: 'debug' }, + { text: 'info', value: 'info' }, + { text: 'warning', value: 'warning' }, + { text: 'error', value: 'error' }, + { text: 'critical', value: 'critical' }, + { text: 'off', value: 'off' }, + ], + includeAgents: ['dotnet', 'ruby', 'java'], }, // Recording @@ -207,4 +218,42 @@ export const generalSettings: RawSettingDefinition[] = [ } ), }, + + // Sanitize field names + { + key: 'sanitize_field_names', + type: 'text', + defaultValue: + 'password, passwd, pwd, secret, *key, *token*, *session*, *credit*, *card*, authorization, set-cookie', + label: i18n.translate('xpack.apm.agentConfig.sanitizeFiledNames.label', { + defaultMessage: 'Sanitize field names', + }), + description: i18n.translate( + 'xpack.apm.agentConfig.sanitizeFiledNames.description', + { + defaultMessage: + 'Sometimes it is necessary to sanitize, i.e., remove, sensitive data sent to Elastic APM. This config accepts a list of wildcard patterns of field names which should be sanitized. These apply to HTTP headers (including cookies) and `application/x-www-form-urlencoded` data (POST form fields). The query string and the captured request body (such as `application/json` data) will not get sanitized.', + } + ), + includeAgents: ['java'], + }, + + // Ignore transactions based on URLs + { + key: 'transaction_ignore_urls', + type: 'text', + defaultValue: + 'Agent specific - check out the documentation of this config option in the corresponding agent documentation.', + label: i18n.translate('xpack.apm.agentConfig.transactionIgnoreUrl.label', { + defaultMessage: 'Ignore transactions based on URLs', + }), + description: i18n.translate( + 'xpack.apm.agentConfig.transactionIgnoreUrl.description', + { + defaultMessage: + 'Used to restrict requests to certain URLs from being instrumented. This config accepts a comma-separated list of wildcard patterns of URL paths that should be ignored. When an incoming HTTP request is detected, its request path will be tested against each element in this list. For example, adding `/home/index` to this list would match and remove instrumentation from `http://localhost/home/index` as well as `http://whatever.com/home/index?value1=123`', + } + ), + includeAgents: ['java'], + }, ]; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index 1f247813104ec..a00f1ab5bb4d1 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -61,12 +61,14 @@ describe('filterByAgent', () => { 'capture_headers', 'circuit_breaker_enabled', 'enable_log_correlation', + 'log_level', 'profiling_inferred_spans_enabled', 'profiling_inferred_spans_excluded_classes', 'profiling_inferred_spans_included_classes', 'profiling_inferred_spans_min_duration', 'profiling_inferred_spans_sampling_interval', 'recording', + 'sanitize_field_names', 'server_timeout', 'span_frames_min_duration', 'stack_trace_limit', @@ -75,6 +77,7 @@ describe('filterByAgent', () => { 'stress_monitor_gc_stress_threshold', 'stress_monitor_system_cpu_relief_threshold', 'stress_monitor_system_cpu_stress_threshold', + 'transaction_ignore_urls', 'transaction_max_spans', 'transaction_sample_rate', ]); diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 2a5ef9ad0c2a7..a0e98eebf65cb 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -4,34 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// This is an APM-specific Jest configuration which overrides the x-pack -// configuration. It's intended for use in development and does not run in CI, -// which runs the entire x-pack suite. Run `npx jest`. - -require('../../../src/setup_node_env'); - -const { createJestConfig } = require('../../dev-tools/jest/create_jest_config'); -const { resolve } = require('path'); - -const rootDir = resolve(__dirname, '.'); -const kibanaDirectory = resolve(__dirname, '../../..'); - -const jestConfig = createJestConfig({ kibanaDirectory, rootDir }); - module.exports = { - ...jestConfig, - reporters: ['default'], - roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], - collectCoverage: true, - collectCoverageFrom: [ - ...(jestConfig.collectCoverageFrom || []), - '**/*.{js,mjs,jsx,ts,tsx}', - '!**/*.stories.{js,mjs,ts,tsx}', - '!**/dev_docs/**', - '!**/e2e/**', - '!**/target/**', - '!**/typings/**', - ], - coverageDirectory: `${rootDir}/target/coverage/jest`, - coverageReporters: ['html'], + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/apm'], }; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 3bf4807877428..2806b8e989ee6 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { History, Location } from 'history'; import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; import { px } from '../../../../../../style/variables'; import { Timeline } from '../../../../../shared/charts/Timeline'; @@ -128,7 +127,7 @@ export function Waterfall({ })} /> )} - +
{renderItems(waterfall.childrenByParentId)} - +
{ ], }; - const wrapper = mountWithTheme( - - - - ); + const wrapper = mountWithTheme(); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -84,12 +79,7 @@ describe('Timeline', () => { }, }; - const mountTimeline = () => - mountWithTheme( - - - - ); + const mountTimeline = () => mountWithTheme(); expect(mountTimeline).not.toThrow(); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx index dcdfee22e3cfc..904917f2f9792 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx @@ -6,7 +6,6 @@ import React, { ReactNode } from 'react'; import { inRange } from 'lodash'; -import { Sticky } from 'react-sticky'; import { XAxis, XYPlot } from 'react-vis'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; @@ -54,57 +53,51 @@ export function TimelineAxis({ const topTraceDurationFormatted = tickFormatter(topTraceDuration).formatted; return ( - - {({ style }) => { - return ( -
- - tickFormatter(time).formatted} - tickPadding={20} - style={{ - text: { fill: theme.eui.euiColorDarkShade }, - }} - /> +
+ + tickFormatter(time).formatted} + tickPadding={20} + style={{ + text: { fill: theme.eui.euiColorDarkShade }, + }} + /> - {topTraceDuration > 0 && ( - - )} + {topTraceDuration > 0 && ( + + )} - {marks.map((mark) => ( - - ))} - -
- ); - }} - + {marks.map((mark) => ( + + ))} +
+
); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap index 2756de6e384bc..76e2960e78e9d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap @@ -2,18 +2,11 @@ exports[`Timeline should render with data 1`] = `
-
-
+ } +/> `; diff --git a/x-pack/plugins/audit_trail/jest.config.js b/x-pack/plugins/audit_trail/jest.config.js new file mode 100644 index 0000000000000..31de78fc6bbd9 --- /dev/null +++ b/x-pack/plugins/audit_trail/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/audit_trail'], +}; diff --git a/x-pack/plugins/beats_management/jest.config.js b/x-pack/plugins/beats_management/jest.config.js new file mode 100644 index 0000000000000..8ffbb97b1656d --- /dev/null +++ b/x-pack/plugins/beats_management/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/beats_management'], +}; diff --git a/x-pack/plugins/canvas/jest.config.js b/x-pack/plugins/canvas/jest.config.js new file mode 100644 index 0000000000000..d010fb8c150bc --- /dev/null +++ b/x-pack/plugins/canvas/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/canvas'], +}; diff --git a/x-pack/plugins/canvas/scripts/jest.js b/x-pack/plugins/canvas/scripts/jest.js index a91431a0141c5..9dd8bac88e1e2 100644 --- a/x-pack/plugins/canvas/scripts/jest.js +++ b/x-pack/plugins/canvas/scripts/jest.js @@ -4,89 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -const { run } = require('@kbn/dev-utils'); -const { runXPackScript } = require('./_helpers'); +const { resolve } = require('path'); +process.argv.push('--config', resolve(__dirname, '../jest.config.js')); -// Due to https://github.com/facebook/jest/issues/7267, folders that start with `.` -// are ignored if using watchman. Disabling watchman makes testing slow. So -// we're making this script allow -run( - ({ log, flags }) => { - const { all, storybook, update, coverage } = flags; - let { path } = flags; - let options = []; - process.argv.splice(2, process.argv.length - 2); +const storybookPosition = process.argv.indexOf('--storybook'); +const allPosition = process.argv.indexOf('--all'); - if (path) { - log.info(`Limiting tests to ${path}...`); - path = 'plugins/canvas/' + path; - } else { - path = 'plugins/canvas'; - } +console.log(` +A helper proxying to the following command: - if (coverage) { - log.info(`Collecting test coverage and writing to canvas/coverage...`); - options = [ - '--coverage', - '--collectCoverageFrom', // Ignore TS definition files - `!${path}/**/*.d.ts`, - '--collectCoverageFrom', // Ignore build directories - `!${path}/**/build/**`, - '--collectCoverageFrom', // Ignore coverage on test files - `!${path}/**/__tests__/**/*`, - '--collectCoverageFrom', // Ignore coverage on example files - `!${path}/**/__examples__/**/*`, - '--collectCoverageFrom', // Ignore flot files - `!${path}/**/flot-charts/**`, - '--collectCoverageFrom', // Ignore coverage files - `!${path}/**/coverage/**`, - '--collectCoverageFrom', // Ignore scripts - `!${path}/**/scripts/**`, - '--collectCoverageFrom', // Ignore mock files - `!${path}/**/mocks/**`, - '--collectCoverageFrom', // Include JS files - `${path}/**/*.js`, - '--collectCoverageFrom', // Include TS/X files - `${path}/**/*.ts*`, - '--coverageDirectory', // Output to canvas/coverage - 'plugins/canvas/coverage', - ]; - } - // Mitigation for https://github.com/facebook/jest/issues/7267 - if (all || storybook) { - options = options.concat(['--no-cache', '--no-watchman']); - } + yarn jest --config x-pack/plugins/canvas/jest.config.js - if (all) { - log.info('Running all available tests. This will take a while...'); - } else if (storybook) { - path = 'plugins/canvas/storybook'; - log.info('Running Storybook Snapshot tests...'); - } else { - log.info('Running tests. This does not include Storybook Snapshots...'); - } +Provides the following additional options: + --all Runs all tests and snapshots. Slower. + --storybook Runs Storybook Snapshot tests only. +`); - if (update) { - log.info('Updating any Jest snapshots...'); - options.push('-u'); - } +if (storybookPosition > -1) { + process.argv.splice(storybookPosition, 1); - runXPackScript('jest', [path].concat(options)); - }, - { - description: ` - Jest test runner for Canvas. By default, will not include Storybook Snapshots. - `, - flags: { - boolean: ['all', 'storybook', 'update', 'coverage'], - string: ['path'], - help: ` - --all Runs all tests and snapshots. Slower. - --storybook Runs Storybook Snapshot tests only. - --update Updates Storybook Snapshot tests. - --path Runs any tests at a given path. - --coverage Collect coverage statistics. - `, - }, - } -); + console.log('Running Storybook Snapshot tests only'); + process.argv.push('canvas/storybook/'); +} else if (allPosition > -1) { + process.argv.splice(allPosition, 1); + console.log('Running all available tests. This will take a while...'); +} else { + console.log('Running tests. This does not include Storybook Snapshots...'); + process.argv.push('--modulePathIgnorePatterns="/canvas/storybook/"'); +} + +if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; +} + +require('jest').run(); diff --git a/x-pack/plugins/canvas/storybook/dll_contexts.js b/x-pack/plugins/canvas/storybook/dll_contexts.js index 02ceafd0b3983..8397f2f2e75f6 100644 --- a/x-pack/plugins/canvas/storybook/dll_contexts.js +++ b/x-pack/plugins/canvas/storybook/dll_contexts.js @@ -10,6 +10,3 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths require('../../../../src/core/server/core_app/assets/legacy_light_theme.css'); - -const json = require.context('../shareable_runtime/test/workpads', false, /\.json$/); -json.keys().forEach((key) => json(key)); diff --git a/x-pack/plugins/case/jest.config.js b/x-pack/plugins/case/jest.config.js new file mode 100644 index 0000000000000..8095c70bc4a14 --- /dev/null +++ b/x-pack/plugins/case/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/case'], +}; diff --git a/x-pack/plugins/cloud/jest.config.js b/x-pack/plugins/cloud/jest.config.js new file mode 100644 index 0000000000000..e3844a97e5692 --- /dev/null +++ b/x-pack/plugins/cloud/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/cloud'], +}; diff --git a/x-pack/plugins/code/jest.config.js b/x-pack/plugins/code/jest.config.js new file mode 100644 index 0000000000000..2b2b078cc966c --- /dev/null +++ b/x-pack/plugins/code/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/code'], +}; diff --git a/x-pack/plugins/cross_cluster_replication/jest.config.js b/x-pack/plugins/cross_cluster_replication/jest.config.js new file mode 100644 index 0000000000000..6202a45413906 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/cross_cluster_replication'], +}; diff --git a/x-pack/plugins/dashboard_enhanced/jest.config.js b/x-pack/plugins/dashboard_enhanced/jest.config.js new file mode 100644 index 0000000000000..5aeb423383c41 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/dashboard_enhanced'], +}; diff --git a/x-pack/plugins/dashboard_mode/jest.config.js b/x-pack/plugins/dashboard_mode/jest.config.js new file mode 100644 index 0000000000000..062ad302da7c4 --- /dev/null +++ b/x-pack/plugins/dashboard_mode/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/dashboard_mode'], +}; diff --git a/x-pack/plugins/data_enhanced/jest.config.js b/x-pack/plugins/data_enhanced/jest.config.js new file mode 100644 index 0000000000000..b0b1e2d94b40a --- /dev/null +++ b/x-pack/plugins/data_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/data_enhanced'], +}; diff --git a/x-pack/plugins/discover_enhanced/jest.config.js b/x-pack/plugins/discover_enhanced/jest.config.js new file mode 100644 index 0000000000000..00e040beba411 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/discover_enhanced'], +}; diff --git a/x-pack/plugins/drilldowns/jest.config.js b/x-pack/plugins/drilldowns/jest.config.js new file mode 100644 index 0000000000000..a7d79f8dac378 --- /dev/null +++ b/x-pack/plugins/drilldowns/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/drilldowns'], +}; diff --git a/x-pack/plugins/embeddable_enhanced/jest.config.js b/x-pack/plugins/embeddable_enhanced/jest.config.js new file mode 100644 index 0000000000000..c5c62f98ca2f3 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/embeddable_enhanced'], +}; diff --git a/x-pack/plugins/encrypted_saved_objects/jest.config.js b/x-pack/plugins/encrypted_saved_objects/jest.config.js new file mode 100644 index 0000000000000..0883bdb224dd0 --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/encrypted_saved_objects'], +}; diff --git a/x-pack/plugins/enterprise_search/jest.config.js b/x-pack/plugins/enterprise_search/jest.config.js new file mode 100644 index 0000000000000..db6a25a1f7efd --- /dev/null +++ b/x-pack/plugins/enterprise_search/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/enterprise_search'], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index 2e7595e3ee87b..51896becd8703 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -8,7 +8,7 @@ import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; -import { IndexingStatus } from '../schema/types'; +import { IIndexingStatus } from '../../../shared/types'; import { EngineDetails } from './types'; interface EngineValues { @@ -25,7 +25,7 @@ interface EngineValues { interface EngineActions { setEngineData(engine: EngineDetails): { engine: EngineDetails }; setEngineName(engineName: string): { engineName: string }; - setIndexingStatus(activeReindexJob: IndexingStatus): { activeReindexJob: IndexingStatus }; + setIndexingStatus(activeReindexJob: IIndexingStatus): { activeReindexJob: IIndexingStatus }; setEngineNotFound(notFound: boolean): { notFound: boolean }; clearEngine(): void; initializeEngine(): void; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index 635d1136291aa..99ad19fea0619 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -5,7 +5,7 @@ */ import { ApiToken } from '../credentials/types'; -import { Schema, SchemaConflicts, IndexingStatus } from '../schema/types'; +import { Schema, SchemaConflicts, IIndexingStatus } from '../../../shared/types'; export interface Engine { name: string; @@ -26,7 +26,7 @@ export interface EngineDetails extends Engine { schema: Schema; schemaConflicts?: SchemaConflicts; unconfirmedFields?: string[]; - activeReindexJob?: IndexingStatus; + activeReindexJob?: IIndexingStatus; invalidBoosts: boolean; sample?: boolean; isMeta: boolean; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts deleted file mode 100644 index 84f402dd3b95f..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; - -export interface Schema { - [key: string]: SchemaTypes; -} - -// this is a mapping of schema field types ("string", "number", "geolocation", "date") to the names -// of source engines which utilize that type -export type SchemaConflictFieldTypes = { - [key in SchemaTypes]: string[]; -}; - -export interface SchemaConflict { - fieldTypes: SchemaConflictFieldTypes; - resolution?: string; -} - -// For now these values are ISchemaConflictFieldTypes, but in the near future will be ISchemaConflict -// once we implement schema conflict resolution -export interface SchemaConflicts { - [key: string]: SchemaConflictFieldTypes; -} - -export interface IndexingStatus { - percentageComplete: number; - numDocumentsWithErrors: number; - activeReindexJobId: number; -} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx index 097c3bbc8e9ff..42cb6c229ad63 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../__mocks__/kea.mock'; +import '../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions, setMockValues } from '../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; @@ -11,41 +16,49 @@ import { EuiPanel } from '@elastic/eui'; import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; -import { IndexingStatusFetcher } from './indexing_status_fetcher'; import { IndexingStatus } from './indexing_status'; describe('IndexingStatus', () => { const getItemDetailPath = jest.fn(); - const getStatusPath = jest.fn(); const onComplete = jest.fn(); const setGlobalIndexingStatus = jest.fn(); + const fetchIndexingStatus = jest.fn(); const props = { percentageComplete: 50, numDocumentsWithErrors: 1, activeReindexJobId: 12, viewLinkPath: '/path', + statusPath: '/other_path', itemId: '1', getItemDetailPath, - getStatusPath, onComplete, setGlobalIndexingStatus, }; + beforeEach(() => { + setMockActions({ fetchIndexingStatus }); + }); + it('renders', () => { + setMockValues({ + percentageComplete: 50, + numDocumentsWithErrors: 0, + }); const wrapper = shallow(); - const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')( - props.percentageComplete, - props.numDocumentsWithErrors - ); - expect(shallow(fetcher).find(EuiPanel)).toHaveLength(1); - expect(shallow(fetcher).find(IndexingStatusContent)).toHaveLength(1); + expect(wrapper.find(EuiPanel)).toHaveLength(1); + expect(wrapper.find(IndexingStatusContent)).toHaveLength(1); + expect(fetchIndexingStatus).toHaveBeenCalled(); }); it('renders errors', () => { - const wrapper = shallow(); - const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')(100, 1); - expect(shallow(fetcher).find(IndexingStatusErrors)).toHaveLength(1); + setMockValues({ + percentageComplete: 100, + numDocumentsWithErrors: 1, + }); + const wrapper = shallow(); + + expect(wrapper.find(IndexingStatusErrors)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx index beec0babea590..b2109b7ef3f0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -4,41 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; + +import { useValues, useActions } from 'kea'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; -import { IndexingStatusFetcher } from './indexing_status_fetcher'; +import { IndexingStatusLogic } from './indexing_status_logic'; import { IIndexingStatus } from '../types'; -export interface IIndexingStatusProps extends IIndexingStatus { +export interface IIndexingStatusProps { viewLinkPath: string; itemId: string; + statusPath: string; getItemDetailPath?(itemId: string): string; - getStatusPath(itemId: string, activeReindexJobId: number): string; onComplete(numDocumentsWithErrors: number): void; setGlobalIndexingStatus?(activeReindexJob: IIndexingStatus): void; } -export const IndexingStatus: React.FC = (props) => ( - - {(percentageComplete, numDocumentsWithErrors) => ( -
- {percentageComplete < 100 && ( - - - - )} - {percentageComplete === 100 && numDocumentsWithErrors > 0 && ( - <> - - - - )} -
- )} -
-); +export const IndexingStatus: React.FC = ({ + viewLinkPath, + statusPath, + onComplete, +}) => { + const { percentageComplete, numDocumentsWithErrors } = useValues(IndexingStatusLogic); + const { fetchIndexingStatus } = useActions(IndexingStatusLogic); + + useEffect(() => { + fetchIndexingStatus({ statusPath, onComplete }); + }, []); + + return ( + <> + {percentageComplete < 100 && ( + + + + )} + {percentageComplete === 100 && numDocumentsWithErrors > 0 && ( + <> + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx index fc706aee659a5..563702a143ab3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCallOut, EuiButton } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; +import { EuiButtonTo } from '../react_router_helpers'; import { IndexingStatusErrors } from './indexing_status_errors'; @@ -17,9 +17,8 @@ describe('IndexingStatusErrors', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiButton)).toHaveLength(1); expect(wrapper.find(EuiCallOut)).toHaveLength(1); - expect(wrapper.find(EuiLinkTo)).toHaveLength(1); - expect(wrapper.find(EuiLinkTo).prop('to')).toEqual('/path'); + expect(wrapper.find(EuiButtonTo)).toHaveLength(1); + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual('/path'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx index a928400b2338c..2be27299fd77f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; +import { EuiButtonTo } from '../react_router_helpers'; import { INDEXING_STATUS_HAS_ERRORS_TITLE, INDEXING_STATUS_HAS_ERRORS_BUTTON } from './constants'; @@ -24,8 +24,9 @@ export const IndexingStatusErrors: React.FC = ({ vie data-test-subj="IndexingStatusErrors" >

{INDEXING_STATUS_HAS_ERRORS_TITLE}

- - {INDEXING_STATUS_HAS_ERRORS_BUTTON} - + + + {INDEXING_STATUS_HAS_ERRORS_BUTTON} + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx deleted file mode 100644 index cb7c82f91ed61..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect, useState, useRef } from 'react'; - -import { HttpLogic } from '../http'; -import { flashAPIErrors } from '../flash_messages'; - -interface IIndexingStatusFetcherProps { - activeReindexJobId: number; - itemId: string; - percentageComplete: number; - numDocumentsWithErrors: number; - onComplete?(numDocumentsWithErrors: number): void; - getStatusPath(itemId: string, activeReindexJobId: number): string; - children(percentageComplete: number, numDocumentsWithErrors: number): JSX.Element; -} - -export const IndexingStatusFetcher: React.FC = ({ - activeReindexJobId, - children, - getStatusPath, - itemId, - numDocumentsWithErrors, - onComplete, - percentageComplete = 0, -}) => { - const [indexingStatus, setIndexingStatus] = useState({ - numDocumentsWithErrors, - percentageComplete, - }); - const pollingInterval = useRef(); - - useEffect(() => { - pollingInterval.current = window.setInterval(async () => { - try { - const response = await HttpLogic.values.http.get(getStatusPath(itemId, activeReindexJobId)); - if (response.percentageComplete >= 100) { - clearInterval(pollingInterval.current); - } - setIndexingStatus({ - percentageComplete: response.percentageComplete, - numDocumentsWithErrors: response.numDocumentsWithErrors, - }); - if (response.percentageComplete >= 100 && onComplete) { - onComplete(response.numDocumentsWithErrors); - } - } catch (e) { - flashAPIErrors(e); - } - }, 3000); - - return () => { - if (pollingInterval.current) { - clearInterval(pollingInterval.current); - } - }; - }, []); - - return children(indexingStatus.percentageComplete, indexingStatus.numDocumentsWithErrors); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts new file mode 100644 index 0000000000000..9fa5fe0f84bab --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +jest.mock('../http', () => ({ + HttpLogic: { + values: { http: { get: jest.fn() } }, + }, +})); +import { HttpLogic } from '../http'; + +jest.mock('../flash_messages', () => ({ + flashAPIErrors: jest.fn(), +})); +import { flashAPIErrors } from '../flash_messages'; + +import { IndexingStatusLogic } from './indexing_status_logic'; + +describe('IndexingStatusLogic', () => { + let unmount: any; + + const mockStatusResponse = { + percentageComplete: 50, + numDocumentsWithErrors: 3, + activeReindexJobId: 1, + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + unmount = IndexingStatusLogic.mount(); + }); + + it('has expected default values', () => { + expect(IndexingStatusLogic.values).toEqual({ + percentageComplete: 100, + numDocumentsWithErrors: 0, + }); + }); + + describe('setIndexingStatus', () => { + it('sets reducers', () => { + IndexingStatusLogic.actions.setIndexingStatus(mockStatusResponse); + + expect(IndexingStatusLogic.values.percentageComplete).toEqual( + mockStatusResponse.percentageComplete + ); + expect(IndexingStatusLogic.values.numDocumentsWithErrors).toEqual( + mockStatusResponse.numDocumentsWithErrors + ); + }); + }); + + describe('fetchIndexingStatus', () => { + jest.useFakeTimers(); + const statusPath = '/api/workplace_search/path/123'; + const onComplete = jest.fn(); + const TIMEOUT = 3000; + + it('calls API and sets values', async () => { + const setIndexingStatusSpy = jest.spyOn(IndexingStatusLogic.actions, 'setIndexingStatus'); + const promise = Promise.resolve(mockStatusResponse); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); + jest.advanceTimersByTime(TIMEOUT); + + expect(HttpLogic.values.http.get).toHaveBeenCalledWith(statusPath); + await promise; + + expect(setIndexingStatusSpy).toHaveBeenCalledWith(mockStatusResponse); + }); + + it('handles error', async () => { + const promise = Promise.reject('An error occured'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); + jest.advanceTimersByTime(TIMEOUT); + + try { + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + }); + + it('handles indexing complete state', async () => { + const promise = Promise.resolve({ ...mockStatusResponse, percentageComplete: 100 }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); + jest.advanceTimersByTime(TIMEOUT); + + await promise; + + expect(clearInterval).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalledWith(mockStatusResponse.numDocumentsWithErrors); + }); + + it('handles unmounting', async () => { + unmount(); + expect(clearInterval).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts new file mode 100644 index 0000000000000..cb484f19c157f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { HttpLogic } from '../http'; +import { IIndexingStatus } from '../types'; +import { flashAPIErrors } from '../flash_messages'; + +interface IndexingStatusProps { + statusPath: string; + onComplete(numDocumentsWithErrors: number): void; +} + +interface IndexingStatusActions { + fetchIndexingStatus(props: IndexingStatusProps): IndexingStatusProps; + setIndexingStatus({ + percentageComplete, + numDocumentsWithErrors, + }: IIndexingStatus): IIndexingStatus; +} + +interface IndexingStatusValues { + percentageComplete: number; + numDocumentsWithErrors: number; +} + +let pollingInterval: number; + +export const IndexingStatusLogic = kea>({ + actions: { + fetchIndexingStatus: ({ statusPath, onComplete }) => ({ statusPath, onComplete }), + setIndexingStatus: ({ numDocumentsWithErrors, percentageComplete }) => ({ + numDocumentsWithErrors, + percentageComplete, + }), + }, + reducers: { + percentageComplete: [ + 100, + { + setIndexingStatus: (_, { percentageComplete }) => percentageComplete, + }, + ], + numDocumentsWithErrors: [ + 0, + { + setIndexingStatus: (_, { numDocumentsWithErrors }) => numDocumentsWithErrors, + }, + ], + }, + listeners: ({ actions }) => ({ + fetchIndexingStatus: ({ statusPath, onComplete }: IndexingStatusProps) => { + const { http } = HttpLogic.values; + + pollingInterval = window.setInterval(async () => { + try { + const response: IIndexingStatus = await http.get(statusPath); + if (response.percentageComplete >= 100) { + clearInterval(pollingInterval); + onComplete(response.numDocumentsWithErrors); + } + actions.setIndexingStatus(response); + } catch (e) { + flashAPIErrors(e); + } + }, 3000); + }, + }), + events: () => ({ + beforeUnmount() { + clearInterval(pollingInterval); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 3866d1a7199e4..38a6187d290b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -4,6 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; + +export interface Schema { + [key: string]: SchemaTypes; +} + +// this is a mapping of schema field types ("string", "number", "geolocation", "date") to the names +// of source engines which utilize that type +export type SchemaConflictFieldTypes = { + [key in SchemaTypes]: string[]; +}; + +export interface SchemaConflict { + fieldTypes: SchemaConflictFieldTypes; + resolution?: string; +} + +// For now these values are ISchemaConflictFieldTypes, but in the near future will be ISchemaConflict +// once we implement schema conflict resolution +export interface SchemaConflicts { + [key: string]: SchemaConflictFieldTypes; +} + export interface IIndexingStatus { percentageComplete: number; numDocumentsWithErrors: number; diff --git a/x-pack/plugins/event_log/jest.config.js b/x-pack/plugins/event_log/jest.config.js new file mode 100644 index 0000000000000..bb847d3b3c7ce --- /dev/null +++ b/x-pack/plugins/event_log/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/event_log'], +}; diff --git a/x-pack/plugins/features/jest.config.js b/x-pack/plugins/features/jest.config.js new file mode 100644 index 0000000000000..e500d35bbbd60 --- /dev/null +++ b/x-pack/plugins/features/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/features'], +}; diff --git a/x-pack/plugins/file_upload/jest.config.js b/x-pack/plugins/file_upload/jest.config.js new file mode 100644 index 0000000000000..6a042a4cc5c1e --- /dev/null +++ b/x-pack/plugins/file_upload/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/file_upload'], +}; diff --git a/x-pack/plugins/fleet/jest.config.js b/x-pack/plugins/fleet/jest.config.js new file mode 100644 index 0000000000000..521cb7467b196 --- /dev/null +++ b/x-pack/plugins/fleet/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/fleet'], +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 9f687b39c094e..f6533a06cea27 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -143,6 +143,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ description: e.target.value, }) } + data-test-subj="packagePolicyDescriptionInput" /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 425d1435a716e..8f798445b2362 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -244,6 +244,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { packagePolicyName: packagePolicy.name, }, }), + 'data-test-subj': 'policyUpdateSuccessToast', text: agentCount && agentPolicy ? i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationMessage', { @@ -406,6 +407,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { iconType="save" color="primary" fill + data-test-subj="saveIntegration" > ; }) => void; } diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index aa6160bbd8914..05060c9d863aa 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; +import mime from 'mime-types'; +import path from 'path'; import { RequestHandler, ResponseHeaders, KnownHeaders } from 'src/core/server'; import { GetInfoResponse, @@ -43,6 +45,8 @@ import { import { defaultIngestErrorHandler, ingestErrorToResponseOptions } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; import { licenseService } from '../../services'; +import { getArchiveEntry } from '../../services/epm/archive/cache'; +import { bufferToStream } from '../../services/epm/streams'; export const getCategoriesHandler: RequestHandler< undefined, @@ -102,22 +106,51 @@ export const getFileHandler: RequestHandler { try { const { pkgName, pkgVersion, filePath } = request.params; - const registryResponse = await getFile(`/package/${pkgName}/${pkgVersion}/${filePath}`); - - const headersToProxy: KnownHeaders[] = ['content-type', 'cache-control']; - const proxiedHeaders = headersToProxy.reduce((headers, knownHeader) => { - const value = registryResponse.headers.get(knownHeader); - if (value !== null) { - headers[knownHeader] = value; + const savedObjectsClient = context.core.savedObjects.client; + const savedObject = await getInstallationObject({ savedObjectsClient, pkgName }); + const pkgInstallSource = savedObject?.attributes.install_source; + // TODO: when package storage is available, remove installSource check and check cache and storage, remove registry call + if (pkgInstallSource === 'upload' && pkgVersion === savedObject?.attributes.version) { + const headerContentType = mime.contentType(path.extname(filePath)); + if (!headerContentType) { + return response.custom({ + body: `unknown content type for file: ${filePath}`, + statusCode: 400, + }); } - return headers; - }, {} as ResponseHeaders); + const archiveFile = getArchiveEntry(`${pkgName}-${pkgVersion}/${filePath}`); + if (!archiveFile) { + return response.custom({ + body: `uploaded package file not found: ${filePath}`, + statusCode: 404, + }); + } + const headers: ResponseHeaders = { + 'cache-control': 'max-age=10, public', + 'content-type': headerContentType, + }; + return response.custom({ + body: bufferToStream(archiveFile), + statusCode: 200, + headers, + }); + } else { + const registryResponse = await getFile(`/package/${pkgName}/${pkgVersion}/${filePath}`); + const headersToProxy: KnownHeaders[] = ['content-type', 'cache-control']; + const proxiedHeaders = headersToProxy.reduce((headers, knownHeader) => { + const value = registryResponse.headers.get(knownHeader); + if (value !== null) { + headers[knownHeader] = value; + } + return headers; + }, {} as ResponseHeaders); - return response.custom({ - body: registryResponse.body, - statusCode: registryResponse.status, - headers: proxiedHeaders, - }); + return response.custom({ + body: registryResponse.body, + statusCode: registryResponse.status, + headers: proxiedHeaders, + }); + } } catch (error) { return defaultIngestErrorHandler({ error, response }); } diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index dc7a91e08799c..b93a9119cd4df 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -5,7 +5,7 @@ */ import yaml from 'js-yaml'; -import { uniq } from 'lodash'; +import { pick, uniq } from 'lodash'; import { ArchivePackage, RegistryPolicyTemplate, @@ -21,6 +21,42 @@ import { pkgToPkgKey } from '../registry'; const MANIFESTS: Record = {}; const MANIFEST_NAME = 'manifest.yml'; +// not sure these are 100% correct but they do the job here +// keeping them local until others need them +type OptionalPropertyOf = Exclude< + { + [K in keyof T]: T extends Record ? never : K; + }[keyof T], + undefined +>; +type RequiredPropertyOf = Exclude>; + +type RequiredPackageProp = RequiredPropertyOf; +type OptionalPackageProp = OptionalPropertyOf; +// pro: guarantee only supplying known values. these keys must be in ArchivePackage. no typos or new values +// pro: any values added to these lists will be passed through by default +// pro & con: values do need to be shadowed / repeated from ArchivePackage, but perhaps we want to limit values +const requiredArchivePackageProps: readonly RequiredPackageProp[] = [ + 'name', + 'version', + 'description', + 'type', + 'categories', + 'format_version', +] as const; + +const optionalArchivePackageProps: readonly OptionalPackageProp[] = [ + 'title', + 'release', + 'readme', + 'screenshots', + 'icons', + 'assets', + 'internal', + 'data_streams', + 'policy_templates', +] as const; + // TODO: everything below performs verification of manifest.yml files, and hence duplicates functionality already implemented in the // package registry. At some point this should probably be replaced (or enhanced) with verification based on // https://github.com/elastic/package-spec/ @@ -58,43 +94,43 @@ function parseAndVerifyArchive(paths: string[]): ArchivePackage { } // ... which must be valid YAML - let manifest; + let manifest: ArchivePackage; try { manifest = yaml.load(manifestBuffer.toString()); } catch (error) { throw new PackageInvalidArchiveError(`Could not parse top-level package manifest: ${error}.`); } - // Package name and version from the manifest must match those from the toplevel directory - const pkgKey = pkgToPkgKey({ name: manifest.name, version: manifest.version }); - if (toplevelDir !== pkgKey) { + // must have mandatory fields + const reqGiven = pick(manifest, requiredArchivePackageProps); + const requiredKeysMatch = + Object.keys(reqGiven).toString() === requiredArchivePackageProps.toString(); + if (!requiredKeysMatch) { + const list = requiredArchivePackageProps.join(', '); throw new PackageInvalidArchiveError( - `Name ${manifest.name} and version ${manifest.version} do not match top-level directory ${toplevelDir}` + `Invalid top-level package manifest: one or more fields missing of ${list}` ); } - const { name, version, description, type, categories, format_version: formatVersion } = manifest; - // check for mandatory fields - if (!(name && version && description && type && categories && formatVersion)) { + // at least have all required properties + // get optional values and combine into one object for the remaining operations + const optGiven = pick(manifest, optionalArchivePackageProps); + const parsed: ArchivePackage = { ...reqGiven, ...optGiven }; + + // Package name and version from the manifest must match those from the toplevel directory + const pkgKey = pkgToPkgKey({ name: parsed.name, version: parsed.version }); + if (toplevelDir !== pkgKey) { throw new PackageInvalidArchiveError( - 'Invalid top-level package manifest: one or more fields missing of name, version, description, type, categories, format_version' + `Name ${parsed.name} and version ${parsed.version} do not match top-level directory ${toplevelDir}` ); } - const dataStreams = parseAndVerifyDataStreams(paths, name, version); - const policyTemplates = parseAndVerifyPolicyTemplates(manifest); + parsed.data_streams = parseAndVerifyDataStreams(paths, parsed.name, parsed.version); + parsed.policy_templates = parseAndVerifyPolicyTemplates(manifest); - return { - name, - version, - description, - type, - categories, - format_version: formatVersion, - data_streams: dataStreams, - policy_templates: policyTemplates, - }; + return parsed; } + function parseAndVerifyDataStreams( paths: string[], pkgName: string, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index c10b26cbf0bd1..9b4b26d6fb8b3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -98,7 +98,10 @@ export async function getPackageInfo(options: { const getPackageRes = await getPackageFromSource({ pkgName, pkgVersion, - pkgInstallSource: savedObject?.attributes.install_source, + pkgInstallSource: + savedObject?.attributes.version === pkgVersion + ? savedObject?.attributes.install_source + : 'registry', }); const paths = getPackageRes.paths; const packageInfo = getPackageRes.packageInfo; diff --git a/x-pack/plugins/global_search/jest.config.js b/x-pack/plugins/global_search/jest.config.js new file mode 100644 index 0000000000000..2ad904d8c57c4 --- /dev/null +++ b/x-pack/plugins/global_search/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/global_search'], +}; diff --git a/x-pack/plugins/global_search_bar/jest.config.js b/x-pack/plugins/global_search_bar/jest.config.js new file mode 100644 index 0000000000000..5b03d4a3f90d7 --- /dev/null +++ b/x-pack/plugins/global_search_bar/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/global_search_bar'], +}; diff --git a/x-pack/plugins/global_search_providers/jest.config.js b/x-pack/plugins/global_search_providers/jest.config.js new file mode 100644 index 0000000000000..3bd70206c936c --- /dev/null +++ b/x-pack/plugins/global_search_providers/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/global_search_providers'], +}; diff --git a/x-pack/plugins/graph/jest.config.js b/x-pack/plugins/graph/jest.config.js new file mode 100644 index 0000000000000..da729b0fae223 --- /dev/null +++ b/x-pack/plugins/graph/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/graph'], +}; diff --git a/x-pack/plugins/index_lifecycle_management/jest.config.js b/x-pack/plugins/index_lifecycle_management/jest.config.js new file mode 100644 index 0000000000000..906f4ff3960ae --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/index_lifecycle_management'], +}; diff --git a/x-pack/plugins/index_management/jest.config.js b/x-pack/plugins/index_management/jest.config.js new file mode 100644 index 0000000000000..d389a91675210 --- /dev/null +++ b/x-pack/plugins/index_management/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/index_management'], +}; diff --git a/x-pack/plugins/infra/jest.config.js b/x-pack/plugins/infra/jest.config.js new file mode 100644 index 0000000000000..507f94a2d6685 --- /dev/null +++ b/x-pack/plugins/infra/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/infra'], +}; diff --git a/x-pack/plugins/ingest_manager/jest.config.js b/x-pack/plugins/ingest_manager/jest.config.js new file mode 100644 index 0000000000000..8aff85670176b --- /dev/null +++ b/x-pack/plugins/ingest_manager/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/ingest_manager'], +}; diff --git a/x-pack/plugins/ingest_pipelines/jest.config.js b/x-pack/plugins/ingest_pipelines/jest.config.js new file mode 100644 index 0000000000000..48ce7dea0b5ba --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/ingest_pipelines'], +}; diff --git a/x-pack/plugins/lens/jest.config.js b/x-pack/plugins/lens/jest.config.js new file mode 100644 index 0000000000000..bcb80519c5ef5 --- /dev/null +++ b/x-pack/plugins/lens/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/lens'], +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 67c6068dd4d91..cc456e843bb68 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -332,7 +332,7 @@ export function LayerPanel( > setActiveDimension(initialActiveDimensionState)} + handleClose={() => { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + props.updateDatasource(datasourceId, newState); + } + setActiveDimension(initialActiveDimensionState); + }} panel={ <> {activeGroup && activeId && ( { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + if (!children) { + return null; + } + + const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen); + const closePopover = () => setIsPopoverOpen(false); + const warningsCount = React.Children.count(children); + return ( + + {i18n.translate('xpack.lens.chartWarnings.number', { + defaultMessage: `{warningsCount} {warningsCount, plural, one {warning} other {warnings}}`, + values: { + warningsCount, + }, + })} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + > +
    + {React.Children.map(children, (child, index) => ( +
  • + {child} +
  • + ))} +
+
+ ); +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index d9fbaa22a0388..046bebb33a57d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -19,6 +19,7 @@ import { Datasource, FramePublicAPI, Visualization } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; import { Action } from '../state_management'; import { ChartSwitch } from './chart_switch'; +import { WarningsPopover } from './warnings_popover'; export interface WorkspacePanelWrapperProps { children: React.ReactNode | React.ReactNode[]; @@ -64,40 +65,59 @@ export function WorkspacePanelWrapper({ }, [dispatch, activeVisualization] ); + const warningMessages = + activeVisualization?.getWarningMessages && + activeVisualization.getWarningMessages(visualizationState, framePublicAPI); return ( <>
- + + + + + {activeVisualization && activeVisualization.renderToolbar && ( + + + + )} + + + + {warningMessages && warningMessages.length ? ( + {warningMessages} + ) : null} - {activeVisualization && activeVisualization.renderToolbar && ( - - - - )}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 76276f8b4c828..56d471be63d3e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -44,6 +44,7 @@ import { IndexPatternsContract } from '../../../../../../src/plugins/data/public import { getEditPath, DOC_TYPE } from '../../../common'; import { IBasePath } from '../../../../../../src/core/public'; import { LensAttributeService } from '../../lens_attribute_service'; +import { LensInspectorAdapters } from '../types'; export type LensSavedObjectAttributes = Omit; @@ -84,6 +85,7 @@ export class Embeddable private subscription: Subscription; private autoRefreshFetchSubscription: Subscription; private isInitialized = false; + private activeData: LensInspectorAdapters | undefined; private externalSearchContext: { timeRange?: TimeRange; @@ -131,6 +133,10 @@ export class Embeddable } } + public getInspectorAdapters() { + return this.activeData; + } + async initializeSavedVis(input: LensEmbeddableInput) { const attributes: | LensSavedObjectAttributes @@ -175,6 +181,13 @@ export class Embeddable } } + private updateActiveData = ( + data: unknown, + inspectorAdapters?: LensInspectorAdapters | undefined + ) => { + this.activeData = inspectorAdapters; + }; + /** * * @param {HTMLElement} domNode @@ -194,6 +207,7 @@ export class Embeddable variables={input.palette ? { theme: { palette: input.palette } } : {}} searchSessionId={this.input.searchSessionId} handleEvent={this.handleEvent} + onData$={this.updateActiveData} renderMode={input.renderMode} />, domNode diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index d18372246b0e6..4645420898314 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -15,6 +15,7 @@ import { import { ExecutionContextSearch } from 'src/plugins/data/public'; import { RenderMode } from 'src/plugins/expressions'; import { getOriginalRequestErrorMessage } from '../error_helper'; +import { LensInspectorAdapters } from '../types'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; @@ -23,6 +24,7 @@ export interface ExpressionWrapperProps { searchContext: ExecutionContextSearch; searchSessionId?: string; handleEvent: (event: ExpressionRendererEvent) => void; + onData$: (data: unknown, inspectorAdapters?: LensInspectorAdapters | undefined) => void; renderMode?: RenderMode; } @@ -33,6 +35,7 @@ export function ExpressionWrapper({ variables, handleEvent, searchSessionId, + onData$, renderMode, }: ExpressionWrapperProps) { return ( @@ -60,6 +63,7 @@ export function ExpressionWrapper({ expression={expression} searchContext={searchContext} searchSessionId={searchSessionId} + onData$={onData$} renderMode={renderMode} renderError={(errorMessage, error) => (
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss index 096047da681b9..6bd6808f17b35 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss @@ -15,6 +15,10 @@ column-gap: $euiSizeXL; } +.lnsIndexPatternDimensionEditor__operation .euiListGroupItem__label { + width: 100%; +} + .lnsIndexPatternDimensionEditor__operation > button { padding-top: 0; padding-bottom: 0; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 62de601bb7888..576825e9c960a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -15,10 +15,11 @@ import { EuiSpacer, EuiListGroupItemProps, EuiFormLabel, + EuiToolTip, } from '@elastic/eui'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { OperationSupportMatrix } from './operation_support'; -import { IndexPatternColumn, OperationType } from '../indexpattern'; +import { IndexPatternColumn } from '../indexpattern'; import { operationDefinitionMap, getOperationDisplay, @@ -26,6 +27,8 @@ import { replaceColumn, deleteColumn, updateColumnParam, + resetIncomplete, + FieldBasedIndexPatternColumn, } from '../operations'; import { mergeLayer } from '../state_helpers'; import { FieldSelect } from './field_select'; @@ -106,14 +109,14 @@ export function DimensionEditor(props: DimensionEditorProps) { hideGrouping, } = props; const { fieldByOperation, operationWithoutField } = operationSupportMatrix; - const [ - incompatibleSelectedOperationType, - setInvalidOperationType, - ] = useState(null); const selectedOperationDefinition = selectedColumn && operationDefinitionMap[selectedColumn.operationType]; + const incompleteInfo = (state.layers[layerId].incompleteColumns ?? {})[columnId]; + const incompleteOperation = incompleteInfo?.operationType; + const incompleteField = incompleteInfo?.sourceField ?? null; + const ParamEditor = selectedOperationDefinition?.paramEditor; const possibleOperations = useMemo(() => { @@ -138,26 +141,23 @@ export function DimensionEditor(props: DimensionEditorProps) { hasField(selectedColumn) && definition.input === 'field' && fieldByOperation[operationType]?.has(selectedColumn.sourceField)) || - (selectedColumn && !hasField(selectedColumn) && definition.input !== 'field'), + (selectedColumn && !hasField(selectedColumn) && definition.input === 'none'), + disabledStatus: + definition.getDisabledStatus && + definition.getDisabledStatus(state.indexPatterns[state.currentIndexPatternId]), }; }); - const selectedColumnSourceField = - selectedColumn && 'sourceField' in selectedColumn ? selectedColumn.sourceField : undefined; - - const currentFieldIsInvalid = useMemo( - () => - fieldIsInvalid(selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern), - [selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern] - ); + const currentFieldIsInvalid = useMemo(() => fieldIsInvalid(selectedColumn, currentIndexPattern), [ + selectedColumn, + currentIndexPattern, + ]); const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map( - ({ operationType, compatibleWithCurrentField }) => { + ({ operationType, compatibleWithCurrentField, disabledStatus }) => { const isActive = Boolean( - incompatibleSelectedOperationType === operationType || - (!incompatibleSelectedOperationType && - selectedColumn && - selectedColumn.operationType === operationType) + incompleteOperation === operationType || + (!incompleteOperation && selectedColumn && selectedColumn.operationType === operationType) ); let color: EuiListGroupItemProps['color'] = 'primary'; @@ -168,7 +168,13 @@ export function DimensionEditor(props: DimensionEditorProps) { } let label: EuiListGroupItemProps['label'] = operationPanels[operationType].displayName; - if (isActive) { + if (disabledStatus) { + label = ( + + {operationPanels[operationType].displayName} + + ); + } else if (isActive) { label = {operationPanels[operationType].displayName}; } @@ -178,15 +184,24 @@ export function DimensionEditor(props: DimensionEditorProps) { color, isActive, size: 's', + isDisabled: !!disabledStatus, className: 'lnsIndexPatternDimensionEditor__operation', 'data-test-subj': `lns-indexPatternDimension-${operationType}${ compatibleWithCurrentField ? '' : ' incompatible' }`, onClick() { if (operationDefinitionMap[operationType].input === 'none') { - // Clear invalid state because we are creating a valid column - setInvalidOperationType(null); if (selectedColumn?.operationType === operationType) { + // Clear invalid state because we are reseting to a valid column + if (incompleteInfo) { + setState( + mergeLayer({ + state, + layerId, + newLayer: resetIncomplete(state.layers[layerId], columnId), + }) + ); + } return; } const newLayer = insertOrReplaceColumn({ @@ -216,15 +231,34 @@ export function DimensionEditor(props: DimensionEditorProps) { }) ); } else { - setInvalidOperationType(operationType); + setState( + mergeLayer({ + state, + layerId, + newLayer: insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: operationType, + field: undefined, + }), + }) + ); } trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } - setInvalidOperationType(null); - if (selectedColumn.operationType === operationType) { + if (incompleteInfo) { + setState( + mergeLayer({ + state, + layerId, + newLayer: resetIncomplete(state.layers[layerId], columnId), + }) + ); + } return; } @@ -237,7 +271,6 @@ export function DimensionEditor(props: DimensionEditorProps) { ? currentIndexPattern.getFieldByName(selectedColumn.sourceField) : undefined, }); - setState(mergeLayer({ state, layerId, newLayer })); }, }; @@ -268,18 +301,17 @@ export function DimensionEditor(props: DimensionEditorProps) {
{!selectedColumn || selectedOperationDefinition?.input === 'field' || - (incompatibleSelectedOperationType && - operationDefinitionMap[incompatibleSelectedOperationType].input === 'field') ? ( + (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field') ? ( { setState( mergeLayer({ @@ -304,53 +342,25 @@ export function DimensionEditor(props: DimensionEditorProps) { ); }} onChoose={(choice) => { - let newLayer: IndexPatternLayer; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // Replaces just the field - newLayer = replaceColumn({ - layer: state.layers[layerId], - columnId, - indexPattern: currentIndexPattern, - op: choice.operationType, - field: currentIndexPattern.getFieldByName(choice.field)!, - }); - } else { - // Finds a new operation - const compatibleOperations = - ('field' in choice && operationSupportMatrix.operationByField[choice.field]) || - new Set(); - let operation; - if (compatibleOperations.size > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.has(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations.values().next().value; - } else if ('field' in choice) { - operation = choice.operationType; - } - newLayer = insertOrReplaceColumn({ - layer: state.layers[layerId], - columnId, - field: currentIndexPattern.getFieldByName(choice.field), - indexPattern: currentIndexPattern, - op: operation as OperationType, - }); - } - - setState(mergeLayer({ state, layerId, newLayer })); - setInvalidOperationType(null); + setState( + mergeLayer({ + state, + layerId, + newLayer: insertOrReplaceColumn({ + layer: state.layers[layerId], + columnId, + indexPattern: currentIndexPattern, + op: choice.operationType, + field: currentIndexPattern.getFieldByName(choice.field), + }), + }) + ); }} /> ) : null} - {!currentFieldIsInvalid && !incompatibleSelectedOperationType && selectedColumn && ( + {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ( )} - {!currentFieldIsInvalid && - !incompatibleSelectedOperationType && - selectedColumn && - ParamEditor && ( - <> - - - )} + {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ParamEditor && ( + <> + + + )}
{!currentFieldIsInvalid && (
- {!incompatibleSelectedOperationType && selectedColumn && ( + {!incompleteInfo && selectedColumn && ( { @@ -411,7 +418,7 @@ export function DimensionEditor(props: DimensionEditorProps) { /> )} - {!incompatibleSelectedOperationType && !hideGrouping && ( + {!incompleteInfo && !hideGrouping && ( { columns: { col1: { label: 'Date histogram of timestamp', + customLabel: true, dataType: 'date', isBucketed: true, @@ -153,11 +154,16 @@ describe('IndexPatternDimensionEditorPanel', () => { sourceField: 'timestamp', }, }, + incompleteColumns: {}, }, }, }; - setState = jest.fn(); + setState = jest.fn().mockImplementation((newState) => { + if (wrapper instanceof ReactWrapper) { + wrapper.setProps({ state: newState }); + } + }); defaultProps = { state, @@ -544,7 +550,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }); describe('transient invalid state', () => { - it('should not set the state if selecting an operation incompatible with the current field', () => { + it('should set the state if selecting an operation incompatible with the current field', () => { wrapper = mount(); act(() => { @@ -553,7 +559,20 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); }); - expect(setState).not.toHaveBeenCalled(); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, + }, + }, + }); }); it('should show error message in invalid state', () => { @@ -566,8 +585,6 @@ describe('IndexPatternDimensionEditorPanel', () => { expect( wrapper.find('[data-test-subj="indexPattern-field-selection-row"]').first().prop('error') ).toBeDefined(); - - expect(setState).not.toHaveBeenCalled(); }); it('should leave error state if a compatible operation is selected', () => { @@ -664,6 +681,17 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { operationType: 'avg' }, + }, + }, + }, + }); const comboBox = wrapper .find(EuiComboBox) @@ -675,7 +703,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([options![1].options![2]]); }); - expect(setState).toHaveBeenCalledWith({ + expect(setState).toHaveBeenLastCalledWith({ ...state, layers: { first: { @@ -759,11 +787,9 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should set datasource state if compatible field is selected for operation', () => { wrapper = mount(); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); const comboBox = wrapper .find(EuiComboBox) @@ -774,7 +800,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith({ + expect(setState).toHaveBeenLastCalledWith({ ...state, layers: { first: { @@ -1046,6 +1072,20 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { + operationType: 'avg', + }, + }, + }, + }, + }); + const comboBox = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]'); @@ -1202,16 +1242,19 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(items.map(({ label }: { label: React.ReactNode }) => label)).toEqual([ 'Average', 'Count', + 'Last value', 'Maximum', 'Median', 'Minimum', 'Sum', 'Unique count', - '\u00a0', ]); }); it('should add a column on selection of a field', () => { + // Prevents field format from being loaded + setState.mockImplementation(() => {}); + wrapper = mount(); const comboBox = wrapper @@ -1231,6 +1274,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { ...state.layers.first.columns, col2: expect.objectContaining({ + operationType: 'range', sourceField: 'bytes', // Other parts of this don't matter for this test }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 2444a6a81c2a0..20134699d2067 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -12,7 +12,7 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternColumn } from '../indexpattern'; -import { fieldIsInvalid } from '../utils'; +import { isColumnInvalid } from '../utils'; import { IndexPatternPrivateState } from '../types'; import { DimensionEditor } from './dimension_editor'; import { DateRange } from '../../../common'; @@ -45,24 +45,22 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens ) { const layerId = props.layerId; const layer = props.state.layers[layerId]; - const selectedColumn: IndexPatternColumn | null = layer.columns[props.columnId] || null; const currentIndexPattern = props.state.indexPatterns[layer.indexPatternId]; + const { columnId, uniqueLabel } = props; - const selectedColumnSourceField = - selectedColumn && 'sourceField' in selectedColumn ? selectedColumn.sourceField : undefined; - const currentFieldIsInvalid = useMemo( - () => - fieldIsInvalid(selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern), - [selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern] + const currentColumnHasErrors = useMemo( + () => isColumnInvalid(layer, columnId, currentIndexPattern), + [layer, columnId, currentIndexPattern] ); - const { columnId, uniqueLabel } = props; + const selectedColumn: IndexPatternColumn | null = layer.columns[props.columnId] || null; + if (!selectedColumn) { return null; } const formattedLabel = wrapOnDot(uniqueLabel); - if (currentFieldIsInvalid) { + if (currentColumnHasErrors) { return ( { columns: { col1: { label: 'Date histogram of timestamp', + customLabel: true, dataType: 'date', isBucketed: true, @@ -117,6 +116,7 @@ describe('IndexPatternDimensionEditorPanel', () => { sourceField: 'timestamp', }, }, + incompleteColumns: {}, }, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 9bc3e52822cf4..135c5dcf37db9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -28,14 +28,14 @@ import { fieldExists } from '../pure_helpers'; export interface FieldChoice { type: 'field'; field: string; - operationType?: OperationType; + operationType: OperationType; } export interface FieldSelectProps extends EuiComboBoxProps<{}> { currentIndexPattern: IndexPattern; - incompatibleSelectedOperationType: OperationType | null; - selectedColumnOperationType?: OperationType; - selectedColumnSourceField?: string; + selectedOperationType?: OperationType; + selectedField?: string; + incompleteOperation?: OperationType; operationSupportMatrix: OperationSupportMatrix; onChoose: (choice: FieldChoice) => void; onDeleteColumn: () => void; @@ -45,9 +45,9 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> { export function FieldSelect({ currentIndexPattern, - incompatibleSelectedOperationType, - selectedColumnOperationType, - selectedColumnSourceField, + incompleteOperation, + selectedOperationType, + selectedField, operationSupportMatrix, onChoose, onDeleteColumn, @@ -59,14 +59,10 @@ export function FieldSelect({ const memoizedFieldOptions = useMemo(() => { const fields = Object.keys(operationByField).sort(); + const currentOperationType = incompleteOperation ?? selectedOperationType; + function isCompatibleWithCurrentOperation(fieldName: string) { - if (incompatibleSelectedOperationType) { - return operationByField[fieldName]!.has(incompatibleSelectedOperationType); - } - return ( - !selectedColumnOperationType || - operationByField[fieldName]!.has(selectedColumnOperationType) - ); + return !currentOperationType || operationByField[fieldName]!.has(currentOperationType); } const [specialFields, normalFields] = _.partition( @@ -81,20 +77,25 @@ export function FieldSelect({ function fieldNamesToOptions(items: string[]) { return items .filter((field) => currentIndexPattern.getFieldByName(field)?.displayName) - .map((field) => ({ - label: currentIndexPattern.getFieldByName(field)?.displayName, - value: { - type: 'field', - field, - dataType: currentIndexPattern.getFieldByName(field)?.type, - operationType: - selectedColumnOperationType && isCompatibleWithCurrentOperation(field) - ? selectedColumnOperationType - : undefined, - }, - exists: containsData(field), - compatible: isCompatibleWithCurrentOperation(field), - })) + .map((field) => { + return { + label: currentIndexPattern.getFieldByName(field)?.displayName, + value: { + type: 'field', + field, + dataType: currentIndexPattern.getFieldByName(field)?.type, + // Use the operation directly, or choose the first compatible operation. + // All fields are guaranteed to have at least one operation because they + // won't appear in the list otherwise + operationType: + currentOperationType && isCompatibleWithCurrentOperation(field) + ? currentOperationType + : operationByField[field]!.values().next().value, + }, + exists: containsData(field), + compatible: isCompatibleWithCurrentOperation(field), + }; + }) .sort((a, b) => { if (a.compatible && !b.compatible) { return -1; @@ -157,8 +158,8 @@ export function FieldSelect({ metaFieldsOptions, ].filter(Boolean); }, [ - incompatibleSelectedOperationType, - selectedColumnOperationType, + incompleteOperation, + selectedOperationType, currentIndexPattern, operationByField, existingFields, @@ -174,15 +175,15 @@ export function FieldSelect({ defaultMessage: 'Field', })} options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionOption[]} - isInvalid={Boolean(incompatibleSelectedOperationType || fieldIsInvalid)} + isInvalid={Boolean(incompleteOperation || fieldIsInvalid)} selectedOptions={ - ((selectedColumnOperationType && selectedColumnSourceField + ((selectedOperationType && selectedField ? [ { label: fieldIsInvalid - ? selectedColumnSourceField - : currentIndexPattern.getFieldByName(selectedColumnSourceField)?.displayName, - value: { type: 'field', field: selectedColumnSourceField }, + ? selectedField + : currentIndexPattern.getFieldByName(selectedField)?.displayName, + value: { type: 'field', field: selectedField }, }, ] : []) as unknown) as EuiComboBoxOptionOption[] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 5153a74409bee..20f71cfd3ce17 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -819,7 +819,7 @@ describe('IndexPattern Data Source', () => { expect(messages).toHaveLength(1); expect(messages![0]).toEqual({ shortMessage: 'Invalid reference.', - longMessage: 'Field "bytes" has an invalid reference.', + longMessage: '"Foo" has an invalid reference.', }); }); @@ -844,7 +844,7 @@ describe('IndexPattern Data Source', () => { col2: { dataType: 'number', isBucketed: false, - label: 'Foo', + label: 'Foo2', operationType: 'count', // <= invalid sourceField: 'memory', }, @@ -857,7 +857,7 @@ describe('IndexPattern Data Source', () => { expect(messages).toHaveLength(1); expect(messages![0]).toEqual({ shortMessage: 'Invalid references.', - longMessage: 'Fields "bytes", "memory" have invalid reference.', + longMessage: '"Foo", "Foo2" have invalid reference.', }); }); @@ -882,7 +882,7 @@ describe('IndexPattern Data Source', () => { col2: { dataType: 'number', isBucketed: false, - label: 'Foo', + label: 'Foo2', operationType: 'count', // <= invalid sourceField: 'memory', }, @@ -909,11 +909,11 @@ describe('IndexPattern Data Source', () => { expect(messages).toEqual([ { shortMessage: 'Invalid references on Layer 1.', - longMessage: 'Layer 1 has invalid references in fields "bytes", "memory".', + longMessage: 'Layer 1 has invalid references in "Foo", "Foo2".', }, { shortMessage: 'Invalid reference on Layer 2.', - longMessage: 'Layer 2 has an invalid reference in field "source".', + longMessage: 'Layer 2 has an invalid reference in "Foo".', }, ]); }); @@ -988,4 +988,44 @@ describe('IndexPattern Data Source', () => { expect(getErrorMessages).toHaveBeenCalledTimes(1); }); }); + + describe('#updateStateOnCloseDimension', () => { + it('should clear the incomplete column', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + incompleteColumns: { + col1: { operationType: 'avg' as const }, + col2: { operationType: 'sum' as const }, + }, + }, + }, + currentIndexPatternId: '1', + }; + expect( + indexPatternDatasource.updateStateOnCloseDimension!({ + state, + layerId: 'first', + columnId: 'col1', + }) + ).toEqual({ + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + incompleteColumns: { col2: { operationType: 'sum' } }, + }, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 289b6bbe3f25b..a639ea2c00ac0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -40,13 +40,13 @@ import { } from './indexpattern_suggestions'; import { - getInvalidFieldsForLayer, + getInvalidColumnsForLayer, getInvalidLayers, isDraggedField, normalizeOperationDataType, } from './utils'; import { LayerPanel } from './layerpanel'; -import { IndexPatternColumn, getErrorMessages } from './operations'; +import { IndexPatternColumn, getErrorMessages, IncompleteColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; @@ -319,6 +319,20 @@ export function getIndexPatternDatasource({ canHandleDrop, onDrop, + // Reset the temporary invalid state when closing the editor + updateStateOnCloseDimension: ({ state, layerId, columnId }) => { + const layer = { ...state.layers[layerId] }; + const newIncomplete: Record = { + ...(state.layers[layerId].incompleteColumns || {}), + }; + delete newIncomplete[columnId]; + return mergeLayer({ + state, + layerId, + newLayer: { ...layer, incompleteColumns: newIncomplete }, + }); + }, + getPublicAPI({ state, layerId }: PublicAPIProps) { const columnLabelMap = indexPatternDatasource.uniqueLabels(state); @@ -373,7 +387,7 @@ export function getIndexPatternDatasource({ } }) .filter(Boolean) as Array<[number, number]>; - const invalidFieldsPerLayer: string[][] = getInvalidFieldsForLayer( + const invalidColumnsForLayer: string[][] = getInvalidColumnsForLayer( invalidLayers, state.indexPatterns ); @@ -383,33 +397,34 @@ export function getIndexPatternDatasource({ return [ ...layerErrors, ...realIndex.map(([filteredIndex, layerIndex]) => { - const fieldsWithBrokenReferences: string[] = invalidFieldsPerLayer[filteredIndex].map( - (columnId) => { - const column = invalidLayers[filteredIndex].columns[ - columnId - ] as FieldBasedIndexPatternColumn; - return column.sourceField; - } - ); + const columnLabelsWithBrokenReferences: string[] = invalidColumnsForLayer[ + filteredIndex + ].map((columnId) => { + const column = invalidLayers[filteredIndex].columns[ + columnId + ] as FieldBasedIndexPatternColumn; + return column.label; + }); if (originalLayersList.length === 1) { return { shortMessage: i18n.translate( 'xpack.lens.indexPattern.dataReferenceFailureShortSingleLayer', { - defaultMessage: 'Invalid {fields, plural, one {reference} other {references}}.', + defaultMessage: + 'Invalid {columns, plural, one {reference} other {references}}.', values: { - fields: fieldsWithBrokenReferences.length, + columns: columnLabelsWithBrokenReferences.length, }, } ), longMessage: i18n.translate( 'xpack.lens.indexPattern.dataReferenceFailureLongSingleLayer', { - defaultMessage: `{fieldsLength, plural, one {Field} other {Fields}} "{fields}" {fieldsLength, plural, one {has an} other {have}} invalid reference.`, + defaultMessage: `"{columns}" {columnsLength, plural, one {has an} other {have}} invalid reference.`, values: { - fields: fieldsWithBrokenReferences.join('", "'), - fieldsLength: fieldsWithBrokenReferences.length, + columns: columnLabelsWithBrokenReferences.join('", "'), + columnsLength: columnLabelsWithBrokenReferences.length, }, } ), @@ -418,18 +433,18 @@ export function getIndexPatternDatasource({ return { shortMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureShort', { defaultMessage: - 'Invalid {fieldsLength, plural, one {reference} other {references}} on Layer {layer}.', + 'Invalid {columnsLength, plural, one {reference} other {references}} on Layer {layer}.', values: { layer: layerIndex, - fieldsLength: fieldsWithBrokenReferences.length, + columnsLength: columnLabelsWithBrokenReferences.length, }, }), longMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureLong', { - defaultMessage: `Layer {layer} has {fieldsLength, plural, one {an invalid} other {invalid}} {fieldsLength, plural, one {reference} other {references}} in {fieldsLength, plural, one {field} other {fields}} "{fields}".`, + defaultMessage: `Layer {layer} has {columnsLength, plural, one {an invalid} other {invalid}} {columnsLength, plural, one {reference} other {references}} in "{columns}".`, values: { layer: layerIndex, - fields: fieldsWithBrokenReferences.join('", "'), - fieldsLength: fieldsWithBrokenReferences.length, + columns: columnLabelsWithBrokenReferences.join('", "'), + columnsLength: columnLabelsWithBrokenReferences.length, }, }), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 263b4646c9feb..ebac396210a5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -18,7 +18,7 @@ import { IndexPatternColumn, OperationType, } from './operations'; -import { hasField, hasInvalidFields } from './utils'; +import { hasField, hasInvalidColumns } from './utils'; import { IndexPattern, IndexPatternPrivateState, @@ -90,7 +90,7 @@ export function getDatasourceSuggestionsForField( indexPatternId: string, field: IndexPatternField ): IndexPatternSugestion[] { - if (hasInvalidFields(state)) return []; + if (hasInvalidColumns(state)) return []; const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); @@ -331,7 +331,7 @@ function createNewLayerWithMetricAggregation( export function getDatasourceSuggestionsFromCurrentState( state: IndexPatternPrivateState ): Array> { - if (hasInvalidFields(state)) return []; + if (hasInvalidColumns(state)) return []; const layers = Object.entries(state.layers || {}); if (layers.length > 1) { // Return suggestions that reduce the data to each layer individually diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index adb86253ab28c..29786d9bc68f3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -25,8 +25,6 @@ import { import { createMockedRestrictedIndexPattern, createMockedIndexPattern } from './mocks'; import { documentField } from './document_field'; -jest.mock('./operations'); - const createMockStorage = (lastData?: Record) => { return { get: jest.fn().mockImplementation(() => lastData), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index 385f2ab941ef2..ff900134df9a1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -24,6 +24,7 @@ export const { getOperationResultType, operationDefinitionMap, operationDefinitions, + getInvalidFieldMessage, } = actualOperations; export const { @@ -40,6 +41,7 @@ export const { isColumnTransferable, getErrorMessages, isReferenced, + resetIncomplete, } = actualHelpers; export const { adjustTimeScaleLabelSuffix, DEFAULT_TIME_SCALE } = actualTimeScaleUtils; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index fd3ca4319669e..2dc3946c62a09 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; +import { getInvalidFieldMessage } from './helpers'; + const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); const SCALE = 'ratio'; @@ -42,6 +44,8 @@ export const cardinalityOperation: OperationDefinition + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), isTransferable: (column, newIndexPattern) => { const newField = newIndexPattern.getFieldByName(column.sourceField); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index d0a0fb4b28588..de3f158cca620 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -6,6 +6,7 @@ import type { Operation } from '../../../types'; import { TimeScaleUnit } from '../../time_scale'; +import type { OperationType } from '../definitions'; export interface BaseIndexPatternColumn extends Operation { // Private @@ -39,6 +40,6 @@ export interface ReferenceBasedIndexPatternColumn // Used to store the temporary invalid state export interface IncompleteColumn { - operationType?: string; + operationType?: OperationType; sourceField?: string; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 8cb95de72f97e..02a69ad8e550f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternField } from '../../types'; +import { getInvalidFieldMessage } from './helpers'; import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange, @@ -29,6 +30,8 @@ export const countOperation: OperationDefinition + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), onFieldChange: (oldColumn, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index efac9c151a435..ca426fb53a3ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -23,6 +23,7 @@ import { updateColumnParam } from '../layer_helpers'; import { OperationDefinition } from './index'; import { FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternAggRestrictions, search } from '../../../../../../../src/plugins/data/public'; +import { getInvalidFieldMessage } from './helpers'; const { isValidInterval } = search.aggs; const autoInterval = 'auto'; @@ -46,6 +47,8 @@ export const dateHistogramOperation: OperationDefinition< }), input: 'field', priority: 5, // Highest priority level used + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'date' && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index a5c08a93467af..640a357d9a7a4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -6,6 +6,10 @@ import { useRef } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; +import { i18n } from '@kbn/i18n'; +import { operationDefinitionMap } from '.'; +import { FieldBasedIndexPatternColumn } from './column_types'; +import { IndexPattern } from '../../types'; export const useDebounceWithOptions = ( fn: Function, @@ -28,3 +32,33 @@ export const useDebounceWithOptions = ( newDeps ); }; + +export function getInvalidFieldMessage( + column: FieldBasedIndexPatternColumn, + indexPattern?: IndexPattern +) { + if (!indexPattern) { + return; + } + const { sourceField, operationType } = column; + const field = sourceField ? indexPattern.getFieldByName(sourceField) : undefined; + const operationDefinition = operationType && operationDefinitionMap[operationType]; + + const isInvalid = Boolean( + sourceField && + operationDefinition && + !( + field && + operationDefinition?.input === 'field' && + operationDefinition.getPossibleOperationForField(field) !== undefined + ) + ); + return isInvalid + ? [ + i18n.translate('xpack.lens.indexPattern.fieldNotFound', { + defaultMessage: 'Field {invalidField} was not found', + values: { invalidField: sourceField }, + }), + ] + : undefined; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 31bb332f791da..460c7c5492879 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -34,6 +34,7 @@ import { MovingAverageIndexPatternColumn, } from './calculations'; import { countOperation, CountIndexPatternColumn } from './count'; +import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; import { StateSetter, OperationMetadata } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; import { @@ -63,6 +64,7 @@ export type IndexPatternColumn = | SumIndexPatternColumn | MedianIndexPatternColumn | CountIndexPatternColumn + | LastValueIndexPatternColumn | CumulativeSumIndexPatternColumn | CounterRateIndexPatternColumn | DerivativeIndexPatternColumn @@ -85,6 +87,7 @@ const internalOperationDefinitions = [ cardinalityOperation, sumOperation, medianOperation, + lastValueOperation, countOperation, rangeOperation, cumulativeSumOperation, @@ -99,6 +102,7 @@ export { filtersOperation } from './filters'; export { dateHistogramOperation } from './date_histogram'; export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; export { countOperation } from './count'; +export { lastValueOperation } from './last_value'; export { cumulativeSumOperation, counterRateOperation, @@ -173,6 +177,24 @@ interface BaseOperationDefinitionProps { */ transfer?: (column: C, newIndexPattern: IndexPattern) => C; /** + * if there is some reason to display the operation in the operations list + * but disable it from usage, this function returns the string describing + * the status. Otherwise it returns undefined + */ + getDisabledStatus?: (indexPattern: IndexPattern) => string | undefined; + /** + * Validate that the operation has the right preconditions in the state. For example: + * + * - Requires a date histogram operation somewhere before it in order + * - Missing references + */ + getErrorMessage?: ( + layer: IndexPatternLayer, + columnId: string, + indexPattern?: IndexPattern + ) => string[] | undefined; + + /* * Flag whether this operation can be scaled by time unit if a date histogram is available. * If set to mandatory or optional, a UI element is shown in the config flyout to configure the time unit * to scale by. The chosen unit will be persisted as `timeScale` property of the column. @@ -245,6 +267,17 @@ interface FieldBasedOperationDefinition { * together with the agg configs returned from other columns. */ toEsAggsConfig: (column: C, columnId: string, indexPattern: IndexPattern) => unknown; + /** + * Validate that the operation has the right preconditions in the state. For example: + * + * - Requires a date histogram operation somewhere before it in order + * - Missing references + */ + getErrorMessage: ( + layer: IndexPatternLayer, + columnId: string, + indexPattern?: IndexPattern + ) => string[] | undefined; } export interface RequiredReference { @@ -297,13 +330,6 @@ interface FullReferenceOperationDefinition { columnId: string, indexPattern: IndexPattern ) => ExpressionFunctionAST[]; - /** - * Validate that the operation has the right preconditions in the state. For example: - * - * - Requires a date histogram operation somewhere before it in order - * - Missing references - */ - getErrorMessage?: (layer: IndexPatternLayer, columnId: string) => string[] | undefined; } interface OperationDefinitionMap { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx new file mode 100644 index 0000000000000..09b68e78d3469 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -0,0 +1,477 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiComboBox } from '@elastic/eui'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { createMockedIndexPattern } from '../../mocks'; +import { LastValueIndexPatternColumn } from './last_value'; +import { lastValueOperation } from './index'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from '../../types'; + +const defaultProps = { + storage: {} as IStorageWrapper, + uiSettings: {} as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + data: dataPluginMock.createStartContract(), + http: {} as HttpSetup, +}; + +describe('last_value', () => { + let state: IndexPatternPrivateState; + const InlineOptions = lastValueOperation.paramEditor!; + + beforeEach(() => { + const indexPattern = createMockedIndexPattern(); + state = { + indexPatternRefs: [], + indexPatterns: { + '1': { + ...indexPattern, + hasRestrictions: false, + } as IndexPattern, + }, + existingFields: {}, + currentIndexPatternId: '1', + isFirstExistenceFetch: false, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + col2: { + label: 'Last value of a', + dataType: 'number', + isBucketed: false, + sourceField: 'a', + operationType: 'last_value', + params: { + sortField: 'datefield', + }, + }, + }, + }, + }, + }; + }); + + describe('toEsAggsConfig', () => { + it('should reflect params correctly', () => { + const lastValueColumn = state.layers.first.columns.col2 as LastValueIndexPatternColumn; + const esAggsConfig = lastValueOperation.toEsAggsConfig( + { ...lastValueColumn, params: { ...lastValueColumn.params } }, + 'col1', + {} as IndexPattern + ); + expect(esAggsConfig).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + aggregate: 'concat', + field: 'a', + size: 1, + sortField: 'datefield', + sortOrder: 'desc', + }), + }) + ); + }); + }); + + describe('onFieldChange', () => { + it('should change correctly to new field', () => { + const oldColumn: LastValueIndexPatternColumn = { + operationType: 'last_value', + sourceField: 'source', + label: 'Last value of source', + isBucketed: true, + dataType: 'string', + params: { + sortField: 'datefield', + }, + }; + const indexPattern = createMockedIndexPattern(); + const newNumberField = indexPattern.getFieldByName('bytes')!; + const column = lastValueOperation.onFieldChange(oldColumn, newNumberField); + + expect(column).toEqual( + expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + params: expect.objectContaining({ + sortField: 'datefield', + }), + }) + ); + expect(column.label).toContain('bytes'); + }); + + it('should remove numeric parameters when changing away from number', () => { + const oldColumn: LastValueIndexPatternColumn = { + operationType: 'last_value', + sourceField: 'bytes', + label: 'Last value of bytes', + isBucketed: false, + dataType: 'number', + params: { + sortField: 'datefield', + }, + }; + const indexPattern = createMockedIndexPattern(); + const newStringField = indexPattern.fields.find((i) => i.name === 'source')!; + + const column = lastValueOperation.onFieldChange(oldColumn, newStringField); + expect(column).toHaveProperty('dataType', 'string'); + expect(column).toHaveProperty('sourceField', 'source'); + expect(column.params.format).toBeUndefined(); + }); + }); + + describe('getPossibleOperationForField', () => { + it('should return operation with the right type', () => { + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + displayName: 'test', + type: 'boolean', + }) + ).toEqual({ + dataType: 'boolean', + isBucketed: false, + scale: 'ratio', + }); + + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + displayName: 'test', + type: 'ip', + }) + ).toEqual({ + dataType: 'ip', + isBucketed: false, + scale: 'ratio', + }); + }); + + it('should not return an operation if restrictions prevent terms', () => { + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + displayName: 'test', + type: 'string', + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }) + ).toEqual(undefined); + + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: true, + aggregationRestrictions: {}, + searchable: true, + name: 'test', + displayName: 'test', + type: 'string', + }) + ).toEqual(undefined); + // does it have to be aggregatable? + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: false, + searchable: true, + name: 'test', + displayName: 'test', + type: 'string', + }) + ).toEqual({ dataType: 'string', isBucketed: false, scale: 'ordinal' }); + }); + }); + + describe('buildColumn', () => { + it('should use type from the passed field', () => { + const lastValueColumn = lastValueOperation.buildColumn({ + indexPattern: createMockedIndexPattern(), + field: { + aggregatable: true, + searchable: true, + type: 'boolean', + name: 'test', + displayName: 'test', + }, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }); + expect(lastValueColumn.dataType).toEqual('boolean'); + }); + + it('should use indexPattern timeFieldName as a default sortField', () => { + const lastValueColumn = lastValueOperation.buildColumn({ + indexPattern: createMockedIndexPattern(), + + layer: { + columns: { + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + columnOrder: [], + indexPatternId: '', + }, + + field: { + aggregatable: true, + searchable: true, + type: 'boolean', + name: 'test', + displayName: 'test', + }, + }); + expect(lastValueColumn.params).toEqual( + expect.objectContaining({ + sortField: 'timestamp', + }) + ); + }); + + it('should use first indexPattern date field if there is no default timefieldName', () => { + const indexPattern = createMockedIndexPattern(); + const indexPatternNoTimeField = { + ...indexPattern, + timeFieldName: undefined, + fields: [ + { + aggregatable: true, + searchable: true, + type: 'date', + name: 'datefield', + displayName: 'datefield', + }, + { + aggregatable: true, + searchable: true, + type: 'boolean', + name: 'test', + displayName: 'test', + }, + ], + }; + const lastValueColumn = lastValueOperation.buildColumn({ + indexPattern: indexPatternNoTimeField, + + layer: { + columns: { + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + columnOrder: [], + indexPatternId: '', + }, + + field: { + aggregatable: true, + searchable: true, + type: 'boolean', + name: 'test', + displayName: 'test', + }, + }); + expect(lastValueColumn.params).toEqual( + expect.objectContaining({ + sortField: 'datefield', + }) + ); + }); + }); + + it('should return disabledStatus if indexPattern does contain date field', () => { + const indexPattern = createMockedIndexPattern(); + + expect(lastValueOperation.getDisabledStatus!(indexPattern)).toEqual(undefined); + + const indexPatternWithoutTimeFieldName = { + ...indexPattern, + timeFieldName: undefined, + }; + expect(lastValueOperation.getDisabledStatus!(indexPatternWithoutTimeFieldName)).toEqual( + undefined + ); + + const indexPatternWithoutTimefields = { + ...indexPatternWithoutTimeFieldName, + fields: indexPattern.fields.filter((f) => f.type !== 'date'), + }; + + const disabledStatus = lastValueOperation.getDisabledStatus!(indexPatternWithoutTimefields); + expect(disabledStatus).toEqual( + 'This function requires the presence of a date field in your index' + ); + }); + + describe('param editor', () => { + it('should render current sortField', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + const select = instance.find('[data-test-subj="lns-indexPattern-lastValue-sortField"]'); + + expect(select.prop('selectedOptions')).toEqual([{ label: 'datefield', value: 'datefield' }]); + }); + + it('should update state when changing sortField', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + + instance + .find('[data-test-subj="lns-indexPattern-lastValue-sortField"]') + .find(EuiComboBox) + .prop('onChange')!([{ label: 'datefield2', value: 'datefield2' }]); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + ...state.layers.first.columns.col2, + params: { + ...(state.layers.first.columns.col2 as LastValueIndexPatternColumn).params, + sortField: 'datefield2', + }, + }, + }, + }, + }, + }); + }); + }); + + describe('getErrorMessage', () => { + let indexPattern: IndexPattern; + let layer: IndexPatternLayer; + beforeEach(() => { + indexPattern = createMockedIndexPattern(); + layer = { + columns: { + col1: { + dataType: 'boolean', + isBucketed: false, + label: 'Last value of test', + operationType: 'last_value', + params: { sortField: 'timestamp' }, + scale: 'ratio', + sourceField: 'bytes', + }, + }, + columnOrder: [], + indexPatternId: '', + }; + }); + it('returns undefined if sourceField exists and sortField is of type date ', () => { + expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual(undefined); + }); + it('shows error message if the sourceField does not exist in index pattern', () => { + layer = { + ...layer, + columns: { + col1: { + ...layer.columns.col1, + sourceField: 'notExisting', + } as LastValueIndexPatternColumn, + }, + }; + expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([ + 'Field notExisting was not found', + ]); + }); + it('shows error message if the sortField does not exist in index pattern', () => { + layer = { + ...layer, + columns: { + col1: { + ...layer.columns.col1, + params: { + ...layer.columns.col1.params, + sortField: 'notExisting', + }, + } as LastValueIndexPatternColumn, + }, + }; + expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([ + 'Field notExisting was not found', + ]); + }); + it('shows error message if the sortField is not date', () => { + layer = { + ...layer, + columns: { + col1: { + ...layer.columns.col1, + params: { + ...layer.columns.col1.params, + sortField: 'bytes', + }, + } as LastValueIndexPatternColumn, + }, + }; + expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([ + 'Field bytes is not a date field and cannot be used for sorting', + ]); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx new file mode 100644 index 0000000000000..5ae5dd472ce22 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { OperationDefinition } from './index'; +import { FieldBasedIndexPatternColumn } from './column_types'; +import { IndexPatternField, IndexPattern } from '../../types'; +import { updateColumnParam } from '../layer_helpers'; +import { DataType } from '../../../types'; +import { getInvalidFieldMessage } from './helpers'; + +function ofName(name: string) { + return i18n.translate('xpack.lens.indexPattern.lastValueOf', { + defaultMessage: 'Last value of {name}', + values: { name }, + }); +} + +const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']); + +export function getInvalidSortFieldMessage(sortField: string, indexPattern?: IndexPattern) { + if (!indexPattern) { + return; + } + const field = indexPattern.getFieldByName(sortField); + if (!field) { + return i18n.translate('xpack.lens.indexPattern.lastValue.sortFieldNotFound', { + defaultMessage: 'Field {invalidField} was not found', + values: { invalidField: sortField }, + }); + } + if (field.type !== 'date') { + return i18n.translate('xpack.lens.indexPattern.lastValue.invalidTypeSortField', { + defaultMessage: 'Field {invalidField} is not a date field and cannot be used for sorting', + values: { invalidField: sortField }, + }); + } +} + +function isTimeFieldNameDateField(indexPattern: IndexPattern) { + return ( + indexPattern.timeFieldName && + indexPattern.fields.find( + (field) => field.name === indexPattern.timeFieldName && field.type === 'date' + ) + ); +} + +function getDateFields(indexPattern: IndexPattern): IndexPatternField[] { + const dateFields = indexPattern.fields.filter((field) => field.type === 'date'); + if (isTimeFieldNameDateField(indexPattern)) { + dateFields.sort(({ name: nameA }, { name: nameB }) => { + if (nameA === indexPattern.timeFieldName) { + return -1; + } + if (nameB === indexPattern.timeFieldName) { + return 1; + } + return 0; + }); + } + return dateFields; +} + +export interface LastValueIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'last_value'; + params: { + sortField: string; + // last value on numeric fields can be formatted + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +export const lastValueOperation: OperationDefinition = { + type: 'last_value', + displayName: i18n.translate('xpack.lens.indexPattern.lastValue', { + defaultMessage: 'Last value', + }), + getDefaultLabel: (column, indexPattern) => + indexPattern.getFieldByName(column.sourceField)!.displayName, + input: 'field', + onFieldChange: (oldColumn, field) => { + const newParams = { ...oldColumn.params }; + + if ('format' in newParams && field.type !== 'number') { + delete newParams.format; + } + return { + ...oldColumn, + dataType: field.type as DataType, + label: ofName(field.displayName), + sourceField: field.name, + params: newParams, + }; + }, + getPossibleOperationForField: ({ aggregationRestrictions, type }) => { + if (supportedTypes.has(type) && !aggregationRestrictions) { + return { + dataType: type as DataType, + isBucketed: false, + scale: type === 'string' ? 'ordinal' : 'ratio', + }; + } + }, + getDisabledStatus(indexPattern: IndexPattern) { + const hasDateFields = indexPattern && getDateFields(indexPattern).length; + if (!hasDateFields) { + return i18n.translate('xpack.lens.indexPattern.lastValue.disabled', { + defaultMessage: 'This function requires the presence of a date field in your index', + }); + } + }, + getErrorMessage(layer, columnId, indexPattern) { + const column = layer.columns[columnId] as LastValueIndexPatternColumn; + let errorMessages: string[] = []; + const invalidSourceFieldMessage = getInvalidFieldMessage(column, indexPattern); + const invalidSortFieldMessage = getInvalidSortFieldMessage( + column.params.sortField, + indexPattern + ); + if (invalidSourceFieldMessage) { + errorMessages = [...invalidSourceFieldMessage]; + } + if (invalidSortFieldMessage) { + errorMessages = [invalidSortFieldMessage]; + } + return errorMessages.length ? errorMessages : undefined; + }, + buildColumn({ field, previousColumn, indexPattern }) { + const sortField = isTimeFieldNameDateField(indexPattern) + ? indexPattern.timeFieldName + : indexPattern.fields.find((f) => f.type === 'date')?.name; + + if (!sortField) { + throw new Error( + i18n.translate('xpack.lens.functions.lastValue.missingSortField', { + defaultMessage: 'This index pattern does not contain any date fields', + }) + ); + } + + return { + label: ofName(field.displayName), + dataType: field.type as DataType, + operationType: 'last_value', + isBucketed: false, + scale: field.type === 'string' ? 'ordinal' : 'ratio', + sourceField: field.name, + params: { + sortField, + }, + }; + }, + toEsAggsConfig: (column, columnId) => ({ + id: columnId, + enabled: true, + schema: 'metric', + type: 'top_hits', + params: { + field: column.sourceField, + aggregate: 'concat', + size: 1, + sortOrder: 'desc', + sortField: column.params.sortField, + }, + }), + + isTransferable: (column, newIndexPattern) => { + const newField = newIndexPattern.getFieldByName(column.sourceField); + const newTimeField = newIndexPattern.getFieldByName(column.params.sortField); + return Boolean( + newField && + newField.type === column.dataType && + !newField.aggregationRestrictions && + newTimeField?.type === 'date' + ); + }, + + paramEditor: ({ state, setState, currentColumn, layerId }) => { + const currentIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; + const dateFields = getDateFields(currentIndexPattern); + const isSortFieldInvalid = !!getInvalidSortFieldMessage( + currentColumn.params.sortField, + currentIndexPattern + ); + return ( + <> + + { + return { + value: field.name, + label: field.displayName, + }; + })} + onChange={(choices) => { + if (choices.length === 0) { + return; + } + setState( + updateColumnParam({ + state, + layerId, + currentColumn, + paramName: 'sortField', + value: choices[0].value, + }) + ); + }} + selectedOptions={ + ((currentColumn.params?.sortField + ? [ + { + label: + currentIndexPattern.getFieldByName(currentColumn.params.sortField) + ?.displayName || currentColumn.params.sortField, + value: currentColumn.params.sortField, + }, + ] + : []) as unknown) as EuiComboBoxOptionOption[] + } + /> + + + ); + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 45ba721981ed5..10a0b915b552d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; +import { getInvalidFieldMessage } from './helpers'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn, @@ -103,6 +104,8 @@ function buildMetricOperation>({ missing: 0, }, }), + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), } as OperationDefinition; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index d2456e1c8d375..f2d3435cc52c0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -17,6 +17,7 @@ import { mergeLayer } from '../../../state_helpers'; import { supportedFormats } from '../../../format_column'; import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; import { IndexPattern, IndexPatternField } from '../../../types'; +import { getInvalidFieldMessage } from '../helpers'; type RangeType = Omit; // Try to cover all possible serialized states for ranges @@ -109,6 +110,8 @@ export const rangeOperation: OperationDefinition + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'number' && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 7c69a70c09351..e8351ea1e1d09 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -22,6 +22,7 @@ import { DataType } from '../../../../types'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { ValuesRangeInput } from './values_range_input'; +import { getInvalidFieldMessage } from '../helpers'; function ofName(name: string) { return i18n.translate('xpack.lens.indexPattern.termsOf', { @@ -31,7 +32,7 @@ function ofName(name: string) { } function isSortableByColumn(column: IndexPatternColumn) { - return !column.isBucketed; + return !column.isBucketed && column.operationType !== 'last_value'; } const DEFAULT_SIZE = 3; @@ -71,6 +72,8 @@ export const termsOperation: OperationDefinition + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), isTransferable: (column, newIndexPattern) => { const newField = newIndexPattern.getFieldByName(column.sourceField); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index e43c7bbd2f72e..0af0f9a9d8613 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -15,7 +15,7 @@ import { createMockedIndexPattern } from '../../../mocks'; import { ValuesRangeInput } from './values_range_input'; import { TermsIndexPatternColumn } from '.'; import { termsOperation } from '../index'; -import { IndexPatternPrivateState, IndexPattern } from '../../../types'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from '../../../types'; const defaultProps = { storage: {} as IStorageWrapper, @@ -368,7 +368,7 @@ describe('terms', () => { }); describe('onOtherColumnChanged', () => { - it('should keep the column if order by column still exists and is metric', () => { + it('should keep the column if order by column still exists and is isSortableByColumn metric', () => { const initialColumn: TermsIndexPatternColumn = { label: 'Top value of category', dataType: 'string', @@ -395,6 +395,40 @@ describe('terms', () => { expect(updatedColumn).toBe(initialColumn); }); + it('should switch to alphabetical ordering if metric is of type last_value', () => { + const initialColumn: TermsIndexPatternColumn = { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col1' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }; + const updatedColumn = termsOperation.onOtherColumnChanged!(initialColumn, { + col1: { + label: 'Last Value', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'last_value', + params: { + sortField: 'time', + }, + }, + }); + expect(updatedColumn.params).toEqual( + expect.objectContaining({ + orderBy: { type: 'alphabetical' }, + }) + ); + }); + it('should switch to alphabetical ordering if there are no columns to order by', () => { const termsColumn = termsOperation.onOtherColumnChanged!( { @@ -770,4 +804,49 @@ describe('terms', () => { }); }); }); + describe('getErrorMessage', () => { + let indexPattern: IndexPattern; + let layer: IndexPatternLayer; + beforeEach(() => { + indexPattern = createMockedIndexPattern(); + layer = { + columns: { + col1: { + dataType: 'boolean', + isBucketed: true, + label: 'Top values of bytes', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + otherBucket: true, + size: 5, + }, + scale: 'ordinal', + sourceField: 'bytes', + }, + }, + columnOrder: [], + indexPatternId: '', + }; + }); + it('returns undefined if sourceField exists in index pattern', () => { + expect(termsOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual(undefined); + }); + it('returns error message if the sourceField does not exist in index pattern', () => { + layer = { + ...layer, + columns: { + col1: { + ...layer.columns.col1, + sourceField: 'notExisting', + } as TermsIndexPatternColumn, + }, + }; + expect(termsOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([ + 'Field notExisting was not found', + ]); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 0d103a766c23a..93447053a6029 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -228,16 +228,22 @@ describe('state_helpers', () => { ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); - it('should throw if the aggregation does not support the field', () => { - expect(() => { + it('should insert both incomplete states if the aggregation does not support the field', () => { + expect( insertNewColumn({ layer: { indexPatternId: '1', columnOrder: [], columns: {} }, columnId: 'col1', indexPattern, op: 'terms', field: indexPattern.fields[0], - }); - }).toThrow(); + }) + ).toEqual( + expect.objectContaining({ + incompleteColumns: { + col1: { operationType: 'terms', sourceField: 'timestamp' }, + }, + }) + ); }); it('should put the terms agg ahead of the date histogram', () => { @@ -531,8 +537,8 @@ describe('state_helpers', () => { }).toThrow(); }); - it('should throw if switching to a field-based operation without providing a field', () => { - expect(() => { + it('should set incompleteColumns when switching to a field-based operation without providing a field', () => { + expect( replaceColumn({ layer: { indexPatternId: '1', @@ -554,12 +560,19 @@ describe('state_helpers', () => { }, columnId: 'col1', indexPattern, - op: 'date_histogram', - }); - }).toThrow(); + op: 'terms', + }) + ).toEqual( + expect.objectContaining({ + columns: { col1: expect.objectContaining({ operationType: 'date_histogram' }) }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, + }) + ); }); - it('should carry over params from old column if the switching fields', () => { + it('should carry over params from old column if switching fields', () => { expect( replaceColumn({ layer: { @@ -592,7 +605,7 @@ describe('state_helpers', () => { ); }); - it('should transition from field-based to fieldless operation', () => { + it('should transition from field-based to fieldless operation, clearing incomplete', () => { expect( replaceColumn({ layer: { @@ -612,14 +625,20 @@ describe('state_helpers', () => { }, }, }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, }, indexPattern, columnId: 'col1', op: 'filters', - }).columns.col1 + }) ).toEqual( expect.objectContaining({ - operationType: 'filters', + columns: { + col1: expect.objectContaining({ operationType: 'filters' }), + }, + incompleteColumns: {}, }) ); }); @@ -944,6 +963,7 @@ describe('state_helpers', () => { isTransferable: jest.fn(), toExpression: jest.fn().mockReturnValue([]), getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), + getDefaultLabel: () => 'Test reference', }; const layer: IndexPatternLayer = { @@ -1686,7 +1706,6 @@ describe('state_helpers', () => { describe('getErrorMessages', () => { it('should collect errors from the operation definitions', () => { const mock = jest.fn().mockReturnValue(['error 1']); - // @ts-expect-error not statically analyzed operationDefinitionMap.testReference.getErrorMessage = mock; const errors = getErrorMessages({ indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 260ed180da921..b16418d44ba33 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -153,15 +153,50 @@ export function insertNewColumn({ } } - if (!field) { - throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); + const invalidFieldName = (layer.incompleteColumns ?? {})[columnId]?.sourceField; + const invalidField = invalidFieldName ? indexPattern.getFieldByName(invalidFieldName) : undefined; + + if (!field && invalidField) { + const possibleOperation = operationDefinition.getPossibleOperationForField(invalidField); + if (!possibleOperation) { + throw new Error( + `Tried to create an invalid operation ${operationDefinition.type} using previously selected field ${invalidField.name}` + ); + } + const isBucketed = Boolean(possibleOperation.isBucketed); + if (isBucketed) { + return addBucket( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), + columnId + ); + } else { + return addMetric( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), + columnId + ); + } + } else if (!field) { + // Labels don't need to be updated because it's incomplete + return { + ...layer, + incompleteColumns: { + ...(layer.incompleteColumns ?? {}), + [columnId]: { operationType: op }, + }, + }; } const possibleOperation = operationDefinition.getPossibleOperationForField(field); if (!possibleOperation) { - throw new Error( - `Tried to create an invalid operation ${operationDefinition.type} on ${field.name}` - ); + return { + ...layer, + incompleteColumns: { + ...(layer.incompleteColumns ?? {}), + [columnId]: { operationType: op, sourceField: field.name }, + }, + }; } const isBucketed = Boolean(possibleOperation.isBucketed); if (isBucketed) { @@ -208,6 +243,8 @@ export function replaceColumn({ if (isNewOperation) { let tempLayer = { ...layer }; + tempLayer = resetIncomplete(tempLayer, columnId); + if (previousDefinition.input === 'fullReference') { (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { tempLayer = deleteColumn({ layer: tempLayer, columnId: id }); @@ -217,8 +254,6 @@ export function replaceColumn({ if (operationDefinition.input === 'fullReference') { const referenceIds = operationDefinition.requiredReferences.map(() => generateId()); - const incompleteColumns = { ...(tempLayer.incompleteColumns || {}) }; - delete incompleteColumns[columnId]; const newColumns = { ...tempLayer.columns, [columnId]: operationDefinition.buildColumn({ @@ -232,7 +267,6 @@ export function replaceColumn({ ...tempLayer, columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), columns: newColumns, - incompleteColumns, }; } @@ -249,7 +283,13 @@ export function replaceColumn({ } if (!field) { - throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); + return { + ...tempLayer, + incompleteColumns: { + ...(tempLayer.incompleteColumns ?? {}), + [columnId]: { operationType: op }, + }, + }; } let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, field }); @@ -296,7 +336,7 @@ function addBucket( column: IndexPatternColumn, addedColumnId: string ): IndexPatternLayer { - const [buckets, metrics] = separateBucketColumns(layer); + const [buckets, metrics, references] = getExistingColumnGroups(layer); const oldDateHistogramIndex = layer.columnOrder.findIndex( (columnId) => layer.columns[columnId].operationType === 'date_histogram' @@ -310,17 +350,19 @@ function addBucket( addedColumnId, ...buckets.slice(oldDateHistogramIndex, buckets.length), ...metrics, + ...references, ]; } else { // Insert the new bucket after existing buckets. Users will see the same data // they already had, with an extra level of detail. - updatedColumnOrder = [...buckets, addedColumnId, ...metrics]; + updatedColumnOrder = [...buckets, addedColumnId, ...metrics, ...references]; } - return { - ...layer, + const tempLayer = { + ...resetIncomplete(layer, addedColumnId), columns: { ...layer.columns, [addedColumnId]: column }, columnOrder: updatedColumnOrder, }; + return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } function addMetric( @@ -328,18 +370,15 @@ function addMetric( column: IndexPatternColumn, addedColumnId: string ): IndexPatternLayer { - return { - ...layer, + const tempLayer = { + ...resetIncomplete(layer, addedColumnId), columns: { ...layer.columns, [addedColumnId]: column, }, columnOrder: [...layer.columnOrder, addedColumnId], }; -} - -function separateBucketColumns(layer: IndexPatternLayer) { - return partition(layer.columnOrder, (columnId) => layer.columns[columnId]?.isBucketed); + return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } export function getMetricOperationTypes(field: IndexPatternField) { @@ -442,9 +481,24 @@ export function deleteColumn({ return { ...newLayer, columnOrder: getColumnOrder(newLayer), incompleteColumns: newIncomplete }; } +// Derives column order from column object, respects existing columnOrder +// when possible, but also allows new columns to be added to the order export function getColumnOrder(layer: IndexPatternLayer): string[] { + const entries = Object.entries(layer.columns); + entries.sort(([idA], [idB]) => { + const indexA = layer.columnOrder.indexOf(idA); + const indexB = layer.columnOrder.indexOf(idB); + if (indexA > -1 && indexB > -1) { + return indexA - indexB; + } else if (indexA > -1) { + return -1; + } else { + return 1; + } + }); + const [direct, referenceBased] = _.partition( - Object.entries(layer.columns), + entries, ([id, col]) => operationDefinitionMap[col.operationType].input !== 'fullReference' ); // If a reference has another reference as input, put it last in sort order @@ -465,6 +519,15 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { .concat(referenceBased.map(([id]) => id)); } +// Splits existing columnOrder into the three categories +function getExistingColumnGroups(layer: IndexPatternLayer): [string[], string[], string[]] { + const [direct, referenced] = partition( + layer.columnOrder, + (columnId) => layer.columns[columnId] && !('references' in layer.columns[columnId]) + ); + return [...partition(direct, (columnId) => layer.columns[columnId]?.isBucketed), referenced]; +} + /** * Returns true if the given column can be applied to the given index pattern */ @@ -601,3 +664,9 @@ function isOperationAllowedAsReference({ hasValidMetadata ); } + +export function resetIncomplete(layer: IndexPatternLayer, columnId: string): IndexPatternLayer { + const incompleteColumns = { ...(layer.incompleteColumns ?? {}) }; + delete incompleteColumns[columnId]; + return { ...layer, incompleteColumns }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts index c3f7dac03ada3..33af8842648f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts @@ -35,5 +35,6 @@ export const createMockedReferenceOperation = () => { toExpression: jest.fn().mockReturnValue([]), getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), getDefaultLabel: jest.fn().mockReturnValue('Default label'), + getErrorMessage: jest.fn(), }; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 63d0fd3d4e5c5..9f2b8eab4e09b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -293,6 +293,25 @@ describe('getOperationTypesForField', () => { "operationType": "median", "type": "field", }, + Object { + "field": "bytes", + "operationType": "last_value", + "type": "field", + }, + ], + }, + Object { + "operationMetaData": Object { + "dataType": "string", + "isBucketed": false, + "scale": "ordinal", + }, + "operations": Array [ + Object { + "field": "source", + "operationType": "last_value", + "type": "field", + }, ], }, ] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 01b834610eb1a..5f4865ca0ac32 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -11,7 +11,9 @@ import { BaseIndexPatternColumn, FieldBasedIndexPatternColumn, } from './operations/definitions/column_types'; -import { operationDefinitionMap, OperationType } from './operations'; +import { operationDefinitionMap, IndexPatternColumn } from './operations'; + +import { getInvalidFieldMessage } from './operations/definitions/helpers'; /** * Normalizes the specified operation type. (e.g. document operations @@ -42,60 +44,46 @@ export function isDraggedField(fieldCandidate: unknown): fieldCandidate is Dragg ); } -export function hasInvalidFields(state: IndexPatternPrivateState) { +export function hasInvalidColumns(state: IndexPatternPrivateState) { return getInvalidLayers(state).length > 0; } export function getInvalidLayers(state: IndexPatternPrivateState) { return Object.values(state.layers).filter((layer) => { - return layer.columnOrder.some((columnId) => { - const column = layer.columns[columnId]; - return ( - hasField(column) && - fieldIsInvalid( - column.sourceField, - column.operationType, - state.indexPatterns[layer.indexPatternId] - ) - ); - }); + return layer.columnOrder.some((columnId) => + isColumnInvalid(layer, columnId, state.indexPatterns[layer.indexPatternId]) + ); }); } -export function getInvalidFieldsForLayer( +export function getInvalidColumnsForLayer( layers: IndexPatternLayer[], indexPatternMap: Record ) { return layers.map((layer) => { - return layer.columnOrder.filter((columnId) => { - const column = layer.columns[columnId]; - return ( - hasField(column) && - fieldIsInvalid( - column.sourceField, - column.operationType, - indexPatternMap[layer.indexPatternId] - ) - ); - }); + return layer.columnOrder.filter((columnId) => + isColumnInvalid(layer, columnId, indexPatternMap[layer.indexPatternId]) + ); }); } -export function fieldIsInvalid( - sourceField: string | undefined, - operationType: OperationType | undefined, +export function isColumnInvalid( + layer: IndexPatternLayer, + columnId: string, indexPattern: IndexPattern ) { - const operationDefinition = operationType && operationDefinitionMap[operationType]; - const field = sourceField ? indexPattern.getFieldByName(sourceField) : undefined; + const column = layer.columns[columnId]; - return Boolean( - sourceField && - operationDefinition && - !( - field && - operationDefinition?.input === 'field' && - operationDefinition.getPossibleOperationForField(field) !== undefined - ) + const operationDefinition = column.operationType && operationDefinitionMap[column.operationType]; + return !!( + operationDefinition.getErrorMessage && + operationDefinition.getErrorMessage(layer, columnId, indexPattern) ); } + +export function fieldIsInvalid(column: IndexPatternColumn | undefined, indexPattern: IndexPattern) { + if (!column || !hasField(column)) { + return false; + } + return !!getInvalidFieldMessage(column, indexPattern)?.length; +} diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 91f0ddb54ad41..2d9a345b978ec 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -245,6 +245,34 @@ export const getPieVisualization = ({ ); }, + getWarningMessages(state, frame) { + if (state?.layers.length === 0 || !frame.activeData) { + return; + } + + const metricColumnsWithArrayValues = []; + + for (const layer of state.layers) { + const { layerId, metric } = layer; + const rows = frame.activeData[layerId] && frame.activeData[layerId].rows; + if (!rows || !metric) { + break; + } + const columnToLabel = frame.datasourceLayers[layerId].getOperationForColumnId(metric)?.label; + + const hasArrayValues = rows.some((row) => Array.isArray(row[metric])); + if (hasArrayValues) { + metricColumnsWithArrayValues.push(columnToLabel || metric); + } + } + return metricColumnsWithArrayValues.map((label) => ( + <> + {label} contains array values. Your visualization may not render as + expected. + + )); + }, + getErrorMessages(state, frame) { // not possible to break it? return undefined; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 2f9310ee24ae9..24075facb68eb 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -5,13 +5,16 @@ */ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; -import { DashboardStart } from 'src/plugins/dashboard/public'; -import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; -import { VisualizationsSetup, VisualizationsStart } from 'src/plugins/visualizations/public'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { UrlForwardingSetup } from 'src/plugins/url_forwarding/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { DashboardStart } from '../../../../src/plugins/dashboard/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../src/plugins/expressions/public'; +import { + VisualizationsSetup, + VisualizationsStart, +} from '../../../../src/plugins/visualizations/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; +import { UrlForwardingSetup } from '../../../../src/plugins/url_forwarding/public'; import { GlobalSearchPluginSetup } from '../../global_search/public'; import { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { EditorFrameService } from './editor_frame_service'; @@ -64,6 +67,7 @@ export interface LensPluginStartDependencies { charts: ChartsPluginStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; } + export class LensPlugin { private datatableVisualization: DatatableVisualization; private editorFrameService: EditorFrameService; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 65c565087af72..ba459a73ea0ee 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -167,6 +167,7 @@ export interface Datasource { renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; + updateStateOnCloseDimension?: (props: { layerId: string; columnId: string; state: T }) => T; toExpression: (state: T, layerId: string) => Ast | string | null; @@ -597,6 +598,11 @@ export interface Visualization { state: T, frame: FramePublicAPI ) => Array<{ shortMessage: string; longMessage: string }> | undefined; + + /** + * The frame calls this function to display warnings about visualization + */ + getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined; } export interface LensFilterEvent { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 546cf06d4014e..d780ce85bad69 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -775,4 +775,69 @@ describe('xy_visualization', () => { ]); }); }); + + describe('#getWarningMessages', () => { + let mockDatasource: ReturnType; + let frame: ReturnType; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'd' }, + { columnId: 'a' }, + { columnId: 'b' }, + { columnId: 'c' }, + ]); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + frame.activeData = { + first: { + type: 'datatable', + columns: [ + { id: 'a', name: 'A', meta: { type: 'number' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, + ], + rows: [ + { a: 1, b: [2, 0] }, + { a: 3, b: 4 }, + { a: 5, b: 6 }, + { a: 7, b: 8 }, + ], + }, + }; + }); + it('should return a warning when numeric accessors contain array', () => { + (frame.datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({ + label: 'Label B', + }); + const warningMessages = xyVisualization.getWarningMessages!( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: ['b'], + }, + ], + }, + frame + ); + expect(warningMessages).toHaveLength(1); + expect(warningMessages && warningMessages[0]).toMatchInlineSnapshot(` + + + Label B + + contains array values. Your visualization may not render as expected. + + `); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index f0dcaf589b1c4..ebf80c61e0cd1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -22,6 +22,7 @@ import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; +import { getColumnToLabelMap } from './state_helpers'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -371,6 +372,37 @@ export const getXyVisualization = ({ return errors.length ? errors : undefined; }, + + getWarningMessages(state, frame) { + if (state?.layers.length === 0 || !frame.activeData) { + return; + } + + const layers = state.layers; + + const filteredLayers = layers.filter(({ accessors }: LayerConfig) => accessors.length > 0); + const accessorsWithArrayValues = []; + for (const layer of filteredLayers) { + const { layerId, accessors } = layer; + const rows = frame.activeData[layerId] && frame.activeData[layerId].rows; + if (!rows) { + break; + } + const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layerId]); + for (const accessor of accessors) { + const hasArrayValues = rows.some((row) => Array.isArray(row[accessor])); + if (hasArrayValues) { + accessorsWithArrayValues.push(columnToLabel[accessor]); + } + } + } + return accessorsWithArrayValues.map((label) => ( + <> + {label} contains array values. Your visualization may not render as + expected. + + )); + }, }); function validateLayersForDimension( diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index aef8b1b3d7076..43c56af7f71bc 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -5,8 +5,9 @@ */ import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; -import { ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; +import { RequestHandlerContext, ElasticsearchClient } from 'src/core/server'; import { CoreSetup, Logger } from 'src/core/server'; import { IndexPattern, IndexPatternsService } from 'src/plugins/data/common'; import { BASE_API_URL } from '../../common'; @@ -68,7 +69,7 @@ export async function existingFieldsRoute(setup: CoreSetup, logger.info( `Field existence check failed: ${isBoomError(e) ? e.output.payload.message : e.message}` ); - if (e.status === 404) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { return res.notFound({ body: e.message }); } if (isBoomError(e)) { @@ -111,7 +112,7 @@ async function fetchFieldExistence({ fromDate, toDate, dslQuery, - client: context.core.elasticsearch.legacy.client, + client: context.core.elasticsearch.client.asCurrentUser, index: indexPattern.title, timeFieldName: timeFieldName || indexPattern.timeFieldName, fields, @@ -149,7 +150,7 @@ async function fetchIndexPatternStats({ toDate, fields, }: { - client: ILegacyScopedClusterClient; + client: ElasticsearchClient; index: string; dslQuery: object; timeFieldName?: string; @@ -179,7 +180,7 @@ async function fetchIndexPatternStats({ }; const scriptedFields = fields.filter((f) => f.isScript); - const result = await client.callAsCurrentUser('search', { + const { body: result } = await client.search({ index, body: { size: SAMPLE_SIZE, diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index e0f1e05ed970d..21dfb90ec0ff4 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -5,6 +5,7 @@ */ import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; @@ -47,7 +48,7 @@ export async function initFieldsRoute(setup: CoreSetup) { }, }, async (context, req, res) => { - const requestClient = context.core.elasticsearch.legacy.client; + const requestClient = context.core.elasticsearch.client.asCurrentUser; const { fromDate, toDate, timeFieldName, field, dslQuery } = req.body; try { @@ -71,18 +72,18 @@ export async function initFieldsRoute(setup: CoreSetup) { }, }; - const search = (aggs: unknown) => - requestClient.callAsCurrentUser('search', { + const search = async (aggs: unknown) => { + const { body: result } = await requestClient.search({ index: req.params.indexPatternTitle, + track_total_hits: true, body: { query, aggs, }, - // The hits total changed in 7.0 from number to object, unless this flag is set - // this is a workaround for elasticsearch response types that are from 6.x - restTotalHitsAsInt: true, size: 0, }); + return result; + }; if (field.type === 'number') { return res.ok({ @@ -98,7 +99,7 @@ export async function initFieldsRoute(setup: CoreSetup) { body: await getStringSamples(search, field), }); } catch (e) { - if (e.status === 404) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { return res.notFound(); } if (e.isBoom) { @@ -142,8 +143,7 @@ export async function getNumberHistogram( const minMaxResult = (await aggSearchWithBody(searchBody)) as ESSearchResponse< unknown, - { body: { aggs: typeof searchBody } }, - { restTotalHitsAsInt: true } + { body: { aggs: typeof searchBody } } >; const minValue = minMaxResult.aggregations!.sample.min_value.value; @@ -164,7 +164,7 @@ export async function getNumberHistogram( if (histogramInterval === 0) { return { - totalDocuments: minMaxResult.hits.total, + totalDocuments: minMaxResult.hits.total.value, sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, sampledDocuments: minMaxResult.aggregations!.sample.doc_count, topValues: topValuesBuckets, @@ -187,12 +187,11 @@ export async function getNumberHistogram( }; const histogramResult = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< unknown, - { body: { aggs: typeof histogramBody } }, - { restTotalHitsAsInt: true } + { body: { aggs: typeof histogramBody } } >; return { - totalDocuments: minMaxResult.hits.total, + totalDocuments: minMaxResult.hits.total.value, sampledDocuments: minMaxResult.aggregations!.sample.doc_count, sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, histogram: { @@ -227,12 +226,11 @@ export async function getStringSamples( }; const topValuesResult = (await aggSearchWithBody(topValuesBody)) as ESSearchResponse< unknown, - { body: { aggs: typeof topValuesBody } }, - { restTotalHitsAsInt: true } + { body: { aggs: typeof topValuesBody } } >; return { - totalDocuments: topValuesResult.hits.total, + totalDocuments: topValuesResult.hits.total.value, sampledDocuments: topValuesResult.aggregations!.sample.doc_count, sampledValues: topValuesResult.aggregations!.sample.sample_count.value!, topValues: { @@ -275,12 +273,11 @@ export async function getDateHistogram( }; const results = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< unknown, - { body: { aggs: typeof histogramBody } }, - { restTotalHitsAsInt: true } + { body: { aggs: typeof histogramBody } } >; return { - totalDocuments: results.hits.total, + totalDocuments: results.hits.total.value, histogram: { buckets: results.aggregations!.histo.buckets.map((bucket) => ({ count: bucket.doc_count, diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index 820e32509923e..2bd891e7c1376 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -5,6 +5,7 @@ */ import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { BASE_API_URL } from '../../common'; @@ -71,7 +72,7 @@ export async function initLensUsageRoute(setup: CoreSetup) return res.ok({ body: {} }); } catch (e) { - if (e.status === 404) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { return res.notFound(); } if (e.isBoom) { diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index 014193fb6566e..0fd797bba68e4 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, CoreSetup, Logger } from 'kibana/server'; +import { CoreSetup, Logger, ElasticsearchClient } from 'kibana/server'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import moment from 'moment'; @@ -69,11 +69,12 @@ async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContra export async function getDailyEvents( kibanaIndex: string, - callCluster: LegacyAPICaller + getEsClient: () => Promise ): Promise<{ byDate: Record>; suggestionsByDate: Record>; }> { + const esClient = await getEsClient(); const aggs = { daily: { date_histogram: { @@ -114,15 +115,10 @@ export async function getDailyEvents( }, }; - const metrics: ESSearchResponse< - unknown, - { - body: { aggs: typeof aggs }; - }, - { restTotalHitsAsInt: true } - > = await callCluster('search', { + const { body: metrics } = await esClient.search< + ESSearchResponse + >({ index: kibanaIndex, - rest_total_hits_as_int: true, body: { query: { bool: { @@ -156,9 +152,9 @@ export async function getDailyEvents( }); // Always delete old date because we don't report it - await callCluster('deleteByQuery', { + await esClient.deleteByQuery({ index: kibanaIndex, - waitForCompletion: true, + wait_for_completion: true, body: { query: { bool: { @@ -184,9 +180,9 @@ export function telemetryTaskRunner( ) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = async (...args: Parameters) => { + const getEsClient = async () => { const [coreStart] = await core.getStartServices(); - return coreStart.elasticsearch.legacy.client.callAsInternalUser(...args); + return coreStart.elasticsearch.client.asInternalUser; }; return { @@ -194,8 +190,8 @@ export function telemetryTaskRunner( const kibanaIndex = (await config.pipe(first()).toPromise()).kibana.index; return Promise.all([ - getDailyEvents(kibanaIndex, callCluster), - getVisualizationCounts(callCluster, kibanaIndex), + getDailyEvents(kibanaIndex, getEsClient), + getVisualizationCounts(getEsClient, kibanaIndex), ]) .then(([lensTelemetry, lensVisualizations]) => { return { diff --git a/x-pack/plugins/lens/server/usage/visualization_counts.ts b/x-pack/plugins/lens/server/usage/visualization_counts.ts index c9cd4aff72b2b..f6858ef941b78 100644 --- a/x-pack/plugins/lens/server/usage/visualization_counts.ts +++ b/x-pack/plugins/lens/server/usage/visualization_counts.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { LensVisualizationUsage } from './types'; export async function getVisualizationCounts( - callCluster: LegacyAPICaller, + getEsClient: () => Promise, kibanaIndex: string ): Promise { - const results = await callCluster('search', { + const esClient = await getEsClient(); + const { body: results } = await esClient.search({ index: kibanaIndex, - rest_total_hits_as_int: true, body: { query: { bool: { diff --git a/x-pack/plugins/license_management/jest.config.js b/x-pack/plugins/license_management/jest.config.js new file mode 100644 index 0000000000000..593b8fce47411 --- /dev/null +++ b/x-pack/plugins/license_management/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/license_management'], +}; diff --git a/x-pack/plugins/licensing/jest.config.js b/x-pack/plugins/licensing/jest.config.js new file mode 100644 index 0000000000000..72f3fd90ae5e1 --- /dev/null +++ b/x-pack/plugins/licensing/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/licensing'], +}; diff --git a/x-pack/plugins/lists/jest.config.js b/x-pack/plugins/lists/jest.config.js new file mode 100644 index 0000000000000..4d933fa20ba76 --- /dev/null +++ b/x-pack/plugins/lists/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/lists'], +}; diff --git a/x-pack/plugins/logstash/jest.config.js b/x-pack/plugins/logstash/jest.config.js new file mode 100644 index 0000000000000..52e1d7b1a6693 --- /dev/null +++ b/x-pack/plugins/logstash/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/logstash'], +}; diff --git a/x-pack/plugins/maps/jest.config.js b/x-pack/plugins/maps/jest.config.js new file mode 100644 index 0000000000000..40dea38c6f2a1 --- /dev/null +++ b/x-pack/plugins/maps/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/maps'], +}; diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index a79e5353048c8..f3241b79759a1 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -147,27 +147,23 @@ export class MapsPlugin implements Plugin { return; } - let routesInitialized = false; let isEnterprisePlus = false; + let lastLicenseId: string | undefined; const emsSettings = new EMSSettings(mapsLegacyConfig, () => isEnterprisePlus); licensing.license$.subscribe((license: ILicense) => { - const basic = license.check(APP_ID, 'basic'); - const enterprise = license.check(APP_ID, 'enterprise'); isEnterprisePlus = enterprise.state === 'valid'; - - if (basic.state === 'valid' && !routesInitialized) { - routesInitialized = true; - initRoutes( - core.http.createRouter(), - license.uid, - emsSettings, - this.kibanaVersion, - this._logger - ); - } + lastLicenseId = license.uid; }); + initRoutes( + core.http.createRouter(), + () => lastLicenseId, + emsSettings, + this.kibanaVersion, + this._logger + ); + this._initHomeData(home, core.http.basePath.prepend, emsSettings); features.registerKibanaFeature({ diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index 49d646f9a4e6d..d98259540f5e4 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -52,26 +52,32 @@ const EMPTY_EMS_CLIENT = { addQueryParams() {}, }; -export function initRoutes(router, licenseUid, emsSettings, kbnVersion, logger) { +export function initRoutes(router, getLicenseId, emsSettings, kbnVersion, logger) { let emsClient; - - if (emsSettings.isIncludeElasticMapsService()) { - emsClient = new EMSClient({ - language: i18n.getLocale(), - appVersion: kbnVersion, - appName: EMS_APP_NAME, - fileApiUrl: emsSettings.getEMSFileApiUrl(), - tileApiUrl: emsSettings.getEMSTileApiUrl(), - landingPageUrl: emsSettings.getEMSLandingPageUrl(), - fetchFunction: fetch, - }); - emsClient.addQueryParams({ license: licenseUid }); - } else { - emsClient = EMPTY_EMS_CLIENT; - } + let lastLicenseId; function getEMSClient() { - return emsSettings.isEMSEnabled() ? emsClient : EMPTY_EMS_CLIENT; + const currentLicenseId = getLicenseId(); + if (emsClient && emsSettings.isEMSEnabled() && lastLicenseId === currentLicenseId) { + return emsClient; + } + + lastLicenseId = currentLicenseId; + if (emsSettings.isIncludeElasticMapsService()) { + emsClient = new EMSClient({ + language: i18n.getLocale(), + appVersion: kbnVersion, + appName: EMS_APP_NAME, + fileApiUrl: emsSettings.getEMSFileApiUrl(), + tileApiUrl: emsSettings.getEMSTileApiUrl(), + landingPageUrl: emsSettings.getEMSLandingPageUrl(), + fetchFunction: fetch, + }); + emsClient.addQueryParams({ license: currentLicenseId }); + return emsClient; + } else { + return EMPTY_EMS_CLIENT; + } } router.get( diff --git a/x-pack/plugins/ml/jest.config.js b/x-pack/plugins/ml/jest.config.js new file mode 100644 index 0000000000000..bd77ac77c5e97 --- /dev/null +++ b/x-pack/plugins/ml/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/ml'], +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx index 6bfd7a66331df..38eb7abc16814 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx @@ -91,8 +91,7 @@ export const DecisionPathChart = ({ maxDomain, baseline, }: DecisionPathChartProps) => { - // adjust the height so it's compact for items with more features - const baselineData: LineAnnotationDatum[] | undefined = useMemo( + const regressionBaselineData: LineAnnotationDatum[] | undefined = useMemo( () => baseline && isRegressionFeatureImportanceBaseline(baseline) ? [ @@ -111,6 +110,19 @@ export const DecisionPathChart = ({ : undefined, [baseline] ); + const xAxisLabel = regressionBaselineData + ? i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.decisionPathLinePredictionTitle', + { + defaultMessage: 'Prediction', + } + ) + : i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.decisionPathLinePredictionProbabilityTitle', + { + defaultMessage: 'Prediction probability', + } + ); // if regression, guarantee up to num_precision significant digits without having it in scientific notation // if classification, hide the numeric values since we only want to show the path const tickFormatter = useCallback((d) => formatSingleValue(d, '').toString(), []); @@ -121,11 +133,11 @@ export const DecisionPathChart = ({ size={{ height: DECISION_PATH_MARGIN + decisionPathData.length * DECISION_PATH_ROW_HEIGHT }} > - {baselineData && ( + {regressionBaselineData && ( @@ -137,8 +149,8 @@ export const DecisionPathChart = ({ title={i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.decisionPathXAxisTitle', { - defaultMessage: "Prediction for '{predictionFieldName}'", - values: { predictionFieldName }, + defaultMessage: "{xAxisLabel} for '{predictionFieldName}'", + values: { predictionFieldName, xAxisLabel }, } )} showGridLines={false} @@ -156,12 +168,7 @@ export const DecisionPathChart = ({ /x-pack/plugins/monitoring'], +}; diff --git a/x-pack/plugins/observability/jest.config.js b/x-pack/plugins/observability/jest.config.js index cbf9a86360b89..54bb6c96ddce9 100644 --- a/x-pack/plugins/observability/jest.config.js +++ b/x-pack/plugins/observability/jest.config.js @@ -4,37 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// This is an APM-specific Jest configuration which overrides the x-pack -// configuration. It's intended for use in development and does not run in CI, -// which runs the entire x-pack suite. Run `npx jest`. - -require('../../../src/setup_node_env'); - -const { createJestConfig } = require('../../dev-tools/jest/create_jest_config'); -const { resolve } = require('path'); - -const rootDir = resolve(__dirname, '.'); -const xPackKibanaDirectory = resolve(__dirname, '../..'); -const kibanaDirectory = resolve(__dirname, '../../..'); - -const jestConfig = createJestConfig({ - kibanaDirectory, - rootDir, - xPackKibanaDirectory, -}); - module.exports = { - ...jestConfig, - reporters: ['default'], - roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], - collectCoverage: true, - collectCoverageFrom: [ - ...jestConfig.collectCoverageFrom, - '**/*.{js,mjs,jsx,ts,tsx}', - '!**/*.stories.{js,mjs,ts,tsx}', - '!**/target/**', - '!**/typings/**', - ], - coverageDirectory: `${rootDir}/target/coverage/jest`, - coverageReporters: ['html'], + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/observability'], }; diff --git a/x-pack/plugins/painless_lab/jest.config.js b/x-pack/plugins/painless_lab/jest.config.js new file mode 100644 index 0000000000000..9e0e0fe792285 --- /dev/null +++ b/x-pack/plugins/painless_lab/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/painless_lab'], +}; diff --git a/x-pack/plugins/remote_clusters/jest.config.js b/x-pack/plugins/remote_clusters/jest.config.js new file mode 100644 index 0000000000000..81728f99934bc --- /dev/null +++ b/x-pack/plugins/remote_clusters/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/remote_clusters'], +}; diff --git a/x-pack/plugins/reporting/jest.config.js b/x-pack/plugins/reporting/jest.config.js new file mode 100644 index 0000000000000..1faa533c09c7b --- /dev/null +++ b/x-pack/plugins/reporting/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/reporting'], +}; diff --git a/x-pack/plugins/rollup/jest.config.js b/x-pack/plugins/rollup/jest.config.js new file mode 100644 index 0000000000000..edb3ba860dc74 --- /dev/null +++ b/x-pack/plugins/rollup/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/rollup'], +}; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js index 66ecb37d68439..0fd7f62511bdb 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js @@ -310,6 +310,10 @@ export class JobTable extends Component { this.toggleItem(id); }} data-test-subj={`indexTableRowCheckbox-${id}`} + aria-label={i18n.translate('xpack.rollupJobs.jobTable.selectRow', { + defaultMessage: 'Select this row {id}', + values: { id }, + })} /> @@ -380,6 +384,9 @@ export class JobTable extends Component { checked={this.areAllItemsSelected()} onChange={this.toggleAll} type="inList" + aria-label={i18n.translate('xpack.rollupJobs.jobTable.selectAllRows', { + defaultMessage: 'Select all rows', + })} /> {this.buildHeader()} diff --git a/x-pack/plugins/runtime_fields/jest.config.js b/x-pack/plugins/runtime_fields/jest.config.js new file mode 100644 index 0000000000000..9c4ec56593c8b --- /dev/null +++ b/x-pack/plugins/runtime_fields/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/runtime_fields'], +}; diff --git a/x-pack/plugins/saved_objects_tagging/jest.config.js b/x-pack/plugins/saved_objects_tagging/jest.config.js new file mode 100644 index 0000000000000..82931258a4055 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/saved_objects_tagging'], +}; diff --git a/x-pack/plugins/searchprofiler/jest.config.js b/x-pack/plugins/searchprofiler/jest.config.js new file mode 100644 index 0000000000000..b80a9924c4fd2 --- /dev/null +++ b/x-pack/plugins/searchprofiler/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/searchprofiler'], +}; diff --git a/x-pack/plugins/security/jest.config.js b/x-pack/plugins/security/jest.config.js new file mode 100644 index 0000000000000..26fee5a787850 --- /dev/null +++ b/x-pack/plugins/security/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/security'], +}; diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index 35703212762fd..3eff6edef33bc 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -222,6 +222,7 @@ export class LoginPage extends Component { http={this.props.http} notifications={this.props.notifications} selector={selector} + // @ts-expect-error Map.get is ok with getting `undefined` infoMessage={infoMessageMap.get(query[LOGOUT_REASON_QUERY_STRING_PARAMETER]?.toString())} loginAssistanceMessage={this.props.loginAssistanceMessage} loginHelp={loginHelp} diff --git a/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts b/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts index 529e8a8aa6e9c..0a43d8dd6973a 100644 --- a/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts +++ b/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts @@ -22,133 +22,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen }, }); - /** - * Perform a [shield.changePassword](Change the password of a user) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - The username of the user to change the password for - */ - shield.changePassword = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - urls: [ - { - fmt: '/_security/user/<%=username%>/_password', - req: { - username: { - type: 'string', - required: false, - }, - }, - }, - { - fmt: '/_security/user/_password', - }, - ], - needBody: true, - method: 'POST', - }); - - /** - * Perform a [shield.clearCachedRealms](Clears the internal user caches for specified realms) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.usernames - Comma-separated list of usernames to clear from the cache - * @param {String} params.realms - Comma-separated list of realms to clear - */ - shield.clearCachedRealms = ca({ - params: { - usernames: { - type: 'string', - required: false, - }, - }, - url: { - fmt: '/_security/realm/<%=realms%>/_clear_cache', - req: { - realms: { - type: 'string', - required: true, - }, - }, - }, - method: 'POST', - }); - - /** - * Perform a [shield.clearCachedRoles](Clears the internal caches for specified roles) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.name - Role name - */ - shield.clearCachedRoles = ca({ - params: {}, - url: { - fmt: '/_security/role/<%=name%>/_clear_cache', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - method: 'POST', - }); - - /** - * Perform a [shield.deleteRole](Remove a role from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.name - Role name - */ - shield.deleteRole = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - url: { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - method: 'DELETE', - }); - - /** - * Perform a [shield.deleteUser](Remove a user from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - username - */ - shield.deleteUser = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - url: { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'string', - required: true, - }, - }, - }, - method: 'DELETE', - }); - /** * Perform a [shield.getRole](Retrieve one or more roles from the native shield realm) request * @@ -173,30 +46,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen ], }); - /** - * Perform a [shield.getUser](Retrieve one or more users from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String, String[], Boolean} params.username - A comma-separated list of usernames - */ - shield.getUser = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'list', - required: false, - }, - }, - }, - { - fmt: '/_security/user', - }, - ], - }); - /** * Perform a [shield.putRole](Update or create a role for the native shield realm) request * @@ -249,19 +98,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen method: 'PUT', }); - /** - * Perform a [shield.getUserPrivileges](Retrieve a user's list of privileges) request - * - */ - shield.getUserPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/_privileges', - }, - ], - }); - /** * Asks Elasticsearch to prepare SAML authentication request to be sent to * the 3rd-party SAML identity provider. @@ -489,36 +325,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen }, }); - shield.getBuiltinPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/privilege/_builtin', - }, - ], - }); - - /** - * Gets API keys in Elasticsearch - * @param {boolean} owner A boolean flag that can be used to query API keys owned by the currently authenticated user. - * Defaults to false. The realm_name or username parameters cannot be specified when this parameter is set to true as - * they are assumed to be the currently authenticated ones. - */ - shield.getAPIKeys = ca({ - method: 'GET', - urls: [ - { - fmt: `/_security/api_key?owner=<%=owner%>`, - req: { - owner: { - type: 'boolean', - required: true, - }, - }, - }, - ], - }); - /** * Creates an API key in Elasticsearch for the current user. * @@ -591,64 +397,4 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen fmt: '/_security/delegate_pki', }, }); - - /** - * Retrieves all configured role mappings. - * - * @returns {{ [roleMappingName]: { enabled: boolean; roles: string[]; rules: Record} }} - */ - shield.getRoleMappings = ca({ - method: 'GET', - urls: [ - { - fmt: '/_security/role_mapping', - }, - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); - - /** - * Saves the specified role mapping. - */ - shield.saveRoleMapping = ca({ - method: 'POST', - needBody: true, - urls: [ - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); - - /** - * Deletes the specified role mapping. - */ - shield.deleteRoleMapping = ca({ - method: 'DELETE', - urls: [ - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 17f2480026cc7..d6fe1356ce145 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -230,7 +230,6 @@ export class Plugin { basePath: core.http.basePath, httpResources: core.http.resources, logger: this.initializerContext.logger.get('routes'), - clusterClient, config, authc: this.authc, authz, diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts index 7968402cd2176..3a22b9fe003a1 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -4,115 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; -import { LicenseCheck } from '../../../../licensing/server'; +import Boom from '@hapi/boom'; +import { + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from '../../../../../../src/core/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; -import Boom from '@hapi/boom'; -import { defineEnabledApiKeysRoutes } from './enabled'; -import { APIKeys } from '../../authentication/api_keys'; -interface TestOptions { - licenseCheckResult?: LicenseCheck; - apiResponse?: () => Promise; - asserts: { statusCode: number; result?: Record }; -} +import { defineEnabledApiKeysRoutes } from './enabled'; +import { Authentication } from '../../authentication'; describe('API keys enabled', () => { - const enabledApiKeysTest = ( - description: string, - { licenseCheckResult = { state: 'valid' }, apiResponse, asserts }: TestOptions - ) => { - test(description, async () => { - const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const apiKeys = new APIKeys({ - logger: mockRouteDefinitionParams.logger, - clusterClient: mockRouteDefinitionParams.clusterClient, - license: mockRouteDefinitionParams.license, - }); - - mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => - apiKeys.areAPIKeysEnabled() + function getMockContext( + licenseCheckResult: { state: string; message?: string } = { state: 'valid' } + ) { + return ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + } + + let routeHandler: RequestHandler; + let authc: jest.Mocked; + beforeEach(() => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + authc = mockRouteDefinitionParams.authc; + + defineEnabledApiKeysRoutes(mockRouteDefinitionParams); + + const [, apiKeyRouteHandler] = mockRouteDefinitionParams.router.get.mock.calls.find( + ([{ path }]) => path === '/internal/security/api_key/_enabled' + )!; + routeHandler = apiKeyRouteHandler; + }); + + describe('failure', () => { + test('returns result of license checker', async () => { + const mockContext = getMockContext({ state: 'invalid', message: 'test forbidden message' }); + const response = await routeHandler( + mockContext, + httpServerMock.createKibanaRequest(), + kibanaResponseFactory ); - if (apiResponse) { - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation(apiResponse); - } - - defineEnabledApiKeysRoutes(mockRouteDefinitionParams); - const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; - - const headers = { authorization: 'foo' }; - const mockRequest = httpServerMock.createKibanaRequest({ - method: 'get', - path: '/internal/security/api_key/_enabled', - headers, - }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; - - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); - - if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.invalidateAPIKey', - { - body: { - id: expect.any(String), - }, - } - ); - } else { - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); - } + expect(response.status).toBe(403); + expect(response.payload).toEqual({ message: 'test forbidden message' }); expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); - }; - describe('failure', () => { - enabledApiKeysTest('returns result of license checker', { - licenseCheckResult: { state: 'invalid', message: 'test forbidden message' }, - asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, - }); + test('returns error from cluster client', async () => { + const error = Boom.notAcceptable('test not acceptable message'); + authc.areAPIKeysEnabled.mockRejectedValue(error); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); - const error = Boom.notAcceptable('test not acceptable message'); - enabledApiKeysTest('returns error from cluster client', { - apiResponse: async () => { - throw error; - }, - asserts: { statusCode: 406, result: error }, + expect(response.status).toBe(406); + expect(response.payload).toEqual(error); }); }); describe('success', () => { - enabledApiKeysTest('returns true if API Keys are enabled', { - apiResponse: async () => ({}), - asserts: { - statusCode: 200, - result: { - apiKeysEnabled: true, - }, - }, + test('returns true if API Keys are enabled', async () => { + authc.areAPIKeysEnabled.mockResolvedValue(true); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ apiKeysEnabled: true }); }); - enabledApiKeysTest('returns false if API Keys are disabled', { - apiResponse: async () => { - const error = new Error(); - (error as any).body = { - error: { 'disabled.feature': 'api_keys' }, - }; - throw error; - }, - asserts: { - statusCode: 200, - result: { - apiKeysEnabled: false, - }, - }, + + test('returns false if API Keys are disabled', async () => { + authc.areAPIKeysEnabled.mockResolvedValue(false); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ apiKeysEnabled: false }); }); }); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.test.ts b/x-pack/plugins/security/server/routes/api_keys/get.test.ts index cb991fb2f5aac..cc9e9a68e6e36 100644 --- a/x-pack/plugins/security/server/routes/api_keys/get.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/get.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineGetApiKeysRoutes } from './get'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock, coreMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import Boom from '@hapi/boom'; @@ -26,11 +26,15 @@ describe('Get API keys', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getApiKey.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetApiKeysRoutes(mockRouteDefinitionParams); @@ -43,22 +47,15 @@ describe('Get API keys', () => { query: { isAdmin: isAdmin.toString() }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getAPIKeys', - { owner: !isAdmin } - ); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getApiKey + ).toHaveBeenCalledWith({ owner: !isAdmin }); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.ts b/x-pack/plugins/security/server/routes/api_keys/get.ts index 6e98b4b098405..b0c6ec090a0e5 100644 --- a/x-pack/plugins/security/server/routes/api_keys/get.ts +++ b/x-pack/plugins/security/server/routes/api_keys/get.ts @@ -10,7 +10,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineGetApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetApiKeysRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key', @@ -28,11 +28,11 @@ export function defineGetApiKeysRoutes({ router, clusterClient }: RouteDefinitio createLicensedRouteHandler(async (context, request, response) => { try { const isAdmin = request.query.isAdmin === 'true'; - const { api_keys: apiKeys } = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getAPIKeys', { owner: !isAdmin })) as { api_keys: ApiKey[] }; + const apiResponse = await context.core.elasticsearch.client.asCurrentUser.security.getApiKey<{ + api_keys: ApiKey[]; + }>({ owner: !isAdmin }); - const validKeys = apiKeys.filter(({ invalidated }) => !invalidated); + const validKeys = apiResponse.body.api_keys.filter(({ invalidated }) => !invalidated); return response.ok({ body: { apiKeys: validKeys } }); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts index 88e52f735395d..9ac41fdfa7483 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts @@ -6,18 +6,18 @@ import Boom from '@hapi/boom'; import { Type } from '@kbn/config-schema'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineInvalidateApiKeysRoutes } from './invalidate'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; interface TestOptions { licenseCheckResult?: LicenseCheck; apiResponses?: Array<() => Promise>; payload?: Record; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[] }; } describe('Invalidate API keys', () => { @@ -27,10 +27,15 @@ describe('Invalidate API keys', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; + for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey.mockImplementationOnce( + (async () => ({ body: await apiResponse() })) as any + ); } defineInvalidateApiKeysRoutes(mockRouteDefinitionParams); @@ -43,9 +48,6 @@ describe('Invalidate API keys', () => { body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); @@ -53,13 +55,10 @@ describe('Invalidate API keys', () => { if (Array.isArray(asserts.apiArguments)) { for (const apiArguments of asserts.apiArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( - mockRequest - ); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey + ).toHaveBeenCalledWith(apiArguments); } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); @@ -128,7 +127,7 @@ describe('Invalidate API keys', () => { isAdmin: true, }, asserts: { - apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV' } }], statusCode: 200, result: { itemsInvalidated: [], @@ -152,7 +151,7 @@ describe('Invalidate API keys', () => { isAdmin: true, }, asserts: { - apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV' } }], statusCode: 200, result: { itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], @@ -168,9 +167,7 @@ describe('Invalidate API keys', () => { isAdmin: false, }, asserts: { - apiArguments: [ - ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV', owner: true } }], - ], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV', owner: true } }], statusCode: 200, result: { itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], @@ -195,8 +192,8 @@ describe('Invalidate API keys', () => { }, asserts: { apiArguments: [ - ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }], - ['shield.invalidateAPIKey', { body: { id: 'ab8If24B1bKsmSLTAhNC' } }], + { body: { id: 'si8If24B1bKsmSLTAhJV' } }, + { body: { id: 'ab8If24B1bKsmSLTAhNC' } }, ], statusCode: 200, result: { diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts index dd472c0b60cbc..3977954197007 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts @@ -15,7 +15,7 @@ interface ResponseType { errors: Array & { error: Error }>; } -export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineInvalidateApiKeysRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/api_key/invalidate', @@ -28,8 +28,6 @@ export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDe }, createLicensedRouteHandler(async (context, request, response) => { try { - const scopedClusterClient = clusterClient.asScoped(request); - // Invalidate all API keys in parallel. const invalidationResult = ( await Promise.all( @@ -41,7 +39,9 @@ export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDe } // Send the request to invalidate the API key and return an error if it could not be deleted. - await scopedClusterClient.callAsCurrentUser('shield.invalidateAPIKey', { body }); + await context.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey({ + body, + }); return { key, error: undefined }; } catch (error) { return { key, error: wrapError(error) }; diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts index ecc3d32e20aec..b06d1329dc1db 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -6,23 +6,17 @@ import Boom from '@hapi/boom'; import { LicenseCheck } from '../../../../licensing/server'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import { defineCheckPrivilegesRoutes } from './privileges'; -import { APIKeys } from '../../authentication/api_keys'; interface TestOptions { licenseCheckResult?: LicenseCheck; - callAsInternalUserResponses?: Array<() => Promise>; - callAsCurrentUserResponses?: Array<() => Promise>; - asserts: { - statusCode: number; - result?: Record; - callAsInternalUserAPIArguments?: unknown[][]; - callAsCurrentUserAPIArguments?: unknown[][]; - }; + areAPIKeysEnabled?: boolean; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown }; } describe('Check API keys privileges', () => { @@ -30,32 +24,23 @@ describe('Check API keys privileges', () => { description: string, { licenseCheckResult = { state: 'valid' }, - callAsInternalUserResponses = [], - callAsCurrentUserResponses = [], + areAPIKeysEnabled = true, + apiResponse, asserts, }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const apiKeys = new APIKeys({ - logger: mockRouteDefinitionParams.logger, - clusterClient: mockRouteDefinitionParams.clusterClient, - license: mockRouteDefinitionParams.license, - }); - - mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => - apiKeys.areAPIKeysEnabled() - ); + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockResolvedValue(areAPIKeysEnabled); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of callAsCurrentUserResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); - } - for (const apiResponse of callAsInternalUserResponses) { - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementationOnce( - apiResponse + if (apiResponse) { + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockImplementation( + (async () => ({ body: await apiResponse() })) as any ); } @@ -68,33 +53,15 @@ describe('Check API keys privileges', () => { path: '/internal/security/api_key/privileges', headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.callAsCurrentUserAPIArguments)) { - for (const apiArguments of asserts.callAsCurrentUserAPIArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( - mockRequest - ); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); - } - - if (Array.isArray(asserts.callAsInternalUserAPIArguments)) { - for (const apiArguments of asserts.callAsInternalUserAPIArguments) { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( - ...apiArguments - ); - } - } else { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).not.toHaveBeenCalled(); + if (asserts.apiArguments) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges + ).toHaveBeenCalledWith(asserts.apiArguments); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); @@ -109,22 +76,13 @@ describe('Check API keys privileges', () => { const error = Boom.notAcceptable('test not acceptable message'); getPrivilegesTest('returns error from cluster client', { - callAsCurrentUserResponses: [ - async () => { - throw error; - }, - ], - callAsInternalUserResponses: [async () => {}], + apiResponse: async () => { + throw error; + }, asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 406, result: error, }, @@ -133,40 +91,17 @@ describe('Check API keys privileges', () => { describe('success', () => { getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [ - async () => ({ - api_keys: [ - { - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved', - }, - ], - }), - ], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: true, canManage: true }, }, @@ -175,36 +110,18 @@ describe('Check API keys privileges', () => { getPrivilegesTest( 'returns areApiKeysEnabled=false when API Keys are disabled in Elasticsearch', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [ - async () => { - const error = new Error(); - (error as any).body = { - error: { - 'disabled.feature': 'api_keys', - }, - }; - throw error; - }, - ], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, + index: {}, + application: {}, + }), + areAPIKeysEnabled: false, asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: false, isAdmin: true, canManage: true }, }, @@ -212,52 +129,34 @@ describe('Check API keys privileges', () => { ); getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [async () => ({})], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: false, canManage: false }, }, }); getPrivilegesTest('returns canManage=true when user can manage their own API Keys', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [async () => ({})], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: false, canManage: true }, }, diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts index 9cccb96752772..dd5d81060c7e5 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts @@ -8,11 +8,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCheckPrivilegesRoutes({ - router, - clusterClient, - authc, -}: RouteDefinitionParams) { +export function defineCheckPrivilegesRoutes({ router, authc }: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key/privileges', @@ -20,19 +16,25 @@ export function defineCheckPrivilegesRoutes({ }, createLicensedRouteHandler(async (context, request, response) => { try { - const scopedClusterClient = clusterClient.asScoped(request); - const [ { - cluster: { - manage_security: manageSecurity, - manage_api_key: manageApiKey, - manage_own_api_key: manageOwnApiKey, + body: { + cluster: { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + }, }, }, areApiKeysEnabled, ] = await Promise.all([ - scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { + context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges<{ + cluster: { + manage_security: boolean; + manage_api_key: boolean; + manage_own_api_key: boolean; + }; + }>({ body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, }), authc.areAPIKeysEnabled(), diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts index 08cd3ba487b0b..39e6a4838d34d 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts @@ -7,13 +7,13 @@ import { BuiltinESPrivileges } from '../../../../common/model'; import { RouteDefinitionParams } from '../..'; -export function defineGetBuiltinPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetBuiltinPrivilegesRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/esPrivileges/builtin', validate: false }, async (context, request, response) => { - const privileges: BuiltinESPrivileges = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getBuiltinPrivileges'); + const { + body: privileges, + } = await context.core.elasticsearch.client.asCurrentUser.security.getBuiltinPrivileges(); // Exclude the `none` privilege, as it doesn't make sense as an option within the Kibana UI privileges.cluster = privileges.cluster.filter((privilege) => privilege !== 'none'); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts index 9f5ec635f56cd..5143b727fcb4e 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts @@ -5,14 +5,11 @@ */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineDeleteRolesRoutes } from './delete'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; interface TestOptions { @@ -29,11 +26,15 @@ describe('DELETE role', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineDeleteRolesRoutes(mockRouteDefinitionParams); @@ -46,22 +47,15 @@ describe('DELETE role', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.deleteRole', - { name } - ); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRole + ).toHaveBeenCalledWith({ name }); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts index eb56143288747..b877aaf6abd77 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -9,7 +9,7 @@ import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; -export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineDeleteRolesRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/api/security/role/{name}', @@ -19,7 +19,7 @@ export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefiniti }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient.asScoped(request).callAsCurrentUser('shield.deleteRole', { + await context.core.elasticsearch.client.asCurrentUser.security.deleteRole({ name: request.params.name, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts index b25b13b9fc04a..a6090ee405329 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineGetRolesRoutes } from './get'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; const application = 'kibana-.kibana'; @@ -32,11 +29,15 @@ describe('GET role', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetRolesRoutes(mockRouteDefinitionParams); @@ -49,22 +50,17 @@ describe('GET role', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.getRole', { - name, - }); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalledWith({ name }); } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); }; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index bf1140e2e6fd1..ce4a622d30e61 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -8,9 +8,9 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../..'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; -import { transformElasticsearchRoleToRole } from './model'; +import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; -export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { +export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) { router.get( { path: '/api/security/role/{name}', @@ -20,9 +20,11 @@ export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefi }, createLicensedRouteHandler(async (context, request, response) => { try { - const elasticsearchRoles = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole', { name: request.params.name }); + const { + body: elasticsearchRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record + >({ name: request.params.name }); const elasticsearchRole = elasticsearchRoles[request.params.name]; if (elasticsearchRole) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts index 30e0c52c4c443..b3a855b2e0ae7 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineGetAllRolesRoutes } from './get_all'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; const application = 'kibana-.kibana'; @@ -32,11 +29,15 @@ describe('GET all roles', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetAllRolesRoutes(mockRouteDefinitionParams); @@ -48,19 +49,15 @@ describe('GET all roles', () => { path: '/api/security/role', headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.getRole'); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalled(); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 24be6c60e4b12..21521dd6dbae3 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -9,14 +9,16 @@ import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; -export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { +export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams) { router.get( { path: '/api/security/role', validate: false }, createLicensedRouteHandler(async (context, request, response) => { try { - const elasticsearchRoles = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole')) as Record; + const { + body: elasticsearchRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record + >(); // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. return response.ok({ diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 811ea080b4316..779e1a7fab177 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -5,15 +5,12 @@ */ import { Type } from '@kbn/config-schema'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { GLOBAL_RESOURCE } from '../../../../common/constants'; import { definePutRolesRoutes } from './put'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; import { KibanaFeature } from '../../../../../features/server'; import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock'; @@ -47,35 +44,43 @@ const privilegeMap = { interface TestOptions { name: string; licenseCheckResult?: LicenseCheck; - apiResponses?: Array<() => Promise>; + apiResponses?: { + get: () => Promise; + put: () => Promise; + }; payload?: Record; asserts: { statusCode: number; result?: Record; - apiArguments?: unknown[][]; + apiArguments?: { get: unknown[]; put: unknown[] }; recordSubFeaturePrivilegeUsage?: boolean; }; } const putRoleTest = ( description: string, - { - name, - payload, - licenseCheckResult = { state: 'valid' }, - apiResponses = [], - asserts, - }: TestOptions + { name, payload, licenseCheckResult = { state: 'valid' }, apiResponses, asserts }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; + + if (apiResponses?.get) { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementationOnce( + (async () => ({ body: await apiResponses?.get() })) as any + ); + } + + if (apiResponses?.put) { + mockContext.core.elasticsearch.client.asCurrentUser.security.putRole.mockImplementationOnce( + (async () => ({ body: await apiResponses?.put() })) as any + ); } mockRouteDefinitionParams.getFeatureUsageService.mockReturnValue( @@ -131,21 +136,20 @@ const putRoleTest = ( body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + if (asserts.apiArguments?.get) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalledWith(...asserts.apiArguments?.get); + } + if (asserts.apiArguments?.put) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRole + ).toHaveBeenCalledWith(...asserts.apiArguments?.put); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); @@ -208,12 +212,11 @@ describe('PUT role', () => { putRoleTest(`creates empty role`, { name: 'foo-role', payload: {}, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -224,7 +227,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -239,12 +242,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -261,7 +263,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -279,12 +281,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -301,7 +302,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -317,12 +318,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -339,7 +339,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -383,12 +383,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -426,7 +425,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -473,8 +472,8 @@ describe('PUT role', () => { }, ], }, - apiResponses: [ - async () => ({ + apiResponses: { + get: async () => ({ 'foo-role': { metadata: { bar: 'old-metadata', @@ -504,13 +503,12 @@ describe('PUT role', () => { ], }, }), - async () => {}, - ], + put: async () => {}, + }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -548,7 +546,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -577,8 +575,8 @@ describe('PUT role', () => { }, ], }, - apiResponses: [ - async () => ({ + apiResponses: { + get: async () => ({ 'foo-role': { metadata: { bar: 'old-metadata', @@ -613,13 +611,12 @@ describe('PUT role', () => { ], }, }), - async () => {}, - ], + put: async () => {}, + }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -652,7 +649,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -670,13 +667,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: true, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -694,7 +690,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -712,13 +708,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: false, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -736,7 +731,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -754,13 +749,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: false, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -778,7 +772,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index cdedc9ac8a5eb..26c61b4ced15f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -42,7 +42,6 @@ const roleGrantsSubFeaturePrivileges = ( export function definePutRolesRoutes({ router, authz, - clusterClient, getFeatures, getFeatureUsageService, }: RouteDefinitionParams) { @@ -64,12 +63,11 @@ export function definePutRolesRoutes({ const { name } = request.params; try { - const rawRoles: Record = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole', { - name: request.params.name, - ignore: [404], - }); + const { + body: rawRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record + >({ name: request.params.name }, { ignore: [404] }); const body = transformPutPayloadToElasticsearchRole( request.body, @@ -77,11 +75,12 @@ export function definePutRolesRoutes({ rawRoles[name] ? rawRoles[name].applications : [] ); - const [features] = await Promise.all([ + const [features] = await Promise.all([ getFeatures(), - clusterClient - .asScoped(request) - .callAsCurrentUser('shield.putRole', { name: request.params.name, body }), + context.core.elasticsearch.client.asCurrentUser.security.putRole({ + name: request.params.name, + body, + }), ]); if (roleGrantsSubFeaturePrivileges(features, request.body)) { diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index fab4a71df0cb0..1df499d981632 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -5,7 +5,6 @@ */ import { - elasticsearchServiceMock, httpServiceMock, loggingSystemMock, httpResourcesMock, @@ -25,7 +24,6 @@ export const routeDefinitionParamsMock = { basePath: httpServiceMock.createBasePath(), csp: httpServiceMock.createSetupContract().csp, logger: loggingSystemMock.create().get(), - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), { isTLSEnabled: false, }), diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 079c9e8ab9ce7..db71b04b3e6f0 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -5,13 +5,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; import { KibanaFeature } from '../../../features/server'; -import { - HttpResources, - IBasePath, - ILegacyClusterClient, - IRouter, - Logger, -} from '../../../../../src/core/server'; +import { HttpResources, IBasePath, IRouter, Logger } from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { Authentication } from '../authentication'; import { AuthorizationServiceSetup } from '../authorization'; @@ -36,7 +30,6 @@ export interface RouteDefinitionParams { basePath: IBasePath; httpResources: HttpResources; logger: Logger; - clusterClient: ILegacyClusterClient; config: ConfigType; authc: Authentication; authz: AuthorizationServiceSetup; diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.test.ts b/x-pack/plugins/security/server/routes/indices/get_fields.test.ts index 4c6182e99431d..6d3de11249a16 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.test.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock, elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock, coreMock } from '../../../../../../src/core/server/mocks'; import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -36,10 +36,12 @@ const mockFieldMappingResponse = { describe('GET /internal/security/fields/{query}', () => { it('returns a list of deduplicated fields, omitting empty and runtime fields', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const scopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - scopedClient.callAsCurrentUser.mockResolvedValue(mockFieldMappingResponse); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(scopedClient); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + }; + mockContext.core.elasticsearch.client.asCurrentUser.indices.getFieldMapping.mockImplementation( + (async () => ({ body: mockFieldMappingResponse })) as any + ); defineGetFieldsRoutes(mockRouteDefinitionParams); @@ -51,7 +53,7 @@ describe('GET /internal/security/fields/{query}', () => { path: `/internal/security/fields/foo`, headers, }); - const response = await handler({} as any, mockRequest, kibanaResponseFactory); + const response = await handler(mockContext as any, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual(['fooField', 'commonField', 'barField']); }); diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts index 44b8804ed8d6e..304e121f7fee1 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -22,7 +22,7 @@ interface FieldMappingResponse { }; } -export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/fields/{query}', @@ -30,14 +30,16 @@ export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinition }, async (context, request, response) => { try { - const indexMappings = (await clusterClient - .asScoped(request) - .callAsCurrentUser('indices.getFieldMapping', { + const { + body: indexMappings, + } = await context.core.elasticsearch.client.asCurrentUser.indices.getFieldMapping( + { index: request.params.query, fields: '*', - allowNoIndices: false, - includeDefaults: true, - })) as FieldMappingResponse; + allow_no_indices: false, + include_defaults: true, + } + ); // The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html): // 1. Iterate over all matched indices. diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts index aec0310129f6e..33fd66f9e929d 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts @@ -5,24 +5,26 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { defineRoleMappingDeleteRoutes } from './delete'; describe('DELETE role mappings', () => { it('allows a role mapping to be deleted', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ acknowledged: true }); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } } as any, + }; + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping.mockResolvedValue( + { body: { acknowledged: true } } as any + ); defineRoleMappingDeleteRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; const name = 'mapping1'; - const headers = { authorization: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ method: 'delete', @@ -30,31 +32,35 @@ describe('DELETE role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual({ acknowledged: true }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); expect( - mockScopedClusterClient.callAsCurrentUser - ).toHaveBeenCalledWith('shield.deleteRoleMapping', { name }); + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping + ).toHaveBeenCalledWith({ name }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: 'invalid', + message: 'test forbidden message', + }), + }, + } as any, + }; defineRoleMappingDeleteRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; const name = 'mapping1'; - const headers = { authorization: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ method: 'delete', @@ -62,21 +68,13 @@ describe('DELETE role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping + ).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.ts index dc11bcd914b35..dbe9c5662a1f1 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.ts @@ -8,9 +8,7 @@ import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; -export function defineRoleMappingDeleteRoutes(params: RouteDefinitionParams) { - const { clusterClient, router } = params; - +export function defineRoleMappingDeleteRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/internal/security/role_mapping/{name}', @@ -22,11 +20,11 @@ export function defineRoleMappingDeleteRoutes(params: RouteDefinitionParams) { }, createLicensedRouteHandler(async (context, request, response) => { try { - const deleteResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.deleteRoleMapping', { - name: request.params.name, - }); + const { + body: deleteResponse, + } = await context.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping({ + name: request.params.name, + }); return response.ok({ body: deleteResponse }); } catch (error) { const wrappedError = wrapError(error); diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts index ee1d550bbe24d..8bd9f095b0f68 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts @@ -5,21 +5,16 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { - kibanaResponseFactory, - RequestHandlerContext, - ILegacyClusterClient, -} from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineRoleMappingFeatureCheckRoute } from './feature_check'; interface TestOptions { licenseCheckResult?: LicenseCheck; canManageRoleMappings?: boolean; - nodeSettingsResponse?: Record; - xpackUsageResponse?: Record; - internalUserClusterClientImpl?: ILegacyClusterClient['callAsInternalUser']; + nodeSettingsResponse?: () => Record; + xpackUsageResponse?: () => Record; asserts: { statusCode: number; result?: Record }; } @@ -38,57 +33,34 @@ const defaultXpackUsageResponse = { }, }; -const getDefaultInternalUserClusterClientImpl = ( - nodeSettingsResponse: TestOptions['nodeSettingsResponse'], - xpackUsageResponse: TestOptions['xpackUsageResponse'] -) => - ((async (endpoint: string, clientParams: Record) => { - if (!clientParams) throw new TypeError('expected clientParams'); - - if (endpoint === 'nodes.info') { - return nodeSettingsResponse; - } - - if (endpoint === 'transport.request') { - if (clientParams.path === '/_xpack/usage') { - return xpackUsageResponse; - } - } - - throw new Error(`unexpected endpoint: ${endpoint}`); - }) as unknown) as TestOptions['internalUserClusterClientImpl']; - describe('GET role mappings feature check', () => { const getFeatureCheckTest = ( description: string, { licenseCheckResult = { state: 'valid' }, canManageRoleMappings = true, - nodeSettingsResponse = {}, - xpackUsageResponse = defaultXpackUsageResponse, - internalUserClusterClientImpl = getDefaultInternalUserClusterClientImpl( - nodeSettingsResponse, - xpackUsageResponse - ), + nodeSettingsResponse = async () => ({}), + xpackUsageResponse = async () => defaultXpackUsageResponse, asserts, }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation( - internalUserClusterClientImpl + mockContext.core.elasticsearch.client.asInternalUser.nodes.info.mockImplementation( + (async () => ({ body: await nodeSettingsResponse() })) as any + ); + mockContext.core.elasticsearch.client.asInternalUser.transport.request.mockImplementation( + (async () => ({ body: await xpackUsageResponse() })) as any ); - mockScopedClusterClient.callAsCurrentUser.mockImplementation(async (method, payload) => { - if (method === 'shield.hasPrivileges') { - return { - has_all_requested: canManageRoleMappings, - }; - } - }); + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockResolvedValue({ + body: { has_all_requested: canManageRoleMappings }, + } as any); defineRoleMappingFeatureCheckRoute(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; @@ -99,9 +71,6 @@ describe('GET role mappings feature check', () => { path: `/internal/security/_check_role_mapping_features`, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); @@ -124,7 +93,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('allows both script types when explicitly enabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -134,7 +103,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -147,7 +116,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('disallows stored scripts when disabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -157,7 +126,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -170,7 +139,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('disallows inline scripts when disabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -180,7 +149,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -193,7 +162,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('indicates incompatible realms when only native and file are enabled', { - xpackUsageResponse: { + xpackUsageResponse: async () => ({ security: { realms: { native: { @@ -206,7 +175,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -231,9 +200,12 @@ describe('GET role mappings feature check', () => { getFeatureCheckTest( 'falls back to allowing both script types if there is an error retrieving node settings', { - internalUserClusterClientImpl: (() => { - return Promise.reject(new Error('something bad happened')); - }) as TestOptions['internalUserClusterClientImpl'], + nodeSettingsResponse: async () => { + throw new Error('something bad happened'); + }, + xpackUsageResponse: async () => { + throw new Error('something bad happened'); + }, asserts: { statusCode: 200, result: { diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts index 88c7f193cea34..470039b8ae92b 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, ILegacyClusterClient } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; @@ -34,24 +34,18 @@ interface XPackUsageResponse { const INCOMPATIBLE_REALMS = ['file', 'native']; -export function defineRoleMappingFeatureCheckRoute({ - router, - clusterClient, - logger, -}: RouteDefinitionParams) { +export function defineRoleMappingFeatureCheckRoute({ router, logger }: RouteDefinitionParams) { router.get( { path: '/internal/security/_check_role_mapping_features', validate: false, }, createLicensedRouteHandler(async (context, request, response) => { - const { has_all_requested: canManageRoleMappings } = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.hasPrivileges', { - body: { - cluster: ['manage_security'], - }, - }); + const { + body: { has_all_requested: canManageRoleMappings }, + } = await context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges<{ + has_all_requested: boolean; + }>({ body: { cluster: ['manage_security'] } }); if (!canManageRoleMappings) { return response.ok({ @@ -61,7 +55,10 @@ export function defineRoleMappingFeatureCheckRoute({ }); } - const enabledFeatures = await getEnabledRoleMappingsFeatures(clusterClient, logger); + const enabledFeatures = await getEnabledRoleMappingsFeatures( + context.core.elasticsearch.client.asInternalUser, + logger + ); return response.ok({ body: { @@ -73,13 +70,12 @@ export function defineRoleMappingFeatureCheckRoute({ ); } -async function getEnabledRoleMappingsFeatures(clusterClient: ILegacyClusterClient, logger: Logger) { +async function getEnabledRoleMappingsFeatures(esClient: ElasticsearchClient, logger: Logger) { logger.debug(`Retrieving role mappings features`); - const nodeScriptSettingsPromise: Promise = clusterClient - .callAsInternalUser('nodes.info', { - filterPath: 'nodes.*.settings.script', - }) + const nodeScriptSettingsPromise = esClient.nodes + .info({ filter_path: 'nodes.*.settings.script' }) + .then(({ body }) => body) .catch((error) => { // fall back to assuming that node settings are unset/at their default values. // this will allow the role mappings UI to permit both role template script types, @@ -88,13 +84,11 @@ async function getEnabledRoleMappingsFeatures(clusterClient: ILegacyClusterClien return {}; }); - const xpackUsagePromise: Promise = clusterClient - // `transport.request` is potentially unsafe when combined with untrusted user input. - // Do not augment with such input. - .callAsInternalUser('transport.request', { - method: 'GET', - path: '/_xpack/usage', - }) + // `transport.request` is potentially unsafe when combined with untrusted user input. + // Do not augment with such input. + const xpackUsagePromise = esClient.transport + .request({ method: 'GET', path: '/_xpack/usage' }) + .then(({ body }) => body as XPackUsageResponse) .catch((error) => { // fall back to no external realms configured. // this will cause a warning in the UI about no compatible realms being enabled, but will otherwise allow diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts index 2519034b386bf..625aae42a3907 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts @@ -6,9 +6,9 @@ import Boom from '@hapi/boom'; import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { defineRoleMappingGetRoutes } from './get'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; const mockRoleMappingResponse = { mapping1: { @@ -49,13 +49,22 @@ const mockRoleMappingResponse = { }, }; +function getMockContext( + licenseCheckResult: { state: string; message?: string } = { state: 'valid' } +) { + return { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; +} + describe('GET role mappings', () => { it('returns all role mappings', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(mockRoleMappingResponse); + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue({ + body: mockRoleMappingResponse, + } as any); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -67,11 +76,6 @@ describe('GET role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); @@ -118,29 +122,27 @@ describe('GET role mappings', () => { }, ]); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getRoleMappings', - { name: undefined } - ); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name: undefined }); }); it('returns role mapping by name', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ - mapping1: { - enabled: true, - roles: ['foo', 'bar'], - rules: { - field: { - dn: 'CN=bob,OU=example,O=com', + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue({ + body: { + mapping1: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, }, }, }, - }); + } as any); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -155,11 +157,6 @@ describe('GET role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); @@ -175,16 +172,15 @@ describe('GET role mappings', () => { }, }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getRoleMappings', - { name } - ); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = getMockContext({ state: 'invalid', message: 'test forbidden message' }); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -196,29 +192,19 @@ describe('GET role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).not.toHaveBeenCalled(); }); it('returns a 404 when the role mapping is not found', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockRejectedValue( Boom.notFound('role mapping not found!') ); @@ -235,18 +221,12 @@ describe('GET role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(404); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); expect( - mockScopedClusterClient.callAsCurrentUser - ).toHaveBeenCalledWith('shield.getRoleMappings', { name }); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name }); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 63598584b5d1b..5ab9b1f6b4a24 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -14,7 +14,7 @@ interface RoleMappingsResponse { } export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { - const { clusterClient, logger, router } = params; + const { logger, router } = params; router.get( { @@ -29,13 +29,11 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { const expectSingleEntity = typeof request.params.name === 'string'; try { - const roleMappingsResponse: RoleMappingsResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRoleMappings', { - name: request.params.name, - }); + const roleMappingsResponse = await context.core.elasticsearch.client.asCurrentUser.security.getRoleMapping( + { name: request.params.name } + ); - const mappings = Object.entries(roleMappingsResponse).map(([name, mapping]) => { + const mappings = Object.entries(roleMappingsResponse.body).map(([name, mapping]) => { return { name, ...mapping, diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts index 8f61d2a122f0c..5dc7a21a02c6f 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts @@ -5,17 +5,20 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { defineRoleMappingPostRoutes } from './post'; describe('POST role mappings', () => { it('allows a role mapping to be created', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ created: true }); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } } as any, + }; + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping.mockResolvedValue({ + body: { created: true }, + } as any); defineRoleMappingPostRoutes(mockRouteDefinitionParams); @@ -39,37 +42,41 @@ describe('POST role mappings', () => { }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual({ created: true }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.saveRoleMapping', - { - name, - body: { - enabled: true, - roles: ['foo', 'bar'], - rules: { - field: { - dn: 'CN=bob,OU=example,O=com', - }, + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name, + body: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', }, }, - } - ); + }, + }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: 'invalid', + message: 'test forbidden message', + }), + }, + } as any, + }; defineRoleMappingPostRoutes(mockRouteDefinitionParams); @@ -81,22 +88,14 @@ describe('POST role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.ts b/x-pack/plugins/security/server/routes/role_mapping/post.ts index 11149f38069a7..6c1b19dacb601 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.ts @@ -8,9 +8,7 @@ import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; -export function defineRoleMappingPostRoutes(params: RouteDefinitionParams) { - const { clusterClient, router } = params; - +export function defineRoleMappingPostRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/role_mapping/{name}', @@ -43,13 +41,10 @@ export function defineRoleMappingPostRoutes(params: RouteDefinitionParams) { }, createLicensedRouteHandler(async (context, request, response) => { try { - const saveResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.saveRoleMapping', { - name: request.params.name, - body: request.body, - }); - return response.ok({ body: saveResponse }); + const saveResponse = await context.core.elasticsearch.client.asCurrentUser.security.putRoleMapping( + { name: request.params.name, body: request.body } + ); + return response.ok({ body: saveResponse.body }); } catch (error) { const wrappedError = wrapError(error); return response.customError({ diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index c66b5f985cb33..d98c0acb7d86d 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -7,21 +7,20 @@ import { errors } from 'elasticsearch'; import { ObjectType } from '@kbn/config-schema'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { - ILegacyClusterClient, + Headers, IRouter, - ILegacyScopedClusterClient, kibanaResponseFactory, RequestHandler, RequestHandlerContext, RouteConfig, - ScopeableRequest, } from '../../../../../../src/core/server'; import { Authentication, AuthenticationResult } from '../../authentication'; import { Session } from '../../session_management'; import { defineChangeUserPasswordRoutes } from './change_password'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { sessionMock } from '../../session_management/session.mock'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -30,19 +29,19 @@ describe('Change password', () => { let router: jest.Mocked; let authc: jest.Mocked; let session: jest.Mocked>; - let mockClusterClient: jest.Mocked; - let mockScopedClusterClient: jest.Mocked; let routeHandler: RequestHandler; let routeConfig: RouteConfig; - let mockContext: RequestHandlerContext; - - function checkPasswordChangeAPICall(username: string, request: ScopeableRequest) { - expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.changePassword', - { username, body: { password: 'new-password' } } + let mockContext: DeeplyMockedKeys; + + function checkPasswordChangeAPICall(username: string, headers?: Headers) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword + ).toHaveBeenCalledTimes(1); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword + ).toHaveBeenCalledWith( + { username, body: { password: 'new-password' } }, + headers && { headers } ); } @@ -56,15 +55,10 @@ describe('Change password', () => { authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); session.get.mockResolvedValue(sessionMock.createValue()); - mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockClusterClient = routeParamsMock.clusterClient; - mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - - mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ check: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; + mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } }, + } as any; defineChangeUserPasswordRoutes(routeParamsMock); @@ -114,20 +108,18 @@ describe('Change password', () => { const changePasswordFailure = new (errors.AuthenticationException as any)('Unauthorized', { body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(changePasswordFailure); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + changePasswordFailure + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual(changePasswordFailure); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledWith({ - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + checkPasswordChangeAPICall(username, { + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); @@ -148,16 +140,16 @@ describe('Change password', () => { expect(response.payload).toEqual(loginFailureReason); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); it('returns 500 if password update request fails with non-401 error.', async () => { const failureReason = new Error('Request failed.'); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + failureReason + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); @@ -165,10 +157,8 @@ describe('Change password', () => { expect(response.payload).toEqual(failureReason); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); @@ -179,10 +169,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).toHaveBeenCalledTimes(1); @@ -209,10 +197,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).toHaveBeenCalledTimes(1); @@ -230,10 +216,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).not.toHaveBeenCalled(); @@ -249,7 +233,9 @@ describe('Change password', () => { it('returns 500 if password update request fails.', async () => { const failureReason = new Error('Request failed.'); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + failureReason + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); @@ -257,7 +243,7 @@ describe('Change password', () => { expect(response.payload).toEqual(failureReason); expect(authc.login).not.toHaveBeenCalled(); - checkPasswordChangeAPICall(username, mockRequest); + checkPasswordChangeAPICall(username); }); it('successfully changes user password.', async () => { @@ -267,7 +253,7 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); expect(authc.login).not.toHaveBeenCalled(); - checkPasswordChangeAPICall(username, mockRequest); + checkPasswordChangeAPICall(username); }); }); }); diff --git a/x-pack/plugins/security/server/routes/users/change_password.ts b/x-pack/plugins/security/server/routes/users/change_password.ts index be868f841eeeb..66d36b4294883 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.ts @@ -14,12 +14,7 @@ import { } from '../../authentication'; import { RouteDefinitionParams } from '..'; -export function defineChangeUserPasswordRoutes({ - authc, - session, - router, - clusterClient, -}: RouteDefinitionParams) { +export function defineChangeUserPasswordRoutes({ authc, session, router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}/password', @@ -43,28 +38,26 @@ export function defineChangeUserPasswordRoutes({ // If user is changing their own password they should provide a proof of knowledge their // current password via sending it in `Authorization: Basic base64(username:current password)` // HTTP header no matter how they logged in to Kibana. - const scopedClusterClient = clusterClient.asScoped( - isUserChangingOwnPassword - ? { - headers: { - ...request.headers, - authorization: new HTTPAuthorizationHeader( - 'Basic', - new BasicHTTPAuthorizationHeaderCredentials( - username, - currentPassword || '' - ).toString() - ).toString(), - }, - } - : request - ); + const options = isUserChangingOwnPassword + ? { + headers: { + ...request.headers, + authorization: new HTTPAuthorizationHeader( + 'Basic', + new BasicHTTPAuthorizationHeaderCredentials( + username, + currentPassword || '' + ).toString() + ).toString(), + }, + } + : undefined; try { - await scopedClusterClient.callAsCurrentUser('shield.changePassword', { - username, - body: { password: newPassword }, - }); + await context.core.elasticsearch.client.asCurrentUser.security.changePassword( + { username, body: { password: newPassword } }, + options + ); } catch (error) { // This may happen only if user's credentials are rejected meaning that current password // isn't correct. diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts index 5a3e50bb11d5c..a98848a583500 100644 --- a/x-pack/plugins/security/server/routes/users/create_or_update.ts +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -9,7 +9,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCreateOrUpdateUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineCreateOrUpdateUserRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}', @@ -28,7 +28,7 @@ export function defineCreateOrUpdateUserRoutes({ router, clusterClient }: RouteD }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient.asScoped(request).callAsCurrentUser('shield.putUser', { + await context.core.elasticsearch.client.asCurrentUser.security.putUser({ username: request.params.username, // Omit `username`, `enabled` and all fields with `null` value. body: Object.fromEntries( diff --git a/x-pack/plugins/security/server/routes/users/delete.ts b/x-pack/plugins/security/server/routes/users/delete.ts index 99a8d5c18ab3d..26a1765b4fbdf 100644 --- a/x-pack/plugins/security/server/routes/users/delete.ts +++ b/x-pack/plugins/security/server/routes/users/delete.ts @@ -9,7 +9,7 @@ import { RouteDefinitionParams } from '../index'; import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; -export function defineDeleteUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineDeleteUserRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/internal/security/users/{username}', @@ -19,9 +19,9 @@ export function defineDeleteUserRoutes({ router, clusterClient }: RouteDefinitio }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.deleteUser', { username: request.params.username }); + await context.core.elasticsearch.client.asCurrentUser.security.deleteUser({ + username: request.params.username, + }); return response.noContent(); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/users/get.ts b/x-pack/plugins/security/server/routes/users/get.ts index 0867910372546..aa6a4f6be8bad 100644 --- a/x-pack/plugins/security/server/routes/users/get.ts +++ b/x-pack/plugins/security/server/routes/users/get.ts @@ -9,7 +9,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineGetUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetUserRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/users/{username}', @@ -20,9 +20,13 @@ export function defineGetUserRoutes({ router, clusterClient }: RouteDefinitionPa createLicensedRouteHandler(async (context, request, response) => { try { const username = request.params.username; - const users = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getUser', { username })) as Record; + const { + body: users, + } = await context.core.elasticsearch.client.asCurrentUser.security.getUser< + Record + >({ + username, + }); if (!users[username]) { return response.notFound(); diff --git a/x-pack/plugins/security/server/routes/users/get_all.ts b/x-pack/plugins/security/server/routes/users/get_all.ts index 492ab27ab27ad..3c5ca184f747c 100644 --- a/x-pack/plugins/security/server/routes/users/get_all.ts +++ b/x-pack/plugins/security/server/routes/users/get_all.ts @@ -8,7 +8,7 @@ import { RouteDefinitionParams } from '../index'; import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; -export function defineGetAllUsersRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetAllUsersRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/users', validate: false }, createLicensedRouteHandler(async (context, request, response) => { @@ -16,7 +16,7 @@ export function defineGetAllUsersRoutes({ router, clusterClient }: RouteDefiniti return response.ok({ // Return only values since keys (user names) are already duplicated there. body: Object.values( - await clusterClient.asScoped(request).callAsCurrentUser('shield.getUser') + (await context.core.elasticsearch.client.asCurrentUser.security.getUser()).body ), }); } catch (error) { diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts index 8ff75b25388b0..4fff99b09d4ad 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts @@ -7,6 +7,7 @@ import { getQueryFilter, buildExceptionFilter, buildEqlSearchRequest } from './get_query_filter'; import { Filter, EsQueryConfig } from 'src/plugins/data/public'; import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListItemSchema } from '../shared_imports'; describe('get_filter', () => { describe('getQueryFilter', () => { @@ -919,19 +920,27 @@ describe('get_filter', () => { dateFormatTZ: 'Zulu', }; test('it should build a filter without chunking exception items', () => { - const exceptionFilter = buildExceptionFilter( - [ - { language: 'kuery', query: 'host.name: linux and some.field: value' }, - { language: 'kuery', query: 'user.name: name' }, + const exceptionItem1: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'host.name', operator: 'included', type: 'match', value: 'linux' }, + { field: 'some.field', operator: 'included', type: 'match', value: 'value' }, ], - { + }; + const exceptionItem2: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.name', operator: 'included', type: 'match', value: 'name' }], + }; + const exceptionFilter = buildExceptionFilter({ + lists: [exceptionItem1, exceptionItem2], + config, + excludeExceptions: true, + chunkSize: 2, + indexPattern: { fields: [], title: 'auditbeat-*', }, - config, - true, - 2 - ); + }); expect(exceptionFilter).toEqual({ meta: { alias: null, @@ -949,7 +958,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'host.name': 'linux', }, }, @@ -961,7 +970,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'some.field': 'value', }, }, @@ -976,7 +985,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'user.name': 'name', }, }, @@ -990,20 +999,31 @@ describe('get_filter', () => { }); test('it should properly chunk exception items', () => { - const exceptionFilter = buildExceptionFilter( - [ - { language: 'kuery', query: 'host.name: linux and some.field: value' }, - { language: 'kuery', query: 'user.name: name' }, - { language: 'kuery', query: 'file.path: /safe/path' }, + const exceptionItem1: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'host.name', operator: 'included', type: 'match', value: 'linux' }, + { field: 'some.field', operator: 'included', type: 'match', value: 'value' }, ], - { + }; + const exceptionItem2: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.name', operator: 'included', type: 'match', value: 'name' }], + }; + const exceptionItem3: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'file.path', operator: 'included', type: 'match', value: '/safe/path' }], + }; + const exceptionFilter = buildExceptionFilter({ + lists: [exceptionItem1, exceptionItem2, exceptionItem3], + config, + excludeExceptions: true, + chunkSize: 2, + indexPattern: { fields: [], title: 'auditbeat-*', }, - config, - true, - 2 - ); + }); expect(exceptionFilter).toEqual({ meta: { alias: null, @@ -1024,7 +1044,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'host.name': 'linux', }, }, @@ -1036,7 +1056,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'some.field': 'value', }, }, @@ -1051,7 +1071,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'user.name': 'name', }, }, @@ -1069,7 +1089,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'file.path': '/safe/path', }, }, diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 73638fc48f381..fcea90402d89d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -6,7 +6,6 @@ import { Filter, - Query, IIndexPattern, isFilterDisabled, buildEsQuery, @@ -18,15 +17,10 @@ import { } from '../../../lists/common/schemas'; import { ESBoolQuery } from '../typed_json'; import { buildExceptionListQueries } from './build_exceptions_query'; -import { - Query as QueryString, - Language, - Index, - TimestampOverrideOrUndefined, -} from './schemas/common/schemas'; +import { Query, Language, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas'; export const getQueryFilter = ( - query: QueryString, + query: Query, language: Language, filters: Array>, index: Index, @@ -53,19 +47,18 @@ export const getQueryFilter = ( * buildEsQuery, this allows us to offer nested queries * regardless */ - const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists }); - if (exceptionQueries.length > 0) { - // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value), - // allowing us to make 1024-item chunks of exception list items. - // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a - // very conservative value. - const exceptionFilter = buildExceptionFilter( - exceptionQueries, - indexPattern, - config, - excludeExceptions, - 1024 - ); + // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value), + // allowing us to make 1024-item chunks of exception list items. + // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a + // very conservative value. + const exceptionFilter = buildExceptionFilter({ + lists, + config, + excludeExceptions, + chunkSize: 1024, + indexPattern, + }); + if (exceptionFilter !== undefined) { enabledFilters.push(exceptionFilter); } const initialQuery = { query, language }; @@ -101,15 +94,17 @@ export const buildEqlSearchRequest = ( ignoreFilterIfFieldNotInIndex: false, dateFormatTZ: 'Zulu', }; - const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists: exceptionLists }); - let exceptionFilter: Filter | undefined; - if (exceptionQueries.length > 0) { - // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value), - // allowing us to make 1024-item chunks of exception list items. - // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a - // very conservative value. - exceptionFilter = buildExceptionFilter(exceptionQueries, indexPattern, config, true, 1024); - } + // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value), + // allowing us to make 1024-item chunks of exception list items. + // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a + // very conservative value. + const exceptionFilter = buildExceptionFilter({ + lists: exceptionLists, + config, + excludeExceptions: true, + chunkSize: 1024, + indexPattern, + }); const indexString = index.join(); const requestFilter: unknown[] = [ { @@ -154,13 +149,23 @@ export const buildEqlSearchRequest = ( } }; -export const buildExceptionFilter = ( - exceptionQueries: Query[], - indexPattern: IIndexPattern, - config: EsQueryConfig, - excludeExceptions: boolean, - chunkSize: number -) => { +export const buildExceptionFilter = ({ + lists, + config, + excludeExceptions, + chunkSize, + indexPattern, +}: { + lists: Array; + config: EsQueryConfig; + excludeExceptions: boolean; + chunkSize: number; + indexPattern?: IIndexPattern; +}) => { + const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists }); + if (exceptionQueries.length === 0) { + return undefined; + } const exceptionFilter: Filter = { meta: { alias: null, diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts index 6d3e320fba3af..b516f7c57a96d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts @@ -10,7 +10,7 @@ import { migratePackagePolicyToV7110 } from './to_v7_11.0'; describe('7.11.0 Endpoint Package Policy migration', () => { const migration = migratePackagePolicyToV7110; - it('adds malware notification checkbox and optional message', () => { + it('adds malware notification checkbox and optional message and adds AV registration config', () => { const doc: SavedObjectUnsanitizedDoc = { attributes: { name: 'Some Policy Name', @@ -77,6 +77,7 @@ describe('7.11.0 Endpoint Package Policy migration', () => { policy: { value: { windows: { + antivirus_registration: { enabled: false }, popup: { malware: { message: '', diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts index 551e0ecfdcb4f..557633a747267 100644 --- a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts +++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts @@ -23,8 +23,13 @@ export const migratePackagePolicyToV7110: SavedObjectMigrationFn { waitForAlertsPanelToBeLoaded(); waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_ALERTS) + cy.get(ALERTS_COUNT) .invoke('text') .then((numberOfAlerts) => { cy.get(SHOWING_ALERTS).should('have.text', `Showing ${numberOfAlerts} alerts`); @@ -64,10 +64,7 @@ describe('Alerts', () => { waitForAlerts(); const expectedNumberOfAlertsAfterClosing = +numberOfAlerts - numberOfAlertsToBeClosed; - cy.get(NUMBER_OF_ALERTS).should( - 'have.text', - expectedNumberOfAlertsAfterClosing.toString() - ); + cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlertsAfterClosing.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', @@ -77,7 +74,7 @@ describe('Alerts', () => { goToClosedAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeClosed.toString()); + cy.get(ALERTS_COUNT).should('have.text', numberOfAlertsToBeClosed.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${numberOfAlertsToBeClosed.toString()} alerts` @@ -98,7 +95,7 @@ describe('Alerts', () => { waitForAlerts(); const expectedNumberOfClosedAlertsAfterOpened = 2; - cy.get(NUMBER_OF_ALERTS).should( + cy.get(ALERTS_COUNT).should( 'have.text', expectedNumberOfClosedAlertsAfterOpened.toString() ); @@ -128,7 +125,7 @@ describe('Alerts', () => { it('Closes one alert when more than one opened alerts are selected', () => { waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_ALERTS) + cy.get(ALERTS_COUNT) .invoke('text') .then((numberOfAlerts) => { const numberOfAlertsToBeClosed = 1; @@ -144,7 +141,7 @@ describe('Alerts', () => { waitForAlerts(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeClosed; - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${expectedNumberOfAlerts.toString()} alerts` @@ -153,7 +150,7 @@ describe('Alerts', () => { goToClosedAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeClosed.toString()); + cy.get(ALERTS_COUNT).should('have.text', numberOfAlertsToBeClosed.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${numberOfAlertsToBeClosed.toString()} alert` @@ -178,7 +175,7 @@ describe('Alerts', () => { goToClosedAlerts(); waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_ALERTS) + cy.get(ALERTS_COUNT) .invoke('text') .then((numberOfAlerts) => { const numberOfAlertsToBeOpened = 1; @@ -195,7 +192,7 @@ describe('Alerts', () => { waitForAlerts(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeOpened; - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${expectedNumberOfAlerts.toString()} alerts` @@ -204,7 +201,7 @@ describe('Alerts', () => { goToOpenedAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeOpened.toString()); + cy.get(ALERTS_COUNT).should('have.text', numberOfAlertsToBeOpened.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${numberOfAlertsToBeOpened.toString()} alert` @@ -228,7 +225,7 @@ describe('Alerts', () => { waitForAlerts(); waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_ALERTS) + cy.get(ALERTS_COUNT) .invoke('text') .then((numberOfAlerts) => { const numberOfAlertsToBeMarkedInProgress = 1; @@ -244,7 +241,7 @@ describe('Alerts', () => { waitForAlertsToBeLoaded(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeMarkedInProgress; - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${expectedNumberOfAlerts.toString()} alerts` @@ -253,10 +250,7 @@ describe('Alerts', () => { goToInProgressAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS).should( - 'have.text', - numberOfAlertsToBeMarkedInProgress.toString() - ); + cy.get(ALERTS_COUNT).should('have.text', numberOfAlertsToBeMarkedInProgress.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${numberOfAlertsToBeMarkedInProgress.toString()} alert` diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_exceptions.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_exceptions.spec.ts index b1d7163ac70e0..160dbad9a06be 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_exceptions.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_exceptions.spec.ts @@ -6,8 +6,8 @@ import { exception } from '../objects/exception'; import { newRule } from '../objects/rule'; +import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../screens/alerts'; import { RULE_STATUS } from '../screens/create_new_rule'; -import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { addExceptionFromFirstAlert, @@ -52,7 +52,8 @@ describe('Exceptions', () => { waitForAlertsToPopulate(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfInitialAlertsText) => { cy.wrap(parseInt(numberOfInitialAlertsText, 10)).should( @@ -77,7 +78,8 @@ describe('Exceptions', () => { goToAlertsTab(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfAlertsAfterCreatingExceptionText, 10)).should('eql', 0); @@ -86,7 +88,8 @@ describe('Exceptions', () => { goToClosedAlerts(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfClosedAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfClosedAlertsAfterCreatingExceptionText, 10)).should( @@ -99,7 +102,8 @@ describe('Exceptions', () => { waitForTheRuleToBeExecuted(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfOpenedAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfOpenedAlertsAfterCreatingExceptionText, 10)).should('eql', 0); @@ -113,7 +117,8 @@ describe('Exceptions', () => { waitForAlertsToPopulate(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfAlertsAfterRemovingExceptionsText) => { cy.wrap(parseInt(numberOfAlertsAfterRemovingExceptionsText, 10)).should( @@ -130,7 +135,8 @@ describe('Exceptions', () => { addsException(exception); esArchiverLoad('auditbeat_for_exceptions2'); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfAlertsAfterCreatingExceptionText, 10)).should('eql', 0); @@ -139,7 +145,8 @@ describe('Exceptions', () => { goToClosedAlerts(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfClosedAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfClosedAlertsAfterCreatingExceptionText, 10)).should( @@ -152,7 +159,8 @@ describe('Exceptions', () => { waitForTheRuleToBeExecuted(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfOpenedAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfOpenedAlertsAfterCreatingExceptionText, 10)).should('eql', 0); @@ -165,7 +173,8 @@ describe('Exceptions', () => { waitForAlertsToPopulate(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfAlertsAfterRemovingExceptionsText) => { cy.wrap(parseInt(numberOfAlertsAfterRemovingExceptionsText, 10)).should( diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_indicator_match.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_indicator_match.spec.ts new file mode 100644 index 0000000000000..03e714f2381c6 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_indicator_match.spec.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { newThreatIndicatorRule } from '../objects/rule'; + +import { + ALERT_RULE_METHOD, + ALERT_RULE_NAME, + ALERT_RULE_RISK_SCORE, + ALERT_RULE_SEVERITY, + ALERT_RULE_VERSION, + NUMBER_OF_ALERTS, +} from '../screens/alerts'; +import { + CUSTOM_RULES_BTN, + RISK_SCORE, + RULE_NAME, + RULES_ROW, + RULES_TABLE, + RULE_SWITCH, + SEVERITY, +} from '../screens/alerts_detection_rules'; +import { + ABOUT_DETAILS, + ABOUT_INVESTIGATION_NOTES, + ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + CUSTOM_QUERY_DETAILS, + DEFINITION_DETAILS, + FALSE_POSITIVES_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, + INDICATOR_INDEX_PATTERNS, + INDICATOR_INDEX_QUERY, + INDICATOR_MAPPING, + INVESTIGATION_NOTES_MARKDOWN, + INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + removeExternalLinkText, + RISK_SCORE_DETAILS, + RULE_NAME_HEADER, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, + SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, +} from '../screens/rule_details'; + +import { + goToManageAlertsDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, +} from '../tasks/alerts'; +import { + changeToThreeHundredRowsPerPage, + deleteRule, + filterByCustomRules, + goToCreateNewRule, + goToRuleDetails, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/alerts_detection_rules'; +import { removeSignalsIndex } from '../tasks/api_calls'; +import { + createAndActivateRule, + fillAboutRuleAndContinue, + fillDefineIndicatorMatchRuleAndContinue, + fillScheduleRuleAndContinue, + selectIndicatorMatchType, + waitForAlertsToPopulate, + waitForTheRuleToBeExecuted, +} from '../tasks/create_new_rule'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS_URL } from '../urls/navigation'; + +const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); +const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); +const expectedTags = newThreatIndicatorRule.tags.join(''); +const expectedMitre = newThreatIndicatorRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); +const expectedNumberOfRules = 1; +const expectedNumberOfAlerts = 1; + +describe('Detection rules, Indicator Match', () => { + beforeEach(() => { + esArchiverLoad('threat_indicator'); + esArchiverLoad('threat_data'); + }); + + afterEach(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('threat_data'); + removeSignalsIndex(); + deleteRule(); + }); + + it('Creates and activates a new Indicator Match rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectIndicatorMatchType(); + fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); + fillAboutRuleAndContinue(newThreatIndicatorRule); + fillScheduleRuleAndContinue(newThreatIndicatorRule); + createAndActivateRule(); + + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); + + filterByCustomRules(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); + }); + cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name); + cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore); + cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); + + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', newThreatIndicatorRule.index.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + getDetails(INDICATOR_INDEX_PATTERNS).should( + 'have.text', + newThreatIndicatorRule.indicatorIndexPattern.join('') + ); + getDetails(INDICATOR_MAPPING).should( + 'have.text', + `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` + ); + getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); + }); + + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}` + ); + }); + + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); + cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); + cy.get(ALERT_RULE_SEVERITY) + .first() + .should('have.text', newThreatIndicatorRule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 8ba545e242b47..06046b9385712 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -30,10 +30,10 @@ interface Interval { } export interface CustomRule { - customQuery: string; + customQuery?: string; name: string; description: string; - index?: string[]; + index: string[]; interval?: string; severity: string; riskScore: string; @@ -43,7 +43,7 @@ export interface CustomRule { falsePositivesExamples: string[]; mitre: Mitre[]; note: string; - timelineId: string; + timelineId?: string; runsEvery: Interval; lookBack: Interval; } @@ -60,6 +60,12 @@ export interface OverrideRule extends CustomRule { timestampOverride: string; } +export interface ThreatIndicatorRule extends CustomRule { + indicatorIndexPattern: string[]; + indicatorMapping: string; + indicatorIndexField: string; +} + export interface MachineLearningRule { machineLearningJob: string; anomalyScoreThreshold: string; @@ -77,6 +83,16 @@ export interface MachineLearningRule { lookBack: Interval; } +export const indexPatterns = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', +]; + const mitre1: Mitre = { tactic: 'Discovery (TA0007)', techniques: ['Cloud Service Discovery (T1526)', 'File and Directory Discovery (T1083)'], @@ -121,6 +137,7 @@ const lookBack: Interval = { export const newRule: CustomRule = { customQuery: 'host.name:*', + index: indexPatterns, name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -162,6 +179,7 @@ export const existingRule: CustomRule = { export const newOverrideRule: OverrideRule = { customQuery: 'host.name:*', + index: indexPatterns, name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -182,6 +200,7 @@ export const newOverrideRule: OverrideRule = { export const newThresholdRule: ThresholdRule = { customQuery: 'host.name:*', + index: indexPatterns, name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -217,6 +236,7 @@ export const machineLearningRule: MachineLearningRule = { export const eqlRule: CustomRule = { customQuery: 'any where process.name == "which"', name: 'New EQL Rule', + index: indexPatterns, description: 'New EQL rule description.', severity: 'High', riskScore: '17', @@ -236,6 +256,7 @@ export const eqlSequenceRule: CustomRule = { [any where process.name == "which"]\ [any where process.name == "xargs"]', name: 'New EQL Sequence Rule', + index: indexPatterns, description: 'New EQL rule description.', severity: 'High', riskScore: '17', @@ -249,15 +270,23 @@ export const eqlSequenceRule: CustomRule = { lookBack, }; -export const indexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', -]; +export const newThreatIndicatorRule: ThreatIndicatorRule = { + name: 'Threat Indicator Rule Test', + description: 'The threat indicator rule description.', + index: ['threat-data-*'], + severity: 'Critical', + riskScore: '20', + tags: ['test', 'threat'], + referenceUrls: ['https://www.google.com/', 'https://elastic.co/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [mitre1, mitre2], + note: '# test markdown', + runsEvery, + lookBack, + indicatorIndexPattern: ['threat-indicator-*'], + indicatorMapping: 'agent.id', + indicatorIndexField: 'agent.threat', +}; export const severitiesOverride = ['Low', 'Medium', 'High', 'Critical']; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 2c80d02cad83d..bc3be900284b4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -8,6 +8,8 @@ export const ADD_EXCEPTION_BTN = '[data-test-subj="addExceptionButton"]'; export const ALERTS = '[data-test-subj="event"]'; +export const ALERTS_COUNT = '[data-test-subj="server-side-event-count"]'; + export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; export const ALERT_ID = '[data-test-subj="draggable-content-_id"]'; @@ -43,7 +45,7 @@ export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-st export const MARK_SELECTED_ALERTS_IN_PROGRESS_BTN = '[data-test-subj="markSelectedAlertsInProgressButton"]'; -export const NUMBER_OF_ALERTS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; +export const NUMBER_OF_ALERTS = '[data-test-subj="local-events-count"]'; export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index d802e97363a68..ab9347f1862cc 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -27,8 +27,12 @@ export const MITRE_BTN = '[data-test-subj="addMitre"]'; export const ADVANCED_SETTINGS_BTN = '[data-test-subj="advancedSettings"] .euiAccordion__button'; +export const COMBO_BOX_CLEAR_BTN = '[data-test-subj="comboBoxClearButton"]'; + export const COMBO_BOX_INPUT = '[data-test-subj="comboBoxInput"]'; +export const COMBO_BOX_RESULT = '.euiFilterSelectItem'; + export const CREATE_AND_ACTIVATE_BTN = '[data-test-subj="create-activate"]'; export const CUSTOM_QUERY_INPUT = @@ -57,6 +61,8 @@ export const EQL_QUERY_VALIDATION_SPINNER = '[data-test-subj="eql-validation-loa export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; +export const INDICATOR_MATCH_TYPE = '[data-test-subj="threatMatchRuleType"]'; + export const INPUT = '[data-test-subj="input"]'; export const INVESTIGATION_NOTES_TEXTAREA = diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 8e93d5dcd6315..ad969b54ffd90 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -36,6 +36,12 @@ export const FALSE_POSITIVES_DETAILS = 'False positive examples'; export const INDEX_PATTERNS_DETAILS = 'Index patterns'; +export const INDICATOR_INDEX_PATTERNS = 'Indicator index patterns'; + +export const INDICATOR_INDEX_QUERY = 'Indicator index query'; + +export const INDICATOR_MAPPING = 'Indicator mapping'; + export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; export const INVESTIGATION_NOTES_TOGGLE = '[data-test-subj="stepAboutDetailsToggle-notes"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 9b809dbe524ae..219c6496ee893 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -9,6 +9,7 @@ import { MachineLearningRule, machineLearningRule, OverrideRule, + ThreatIndicatorRule, ThresholdRule, } from '../objects/rule'; import { @@ -26,6 +27,7 @@ import { DEFINE_EDIT_TAB, FALSE_POSITIVES_INPUT, IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, + INDICATOR_MATCH_TYPE, INPUT, INVESTIGATION_NOTES_TEXTAREA, LOOK_BACK_INTERVAL, @@ -63,11 +65,13 @@ import { QUERY_PREVIEW_BUTTON, EQL_QUERY_PREVIEW_HISTOGRAM, EQL_QUERY_VALIDATION_SPINNER, + COMBO_BOX_CLEAR_BTN, + COMBO_BOX_RESULT, } from '../screens/create_new_rule'; -import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { NOTIFICATION_TOASTS, TOAST_ERROR_CLASS } from '../screens/shared'; import { TIMELINE } from '../screens/timelines'; import { refreshPage } from './security_header'; +import { NUMBER_OF_ALERTS } from '../screens/alerts'; export const createAndActivateRule = () => { cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); @@ -75,7 +79,9 @@ export const createAndActivateRule = () => { cy.get(CREATE_AND_ACTIVATE_BTN).should('not.exist'); }; -export const fillAboutRule = (rule: CustomRule | MachineLearningRule | ThresholdRule) => { +export const fillAboutRule = ( + rule: CustomRule | MachineLearningRule | ThresholdRule | ThreatIndicatorRule +) => { cy.get(RULE_NAME_INPUT).clear({ force: true }).type(rule.name, { force: true }); cy.get(RULE_DESCRIPTION_INPUT).clear({ force: true }).type(rule.description, { force: true }); @@ -121,7 +127,7 @@ export const fillAboutRule = (rule: CustomRule | MachineLearningRule | Threshold }; export const fillAboutRuleAndContinue = ( - rule: CustomRule | MachineLearningRule | ThresholdRule + rule: CustomRule | MachineLearningRule | ThresholdRule | ThreatIndicatorRule ) => { fillAboutRule(rule); cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); @@ -195,7 +201,7 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = ( rule: CustomRule | OverrideRule ) => { cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); - cy.get(TIMELINE(rule.timelineId)).click(); + cy.get(TIMELINE(rule.timelineId!)).click(); cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); @@ -213,7 +219,7 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const thresholdField = 0; const threshold = 1; - cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery!); cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(THRESHOLD_INPUT_AREA) .find(INPUT) @@ -228,7 +234,7 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { }; export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { - cy.get(EQL_QUERY_INPUT).type(rule.customQuery); + cy.get(EQL_QUERY_INPUT).type(rule.customQuery!); cy.get(EQL_QUERY_VALIDATION_SPINNER).should('not.exist'); cy.get(QUERY_PREVIEW_BUTTON).should('not.be.disabled').click({ force: true }); cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); @@ -238,6 +244,22 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { cy.get(EQL_QUERY_INPUT).should('not.exist'); }; +export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRule) => { + const INDEX_PATTERNS = 0; + const INDICATOR_INDEX_PATTERN = 2; + const INDICATOR_MAPPING = 3; + const INDICATOR_INDEX_FIELD = 4; + + cy.get(COMBO_BOX_CLEAR_BTN).click(); + cy.get(COMBO_BOX_INPUT).eq(INDEX_PATTERNS).type(`${rule.index}{enter}`); + cy.get(COMBO_BOX_INPUT).eq(INDICATOR_INDEX_PATTERN).type(`${rule.indicatorIndexPattern}{enter}`); + cy.get(COMBO_BOX_INPUT).eq(INDICATOR_MAPPING).type(`${rule.indicatorMapping}{enter}`); + cy.get(COMBO_BOX_RESULT).first().click(); + cy.get(COMBO_BOX_INPUT).eq(INDICATOR_INDEX_FIELD).type(`${rule.indicatorIndexField}{enter}`); + cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); +}; + export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRule) => { cy.get(MACHINE_LEARNING_DROPDOWN).click({ force: true }); cy.contains(MACHINE_LEARNING_LIST, rule.machineLearningJob).click(); @@ -265,6 +287,14 @@ export const goToActionsStepTab = () => { cy.get(ACTIONS_EDIT_TAB).click({ force: true }); }; +export const selectEqlRuleType = () => { + cy.get(EQL_TYPE).click({ force: true }); +}; + +export const selectIndicatorMatchType = () => { + cy.get(INDICATOR_MATCH_TYPE).click({ force: true }); +}; + export const selectMachineLearningRuleType = () => { cy.get(MACHINE_LEARNING_TYPE).click({ force: true }); }; @@ -273,22 +303,6 @@ export const selectThresholdRuleType = () => { cy.get(THRESHOLD_TYPE).click({ force: true }); }; -export const waitForAlertsToPopulate = async () => { - cy.waitUntil( - () => { - refreshPage(); - return cy - .get(SERVER_SIDE_EVENT_COUNT) - .invoke('text') - .then((countText) => { - const alertCount = parseInt(countText, 10) || 0; - return alertCount > 0; - }); - }, - { interval: 500, timeout: 12000 } - ); -}; - export const waitForTheRuleToBeExecuted = () => { cy.waitUntil(() => { cy.get(REFRESH_BUTTON).click(); @@ -299,6 +313,15 @@ export const waitForTheRuleToBeExecuted = () => { }); }; -export const selectEqlRuleType = () => { - cy.get(EQL_TYPE).click({ force: true }); +export const waitForAlertsToPopulate = async () => { + cy.waitUntil(() => { + refreshPage(); + return cy + .get(NUMBER_OF_ALERTS) + .invoke('text') + .then((countText) => { + const alertCount = parseInt(countText, 10) || 0; + return alertCount > 0; + }); + }); }; diff --git a/x-pack/plugins/security_solution/jest.config.js b/x-pack/plugins/security_solution/jest.config.js new file mode 100644 index 0000000000000..ae7a2dbbd05ca --- /dev/null +++ b/x-pack/plugins/security_solution/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/security_solution'], +}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index b2f8426413b12..0bbe4c71ef5a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint complexity: ["error", 30]*/ + import React, { memo, useEffect, useState, useCallback, useMemo } from 'react'; import styled, { css } from 'styled-components'; import { @@ -53,6 +55,7 @@ import { import { ErrorInfo, ErrorCallout } from '../error_callout'; import { ExceptionsBuilderExceptionItem } from '../types'; import { useFetchIndex } from '../../../containers/source'; +import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs'; export interface AddExceptionModalProps { ruleName: string; @@ -108,7 +111,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const { http } = useKibana().services; const [errorsExist, setErrorExists] = useState(false); const [comment, setComment] = useState(''); - const { rule: maybeRule } = useRuleAsync(ruleId); + const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId); const [shouldCloseAlert, setShouldCloseAlert] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); @@ -124,8 +127,22 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const [isSignalIndexPatternLoading, { indexPatterns: signalIndexPatterns }] = useFetchIndex( memoSignalIndexName ); - const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices); + const memoMlJobIds = useMemo( + () => (maybeRule?.machine_learning_job_id != null ? [maybeRule.machine_learning_job_id] : []), + [maybeRule] + ); + const { loading: mlJobLoading, jobs } = useGetInstalledJob(memoMlJobIds); + + const memoRuleIndices = useMemo(() => { + if (jobs.length > 0) { + return jobs[0].results_index_name ? [`.ml-anomalies-${jobs[0].results_index_name}`] : []; + } else { + return ruleIndices; + } + }, [jobs, ruleIndices]); + + const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(memoRuleIndices); const onError = useCallback( (error: Error): void => { addError(error, { title: i18n.ADD_EXCEPTION_ERROR }); @@ -364,6 +381,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({ !isSignalIndexPatternLoading && !isLoadingExceptionList && !isIndexPatternLoading && + !isRuleLoading && + !mlJobLoading && ruleExceptionList && ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index ab0c566aa55c6..e97f745d6f979 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -47,6 +47,7 @@ import { } from '../helpers'; import { Loader } from '../../loader'; import { ErrorInfo, ErrorCallout } from '../error_callout'; +import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs'; interface EditExceptionModalProps { ruleName: string; @@ -100,7 +101,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const { http } = useKibana().services; const [comment, setComment] = useState(''); const [errorsExist, setErrorExists] = useState(false); - const { rule: maybeRule } = useRuleAsync(ruleId); + const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId); const [updateError, setUpdateError] = useState(null); const [hasVersionConflict, setHasVersionConflict] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); @@ -117,7 +118,21 @@ export const EditExceptionModal = memo(function EditExceptionModal({ memoSignalIndexName ); - const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices); + const memoMlJobIds = useMemo( + () => (maybeRule?.machine_learning_job_id != null ? [maybeRule.machine_learning_job_id] : []), + [maybeRule] + ); + const { loading: mlJobLoading, jobs } = useGetInstalledJob(memoMlJobIds); + + const memoRuleIndices = useMemo(() => { + if (jobs.length > 0) { + return jobs[0].results_index_name ? [`.ml-anomalies-${jobs[0].results_index_name}`] : []; + } else { + return ruleIndices; + } + }, [jobs, ruleIndices]); + + const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(memoRuleIndices); const handleExceptionUpdateError = useCallback( (error: Error, statusCode: number | null, message: string | null) => { @@ -280,69 +295,75 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( )} - {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( - <> - - {isRuleEQLSequenceStatement && ( - <> - - - - )} - {i18n.EXCEPTION_BUILDER_INFO} - - - - - - - - - - - + + {isRuleEQLSequenceStatement && ( + <> + + + + )} + {i18n.EXCEPTION_BUILDER_INFO} + + - - {exceptionListType === 'endpoint' && ( - <> - - - {i18n.ENDPOINT_QUARANTINE_TEXT} - - - )} - - - )} + + + + + + + + + + + {exceptionListType === 'endpoint' && ( + <> + + + {i18n.ENDPOINT_QUARANTINE_TEXT} + + + )} + + + )} {updateError != null && ( => + http.fetch('/api/ml/jobs/jobs', { + method: 'POST', + body: JSON.stringify({ jobIds }), + asSystemRequest: true, + signal, + }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs.ts new file mode 100644 index 0000000000000..4d7b342773d6c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; +import { getJobs } from '../api/get_jobs'; +import { CombinedJobWithStats } from '../../../../../../ml/common/types/anomaly_detection_jobs'; + +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useHttp } from '../../../lib/kibana'; +import { useMlCapabilities } from './use_ml_capabilities'; +import * as i18n from '../translations'; + +const _getJobs = withOptionalSignal(getJobs); + +export const useGetJobs = () => useAsync(_getJobs); + +export interface UseGetInstalledJobReturn { + loading: boolean; + jobs: CombinedJobWithStats[]; + isMlUser: boolean; + isLicensed: boolean; +} + +export const useGetInstalledJob = (jobIds: string[]): UseGetInstalledJobReturn => { + const [jobs, setJobs] = useState([]); + const { addError } = useAppToasts(); + const mlCapabilities = useMlCapabilities(); + const http = useHttp(); + const { error, loading, result, start } = useGetJobs(); + + const isMlUser = hasMlUserPermissions(mlCapabilities); + const isLicensed = hasMlLicense(mlCapabilities); + + useEffect(() => { + if (isMlUser && isLicensed && jobIds.length > 0) { + start({ http, jobIds }); + } + }, [http, isMlUser, isLicensed, start, jobIds]); + + useEffect(() => { + if (result) { + setJobs(result); + } + }, [result]); + + useEffect(() => { + if (error) { + addError(error, { title: i18n.SIEM_JOB_FETCH_FAILURE }); + } + }, [addError, error]); + + return { isLicensed, isMlUser, jobs, loading }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index cf8204478a955..9eb0a97a1c9a2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -21,7 +21,6 @@ import { TimelineId } from '../../../../../common/types/timeline'; import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; import { Status, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { isThresholdRule } from '../../../../../common/detection_engine/utils'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { timelineActions } from '../../../../timelines/store/timeline'; import { EventsTd, EventsTdContent } from '../../../../timelines/components/timeline/styles'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; @@ -75,11 +74,17 @@ const AlertContextMenuComponent: React.FC = ({ '', [ecsRowData] ); - const ruleIndices = useMemo( - (): string[] => - (ecsRowData.signal?.rule && ecsRowData.signal.rule.index) ?? DEFAULT_INDEX_PATTERN, - [ecsRowData] - ); + const ruleIndices = useMemo((): string[] => { + if ( + ecsRowData.signal?.rule && + ecsRowData.signal.rule.index && + ecsRowData.signal.rule.index.length > 0 + ) { + return ecsRowData.signal.rule.index; + } else { + return DEFAULT_INDEX_PATTERN; + } + }, [ecsRowData]); const { addWarning } = useAppToasts(); @@ -321,7 +326,7 @@ const AlertContextMenuComponent: React.FC = ({ const areExceptionsAllowed = useMemo((): boolean => { const ruleTypes = getOr([], 'signal.rule.type', ecsRowData); const [ruleType] = ruleTypes as Type[]; - return !isMlRule(ruleType) && !isThresholdRule(ruleType); + return !isThresholdRule(ruleType); }, [ecsRowData]); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 2479a260872be..40b73fc7d158c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -8,7 +8,6 @@ import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleStepProps, @@ -76,10 +75,7 @@ const StepAboutRuleComponent: FC = ({ const [severityValue, setSeverityValue] = useState(initialState.severity.value); const [indexPatternLoading, { indexPatterns }] = useFetchIndex(defineRuleData?.index ?? []); - const canUseExceptions = - defineRuleData?.ruleType && - !isMlRule(defineRuleData.ruleType) && - !isThresholdRule(defineRuleData.ruleType); + const canUseExceptions = defineRuleData?.ruleType && !isThresholdRule(defineRuleData.ruleType); const { form } = useForm({ defaultValue: initialState, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 4866824f882cf..d04980d764831 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -80,7 +80,6 @@ import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports'; -import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; import { showGlobalFilters } from '../../../../../timelines/components/timeline/helpers'; @@ -104,7 +103,7 @@ enum RuleDetailTabs { } const getRuleDetailsTabs = (rule: Rule | null) => { - const canUseExceptions = rule && !isMlRule(rule.type) && !isThresholdRule(rule.type); + const canUseExceptions = rule && !isThresholdRule(rule.type); return [ { id: RuleDetailTabs.alerts, diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index c2c82639bf7d5..11caab837a766 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -118,7 +118,10 @@ const normalizeTrustedAppsPageLocation = ( * @param query * @param key */ -export const extractFirstParamValue = (query: querystring.ParsedUrlQuery, key: string): string => { +export const extractFirstParamValue = ( + query: querystring.ParsedUrlQuery, + key: string +): string | undefined => { const value = query[key]; return Array.isArray(value) ? value[value.length - 1] : value; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 1901f3589104a..05c3ac0faea69 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -186,7 +186,7 @@ export const uiQueryParams: ( typeof query[key] === 'string' ? (query[key] as string) : Array.isArray(query[key]) - ? (query[key][query[key].length - 1] as string) + ? (query[key] as string[])[(query[key] as string[]).length - 1] : undefined; if (value !== undefined) { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx similarity index 60% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx index 21fe14df81dd2..072f588663c5a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx @@ -14,6 +14,29 @@ import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/se import { usePolicyDetailsSelector } from '../../policy_hooks'; import { ConfigForm } from '../../components/config_form'; +const TRANSLATIONS: Readonly<{ [K in 'title' | 'description' | 'label']: string }> = { + title: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type', + { + defaultMessage: 'Register as anti-virus', + } + ), + description: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', + { + defaultMessage: + 'Toggle on to register Elastic as an official Anti-Virus solution for Windows OS. ' + + 'This will also disable Windows Defender.', + } + ), + label: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle', + { + defaultMessage: 'Register as anti-virus', + } + ), +}; + export const AntivirusRegistrationForm = memo(() => { const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); const dispatch = useDispatch(); @@ -30,31 +53,11 @@ export const AntivirusRegistrationForm = memo(() => { ); return ( - - - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', - { - defaultMessage: 'Switch the toggle to on to register Elastic anti-virus', - } - )} - + + {TRANSLATIONS.description} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx index 30c35de9b907f..ce5eb03d60cd0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -58,12 +58,12 @@ export const ConfigForm: FC = memo( {TITLES.type} {type} - + {TITLES.os} {supportedOss.map((os) => OS_TITLES[os]).join(', ')} - + {rightCorner} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx new file mode 100644 index 0000000000000..7cdf54316e4e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCheckbox, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui'; +import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { OS } from '../../../types'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; + +const OPERATING_SYSTEM_TO_TEST_SUBJ: { [K in OperatingSystem]: string } = { + [OperatingSystem.WINDOWS]: 'Windows', + [OperatingSystem.LINUX]: 'Linux', + [OperatingSystem.MAC]: 'Mac', +}; + +interface OperatingSystemToOsMap { + [OperatingSystem.WINDOWS]: OS.windows; + [OperatingSystem.LINUX]: OS.linux; + [OperatingSystem.MAC]: OS.mac; +} + +export type ProtectionField< + T extends OperatingSystem +> = keyof UIPolicyConfig[OperatingSystemToOsMap[T]]['events']; + +export type EventFormSelection = { [K in ProtectionField]: boolean }; + +export interface EventFormOption { + name: string; + protectionField: ProtectionField; +} + +export interface EventsFormProps { + os: T; + options: ReadonlyArray>; + selection: EventFormSelection; + onValueSelection: (value: ProtectionField, selected: boolean) => void; +} + +const countSelected = (selection: EventFormSelection) => { + return Object.values(selection).filter((value) => value).length; +}; + +export const EventsForm = ({ + os, + options, + selection, + onValueSelection, +}: EventsFormProps) => ( + + {i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { + defaultMessage: '{selected} / {total} event collections enabled', + values: { selected: countSelected(selection), total: options.length }, + })} + + } + > + + {i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', { + defaultMessage: 'Events', + })} + + + {options.map(({ name, protectionField }) => ( + onValueSelection(protectionField, event.target.checked)} + /> + ))} + +); + +EventsForm.displayName = 'EventsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index 95bf23b532f41..6d464280b2763 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useCallback, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, @@ -19,9 +19,11 @@ import { EuiContextMenuPanelProps, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useDispatch } from 'react-redux'; import { pagePathGetters, PackagePolicyEditExtensionComponentProps, + NewPackagePolicy, } from '../../../../../../../fleet/public'; import { getPolicyDetailPath, getTrustedAppsListPath } from '../../../../common/routing'; import { MANAGEMENT_APP_ID } from '../../../../common/constants'; @@ -31,13 +33,17 @@ import { } from '../../../../../../common/endpoint/types'; import { useKibana } from '../../../../../common/lib/kibana'; import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { PolicyDetailsForm } from '../policy_details_form'; +import { AppAction } from '../../../../../common/store/actions'; +import { usePolicyDetailsSelector } from '../policy_hooks'; +import { policyDetailsForUpdate } from '../../store/policy_details/selectors'; /** * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy */ export const EndpointPolicyEditExtension = memo( - ({ policy }) => { + ({ policy, onChange }) => { return ( <> @@ -46,12 +52,81 @@ export const EndpointPolicyEditExtension = memo + + ); } ); EndpointPolicyEditExtension.displayName = 'EndpointPolicyEditExtension'; +const WrappedPolicyDetailsForm = memo<{ + policyId: string; + onChange: PackagePolicyEditExtensionComponentProps['onChange']; +}>(({ policyId, onChange }) => { + const dispatch = useDispatch<(a: AppAction) => void>(); + const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate); + const [, setLastUpdatedPolicy] = useState(updatedPolicy); + + // When the form is initially displayed, trigger the Redux middleware which is based on + // the location information stored via the `userChangedUrl` action. + useEffect(() => { + dispatch({ + type: 'userChangedUrl', + payload: { + hash: '', + pathname: getPolicyDetailPath(policyId, ''), + search: '', + }, + }); + + // When form is unloaded, reset the redux store + return () => { + dispatch({ + type: 'userChangedUrl', + payload: { + hash: '', + pathname: '/', + search: '', + }, + }); + }; + }, [dispatch, policyId]); + + useEffect(() => { + // Currently, the `onChange` callback provided by the fleet UI extension is regenerated every + // time the policy data is updated, which means this will go into a continious loop if we don't + // actually check to see if an update should be reported back to fleet + setLastUpdatedPolicy((prevState) => { + if (prevState === updatedPolicy) { + return prevState; + } + + if (updatedPolicy) { + onChange({ + isValid: true, + // send up only the updated policy data which is stored in the `inputs` section. + // All other attributes (like name, id) are updated from the Fleet form, so we want to + // ensure we don't override it. + updatedPolicy: { + // Casting is needed due to the use of `Immutable<>` in our store data + inputs: (updatedPolicy.inputs as unknown) as NewPackagePolicy['inputs'], + }, + }); + } + + return updatedPolicy; + }); + }, [onChange, updatedPolicy]); + + return ( +
+ +
+ ); +}); +WrappedPolicyDetailsForm.displayName = 'WrappedPolicyDetailsForm'; + const EditFlowMessage = memo<{ agentPolicyId: string; integrationPolicyId: string; @@ -82,17 +157,6 @@ const EditFlowMessage = memo<{ const handleClosePopup = useCallback(() => setIsMenuOpen(false), []); - const handleSecurityPolicyAction = useNavigateToAppEventHandler( - MANAGEMENT_APP_ID, - { - path: getPolicyDetailPath(integrationPolicyId), - state: { - onSaveNavigateTo: navigateBackToIngest, - onCancelNavigateTo: navigateBackToIngest, - }, - } - ); - const handleTrustedAppsAction = useNavigateToAppEventHandler( MANAGEMENT_APP_ID, { @@ -129,16 +193,6 @@ const EditFlowMessage = memo<{ const actionItems = useMemo(() => { return [ - - - , , ]; - }, [handleSecurityPolicyAction, handleTrustedAppsAction]); + }, [handleTrustedAppsAction]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx index b417bc9ad5d9c..78a83fa11ae3f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx @@ -5,14 +5,29 @@ */ import { lazy } from 'react'; -import { PackagePolicyEditExtensionComponent } from '../../../../../../../fleet/public'; +import { CoreStart } from 'kibana/public'; +import { + PackagePolicyEditExtensionComponent, + PackagePolicyEditExtensionComponentProps, +} from '../../../../../../../fleet/public'; +import { StartPlugins } from '../../../../../types'; + +export const getLazyEndpointPolicyEditExtension = ( + coreStart: CoreStart, + depsStart: Pick +) => { + return lazy(async () => { + const [{ withSecurityContext }, { EndpointPolicyEditExtension }] = await Promise.all([ + import('./with_security_context'), + import('./endpoint_policy_edit_extension'), + ]); -export const LazyEndpointPolicyEditExtension = lazy( - async () => { - const { EndpointPolicyEditExtension } = await import('./endpoint_policy_edit_extension'); return { - // FIXME: remove casting once old UI component registration is removed - default: (EndpointPolicyEditExtension as unknown) as PackagePolicyEditExtensionComponent, + default: withSecurityContext({ + coreStart, + depsStart, + WrappedComponent: EndpointPolicyEditExtension, + }), }; - } -); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx new file mode 100644 index 0000000000000..f65dbaf1087d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ComponentType, memo } from 'react'; +import { CoreStart } from 'kibana/public'; +import { combineReducers, createStore, compose, applyMiddleware } from 'redux'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { StartPlugins } from '../../../../../types'; +import { managementReducer } from '../../../../store/reducer'; +import { managementMiddlewareFactory } from '../../../../store/middleware'; + +type ComposeType = typeof compose; +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: ComposeType; + } +} +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + +interface WithSecurityContextProps

{ + coreStart: CoreStart; + depsStart: Pick; + WrappedComponent: ComponentType

; +} + +/** + * Returns a new component that wraps the provided `WrappedComponent` in a bare minimum set of rendering context + * needed to render Security Solution components that may be dependent on a Redux store and/or Security Solution + * specific context based functionality + * + * @param coreStart + * @param depsStart + * @param WrappedComponent + */ +export const withSecurityContext =

({ + coreStart, + depsStart, + WrappedComponent, +}: WithSecurityContextProps

): ComponentType

=> { + let store: ReturnType; // created on first render + + return memo((props) => { + if (!store) { + // Most of the code here was copied form + // x-pack/plugins/security_solution/public/management/index.ts + store = createStore( + combineReducers({ + management: managementReducer, + }), + { management: undefined }, + composeEnhancers(applyMiddleware(...managementMiddlewareFactory(coreStart, depsStart))) + ); + } + + return ( + + + + ); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 9c11bc6f5a4d1..666e27c9d9a26 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -10,7 +10,6 @@ import { EuiFlexItem, EuiButton, EuiButtonEmpty, - EuiText, EuiSpacer, EuiOverlayMask, EuiConfirmModal, @@ -34,9 +33,6 @@ import { import { useKibana, toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; -import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; -import { MalwareProtections } from './policy_forms/protections/malware'; -import { AntivirusRegistrationForm } from './policy_forms/antivirus_registration'; import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; @@ -48,7 +44,7 @@ import { MANAGEMENT_APP_ID } from '../../../common/constants'; import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; import { WrapperPage } from '../../../../common/components/wrapper_page'; import { HeaderPage } from '../../../../common/components/header_page'; -import { AdvancedPolicyForms } from './policy_advanced'; +import { PolicyDetailsForm } from './policy_details_form'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -71,7 +67,6 @@ export const PolicyDetails = React.memo(() => { // Local state const [showConfirm, setShowConfirm] = useState(false); const [routeState, setRouteState] = useState(); - const [showAdvancedPolicy, setShowAdvancedPolicy] = useState(false); const policyName = policyItem?.name ?? ''; const hostListRouterPath = getEndpointListPath({ name: 'endpointList' }); @@ -111,9 +106,11 @@ export const PolicyDetails = React.memo(() => { } }, [navigateToApp, toasts, policyName, policyUpdateStatus, routeState]); + const routingOnCancelNavigateTo = routeState?.onCancelNavigateTo; const navigateToAppArguments = useMemo((): Parameters => { - return routeState?.onCancelNavigateTo ?? [MANAGEMENT_APP_ID, { path: hostListRouterPath }]; - }, [hostListRouterPath, routeState?.onCancelNavigateTo]); + return routingOnCancelNavigateTo ?? [MANAGEMENT_APP_ID, { path: hostListRouterPath }]; + }, [hostListRouterPath, routingOnCancelNavigateTo]); + const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments); const handleSaveOnClick = useCallback(() => { @@ -131,10 +128,6 @@ export const PolicyDetails = React.memo(() => { setShowConfirm(false); }, []); - const handleAdvancedPolicyClick = useCallback(() => { - setShowAdvancedPolicy(!showAdvancedPolicy); - }, [showAdvancedPolicy]); - useEffect(() => { if (!routeState && locationRouteState) { setRouteState(locationRouteState); @@ -224,48 +217,7 @@ export const PolicyDetails = React.memo(() => { {headerRightContent} - -

- -

- - - - - - - -

- -

-
- - - - - - - - - - - - - - - - - {showAdvancedPolicy && } + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx new file mode 100644 index 0000000000000..a0bf2b37e8a12 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MalwareProtections } from './policy_forms/protections/malware'; +import { LinuxEvents, MacEvents, WindowsEvents } from './policy_forms/events'; +import { AdvancedPolicyForms } from './policy_advanced'; +import { AntivirusRegistrationForm } from './components/antivirus_registration_form'; + +export const PolicyDetailsForm = memo(() => { + const [showAdvancedPolicy, setShowAdvancedPolicy] = useState(false); + const handleAdvancedPolicyClick = useCallback(() => { + setShowAdvancedPolicy(!showAdvancedPolicy); + }, [showAdvancedPolicy]); + + return ( + <> + +

+ +

+
+ + + + + + +

+ +

+
+ + + + + + + + + + + + + + + + + {showAdvancedPolicy && } + + ); +}); +PolicyDetailsForm.displayName = 'PolicyDetailsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx deleted file mode 100644 index 76077831c670b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx +++ /dev/null @@ -1,55 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useMemo } from 'react'; -import { EuiCheckbox, EuiCheckboxProps, htmlIdGenerator } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { PolicyDetailsAction } from '../../../store/policy_details'; -import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; - -type EventsCheckboxProps = Omit & { - name: string; - setter: (config: UIPolicyConfig, checked: boolean) => UIPolicyConfig; - getter: (config: UIPolicyConfig) => boolean; -}; - -export const EventsCheckbox = React.memo(function ({ - name, - setter, - getter, - ...otherProps -}: EventsCheckboxProps) { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const selected = getter(policyDetailsConfig); - const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); - const checkboxId = useMemo(() => htmlIdGenerator()(), []); - - const handleCheckboxChange = useCallback( - (event: React.ChangeEvent) => { - if (policyDetailsConfig) { - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: setter(policyDetailsConfig, event.target.checked) }, - }); - } - }, - [dispatch, policyDetailsConfig, setter] - ); - - return ( - - ); -}); - -EventsCheckbox.displayName = 'EventsCheckbox'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index 999e3bac5653a..f9532eaecf701 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -4,96 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const LinuxEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedLinuxEvents); - const total = usePolicyDetailsSelector(totalLinuxEvents); - - const checkboxes = useMemo(() => { - const items: Array<{ - name: string; - os: 'linux'; - protectionField: keyof UIPolicyConfig['linux']['events']; - }> = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.linux, - protectionField: 'file', - }, +const OPTIONS: ReadonlyArray> = [ + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file', { + defaultMessage: 'File', + }), + protectionField: 'file', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.linux, - protectionField: 'process', - }, + defaultMessage: 'Process', + } + ), + protectionField: 'process', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.linux, - protectionField: 'network', - }, - ]; - return ( - <> - {EVENTS_HEADING} - - {items.map((item, index) => { - return ( - - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - - ); - }, []); + defaultMessage: 'Network', + } + ), + protectionField: 'network', + }, +]; + +export const LinuxEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - + + os={OperatingSystem.LINUX} + selection={policyDetailsConfig.linux.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: setIn(policyDetailsConfig)('linux')('events')(value)(selected) }, + }) } - > - {checkboxes} - + /> ); }); + +LinuxEvents.displayName = 'LinuxEvents'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index 6e15a3c4cd43b..ac6ae531ba172 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -4,96 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const MacEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedMacEvents); - const total = usePolicyDetailsSelector(totalMacEvents); +const OPTIONS: ReadonlyArray> = [ + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file', { + defaultMessage: 'File', + }), + protectionField: 'file', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.process', { + defaultMessage: 'Process', + }), + protectionField: 'process', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network', { + defaultMessage: 'Network', + }), + protectionField: 'network', + }, +]; - const checkboxes = useMemo(() => { - const items: Array<{ - name: string; - os: 'mac'; - protectionField: keyof UIPolicyConfig['mac']['events']; - }> = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.mac, - protectionField: 'file', - }, - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.mac, - protectionField: 'process', - }, - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.mac, - protectionField: 'network', - }, - ]; - return ( - <> - {EVENTS_HEADING} - - {items.map((item, index) => { - return ( - - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - - ); - }, []); +export const MacEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - + + os={OperatingSystem.MAC} + selection={policyDetailsConfig.mac.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: setIn(policyDetailsConfig)('mac')('events')(value)(selected) }, + }) } - > - {checkboxes} - + /> ); }); + +MacEvents.displayName = 'MacEvents'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts deleted file mode 100644 index 3b48b7969a8ce..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const EVENTS_HEADING = i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', - { - defaultMessage: 'Events', - } -); - -export const EVENTS_FORM_TYPE_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.eventCollection', - { - defaultMessage: 'Event Collection', - } -); - -export const COLLECTIONS_ENABLED_MESSAGE = (selected: number, total: number) => { - return i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { - defaultMessage: '{selected} / {total} event collections enabled', - values: { selected, total }, - }); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index c381249cf24b9..c99f2a6b72ac3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -4,142 +4,97 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { - Immutable, - OperatingSystem, - UIPolicyConfig, -} from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const WindowsEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedWindowsEvents); - const total = usePolicyDetailsSelector(totalWindowsEvents); - - const checkboxes = useMemo(() => { - const items: Immutable< - Array<{ - name: string; - os: 'windows'; - protectionField: keyof UIPolicyConfig['windows']['events']; - }> - > = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', - { - defaultMessage: 'DLL and Driver Load', - } - ), - os: OS.windows, - protectionField: 'dll_and_driver_load', - }, +const OPTIONS: ReadonlyArray> = [ + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dns', - { - defaultMessage: 'DNS', - } - ), - os: OS.windows, - protectionField: 'dns', - }, + defaultMessage: 'DLL and Driver Load', + } + ), + protectionField: 'dll_and_driver_load', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dns', { + defaultMessage: 'DNS', + }), + protectionField: 'dns', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.file', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.windows, - protectionField: 'file', - }, + defaultMessage: 'File', + } + ), + protectionField: 'file', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.network', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.windows, - protectionField: 'network', - }, + defaultMessage: 'Network', + } + ), + protectionField: 'network', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.process', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.windows, - protectionField: 'process', - }, + defaultMessage: 'Process', + } + ), + protectionField: 'process', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.registry', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.registry', - { - defaultMessage: 'Registry', - } - ), - os: OS.windows, - protectionField: 'registry', - }, + defaultMessage: 'Registry', + } + ), + protectionField: 'registry', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.security', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.security', - { - defaultMessage: 'Security', - } - ), - os: OS.windows, - protectionField: 'security', - }, - ]; - return ( - <> - {EVENTS_HEADING} - - {items.map((item, index) => { - return ( - - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - - ); - }, []); + defaultMessage: 'Security', + } + ), + protectionField: 'security', + }, +]; + +export const WindowsEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - + + os={OperatingSystem.WINDOWS} + selection={policyDetailsConfig.windows.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { + policyConfig: setIn(policyDetailsConfig)('windows')('events')(value)(selected), + }, + }) } - > - {checkboxes} - + /> ); }); + +WindowsEvents.displayName = 'WindowsEvents'; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index b81be3249953e..4f37b5b15d73a 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -61,7 +61,7 @@ import { import { SecurityAppStore } from './common/store/store'; import { getCaseConnectorUI } from './cases/components/connectors'; import { licenseService } from './common/hooks/use_license'; -import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; +import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; export class Plugin implements IPlugin { @@ -337,7 +337,7 @@ export class Plugin implements IPlugin PolicyConfig): PackagePolicy => { + const packagePolicy = createPackagePolicyMock(); + if (!cb) { + // eslint-disable-next-line no-param-reassign + cb = (p) => p; + } + const policyConfig = cb(factory()); + packagePolicy.inputs[0].config = { policy: { value: policyConfig } }; + return packagePolicy; +}; + +describe('Policy-Changing license watcher', () => { + const logger = loggingSystemMock.create().get('license_watch.test'); + const soStartMock = savedObjectsServiceMock.createStartContract(); + let packagePolicySvcMock: jest.Mocked; + + const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); + const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); + const Basic = licenseMock.createLicense({ license: { type: 'basic', mode: 'basic' } }); + + beforeEach(() => { + packagePolicySvcMock = createPackagePolicyServiceMock(); + }); + + it('is activated on license changes', () => { + // mock a license-changing service to test reactivity + const licenseEmitter: Subject = new Subject(); + const licenseService = new LicenseService(); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + + // swap out watch function, just to ensure it gets called when a license change happens + const mockWatch = jest.fn(); + pw.watch = mockWatch; + + // licenseService is watching our subject for incoming licenses + licenseService.start(licenseEmitter); + pw.start(licenseService); // and the PolicyWatcher under test, uses that to subscribe as well + + // Enqueue a license change! + licenseEmitter.next(Platinum); + + // policywatcher should have triggered + expect(mockWatch.mock.calls.length).toBe(1); + + pw.stop(); + licenseService.stop(); + licenseEmitter.complete(); + }); + + it('pages through all endpoint policies', async () => { + const TOTAL = 247; + + // set up the mocked package policy service to return and do what we want + packagePolicySvcMock.list + .mockResolvedValueOnce({ + items: Array.from({ length: 100 }, () => MockPPWithEndpointPolicy()), + total: TOTAL, + page: 1, + perPage: 100, + }) + .mockResolvedValueOnce({ + items: Array.from({ length: 100 }, () => MockPPWithEndpointPolicy()), + total: TOTAL, + page: 2, + perPage: 100, + }) + .mockResolvedValueOnce({ + items: Array.from({ length: TOTAL - 200 }, () => MockPPWithEndpointPolicy()), + total: TOTAL, + page: 3, + perPage: 100, + }); + + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + await pw.watch(Gold); // just manually trigger with a given license + + expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts + + // Assert: on the first call to packagePolicy.list, we asked for page 1 + expect(packagePolicySvcMock.list.mock.calls[0][1].page).toBe(1); + expect(packagePolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2 + expect(packagePolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc + }); + + it('alters no-longer-licensed features', async () => { + const CustomMessage = 'Custom string'; + + // mock a Policy with a higher-tiered feature enabled + packagePolicySvcMock.list.mockResolvedValueOnce({ + items: [ + MockPPWithEndpointPolicy( + (pc: PolicyConfig): PolicyConfig => { + pc.windows.popup.malware.message = CustomMessage; + return pc; + } + ), + ], + total: 1, + page: 1, + perPage: 100, + }); + + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + + // emulate a license change below paid tier + await pw.watch(Basic); + + expect(packagePolicySvcMock.update).toHaveBeenCalled(); + expect( + packagePolicySvcMock.update.mock.calls[0][2].inputs[0].config!.policy.value.windows.popup + .malware.message + ).not.toEqual(CustomMessage); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts new file mode 100644 index 0000000000000..cae3b9f33850a --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Subscription } from 'rxjs'; + +import { + KibanaRequest, + Logger, + SavedObjectsClientContract, + SavedObjectsServiceStart, +} from 'src/core/server'; +import { PackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../fleet/common'; +import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; +import { ILicense } from '../../../../../licensing/common/types'; +import { + isEndpointPolicyValidForLicense, + unsetPolicyFeaturesAboveLicenseLevel, +} from '../../../../common/license/policy_config'; +import { isAtLeast, LicenseService } from '../../../../common/license/license'; + +export class PolicyWatcher { + private logger: Logger; + private soClient: SavedObjectsClientContract; + private policyService: PackagePolicyServiceInterface; + private subscription: Subscription | undefined; + constructor( + policyService: PackagePolicyServiceInterface, + soStart: SavedObjectsServiceStart, + logger: Logger + ) { + this.policyService = policyService; + this.soClient = this.makeInternalSOClient(soStart); + this.logger = logger; + } + + /** + * The policy watcher is not called as part of a HTTP request chain, where the + * request-scoped SOClient could be passed down. It is called via license observable + * changes. We are acting as the 'system' in response to license changes, so we are + * intentionally using the system user here. Be very aware of what you are using this + * client to do + */ + private makeInternalSOClient(soStart: SavedObjectsServiceStart): SavedObjectsClientContract { + const fakeRequest = ({ + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { href: {} }, + raw: { req: { url: '/' } }, + } as unknown) as KibanaRequest; + return soStart.getScopedClient(fakeRequest, { excludedWrappers: ['security'] }); + } + + public start(licenseService: LicenseService) { + this.subscription = licenseService.getLicenseInformation$()?.subscribe(this.watch.bind(this)); + } + + public stop() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public async watch(license: ILicense) { + if (isAtLeast(license, 'platinum')) { + return; + } + + let page = 1; + let response: { + items: PackagePolicy[]; + total: number; + page: number; + perPage: number; + }; + do { + try { + response = await this.policyService.list(this.soClient, { + page: page++, + perPage: 100, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, + }); + } catch (e) { + this.logger.warn( + `Unable to verify endpoint policies in line with license change: failed to fetch package policies: ${e.message}` + ); + return; + } + response.items.forEach(async (policy) => { + const policyConfig = policy.inputs[0].config?.policy.value; + if (!isEndpointPolicyValidForLicense(policyConfig, license)) { + policy.inputs[0].config!.policy.value = unsetPolicyFeaturesAboveLicenseLevel( + policyConfig, + license + ); + try { + await this.policyService.update(this.soClient, policy.id, policy); + } catch (e) { + // try again for transient issues + try { + await this.policyService.update(this.soClient, policy.id, policy); + } catch (ee) { + this.logger.warn( + `Unable to remove platinum features from policy ${policy.id}: ${ee.message}` + ); + } + } + } + }); + } while (response.page * response.perPage < response.total); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 0a38bdc790b41..764604a793788 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -150,8 +150,8 @@ export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): Sig export const sampleDocWithSortId = ( someUuid: string = sampleIdGuid, - ip?: string, - destIp?: string + ip?: string | string[], + destIp?: string | string[] ): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', @@ -502,8 +502,8 @@ export const repeatedSearchResultsWithSortId = ( total: number, pageSize: number, guids: string[], - ips?: string[], - destIps?: string[] + ips?: Array, + destIps?: Array ): SignalSearchResponse => ({ took: 10, timed_out: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 5c2dfa62e5951..d530fe10c6498 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -6,7 +6,6 @@ import { flow, omit } from 'lodash/fp'; import set from 'set-value'; -import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; import { AlertServices } from '../../../../../alerts/server'; @@ -15,6 +14,7 @@ import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { AnomalyResults, Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; +import { SearchResponse } from '../../types'; interface BulkCreateMlSignalsParams { actions: RuleAlertAction[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 3334cc17b9050..01e7e7160e1ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -400,6 +400,87 @@ describe('filterEventsAgainstList', () => { '9.9.9.9', ]).toEqual(ipVals); }); + + it('should respond with less items in the list given one exception item with two entries of type list and array of values in document', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + { + field: 'destination.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys-again.txt', + type: 'ip', + }, + }, + ]; + + // this call represents an exception list with a value list containing ['2.2.2.2'] + (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getListItemResponseMock(), value: '2.2.2.2' }, + ]); + // this call represents an exception list with a value list containing ['4.4.4.4'] + (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getListItemResponseMock(), value: '4.4.4.4' }, + ]); + + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient, + exceptionsList: [exceptionItem], + eventSearchResult: repeatedSearchResultsWithSortId( + 3, + 3, + someGuids.slice(0, 3), + [ + ['1.1.1.1', '1.1.1.1'], + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ], + [ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ['3.3.3.3', '4.4.4.4'], + ] + ), + buildRuleMessage, + }); + expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + ]); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[1][0].value).toEqual([ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '4.4.4.4', + ]); + expect(res.hits.hits.length).toEqual(2); + + // @ts-expect-error + const sourceIpVals = res.hits.hits.map((item) => item._source.source.ip); + expect([ + ['1.1.1.1', '1.1.1.1'], + ['1.1.1.1', '2.2.2.2'], + ]).toEqual(sourceIpVals); + // @ts-expect-error + const destIpVals = res.hits.hits.map((item) => item._source.destination.ip); + expect([ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ]).toEqual(destIpVals); + }); }); describe('operator type is excluded', () => { it('should respond with empty list if no items match value list', async () => { @@ -463,5 +544,86 @@ describe('filterEventsAgainstList', () => { ); expect(res.hits.hits.length).toEqual(2); }); + + it('should respond with less items in the list given one exception item with two entries of type list and array of values in document', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + { + field: 'destination.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys-again.txt', + type: 'ip', + }, + }, + ]; + + // this call represents an exception list with a value list containing ['2.2.2.2'] + (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getListItemResponseMock(), value: '2.2.2.2' }, + ]); + // this call represents an exception list with a value list containing ['4.4.4.4'] + (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getListItemResponseMock(), value: '4.4.4.4' }, + ]); + + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient, + exceptionsList: [exceptionItem], + eventSearchResult: repeatedSearchResultsWithSortId( + 3, + 3, + someGuids.slice(0, 3), + [ + ['1.1.1.1', '1.1.1.1'], + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ], + [ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ['3.3.3.3', '4.4.4.4'], + ] + ), + buildRuleMessage, + }); + expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + ]); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[1][0].value).toEqual([ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '4.4.4.4', + ]); + expect(res.hits.hits.length).toEqual(2); + + // @ts-expect-error + const sourceIpVals = res.hits.hits.map((item) => item._source.source.ip); + expect([ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ]).toEqual(sourceIpVals); + // @ts-expect-error + const destIpVals = res.hits.hits.map((item) => item._source.destination.ip); + expect([ + ['2.2.2.2', '3.3.3.3'], + ['3.3.3.3', '4.4.4.4'], + ]).toEqual(destIpVals); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index 908d073fbeabd..1c13de16d9b1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -7,7 +7,6 @@ import { get } from 'lodash/fp'; import { Logger } from 'src/core/server'; import { ListClient } from '../../../../../lists/server'; -import { SignalSearchResponse } from './types'; import { BuildRuleMessage } from './rule_messages'; import { EntryList, @@ -17,16 +16,23 @@ import { } from '../../../../../lists/common/schemas'; import { hasLargeValueList } from '../../../../common/detection_engine/utils'; import { SearchTypes } from '../../../../common/detection_engine/types'; +import { SearchResponse } from '../../types'; -interface FilterEventsAgainstList { - listClient: ListClient; - exceptionsList: ExceptionListItemSchema[]; - logger: Logger; - eventSearchResult: SignalSearchResponse; - buildRuleMessage: BuildRuleMessage; -} +// narrow unioned type to be single +const isStringableType = (val: SearchTypes): val is string | number | boolean => + ['string', 'number', 'boolean'].includes(typeof val); + +const isStringableArray = (val: SearchTypes): val is Array => { + if (!Array.isArray(val)) { + return false; + } + // TS does not allow .every to be called on val as-is, even though every type in the union + // is an array. https://github.com/microsoft/TypeScript/issues/36390 + // @ts-expect-error + return val.every((subVal) => isStringableType(subVal)); +}; -export const createSetToFilterAgainst = async ({ +export const createSetToFilterAgainst = async ({ events, field, listId, @@ -35,7 +41,7 @@ export const createSetToFilterAgainst = async ({ logger, buildRuleMessage, }: { - events: SignalSearchResponse['hits']['hits']; + events: SearchResponse['hits']['hits']; field: string; listId: string; listType: Type; @@ -43,13 +49,14 @@ export const createSetToFilterAgainst = async ({ logger: Logger; buildRuleMessage: BuildRuleMessage; }): Promise> => { - // narrow unioned type to be single - const isStringableType = (val: SearchTypes) => - ['string', 'number', 'boolean'].includes(typeof val); const valuesFromSearchResultField = events.reduce((acc, searchResultItem) => { const valueField = get(field, searchResultItem._source); - if (valueField != null && isStringableType(valueField)) { - acc.add(valueField.toString()); + if (valueField != null) { + if (isStringableType(valueField)) { + acc.add(valueField.toString()); + } else if (isStringableArray(valueField)) { + valueField.forEach((subVal) => acc.add(subVal.toString())); + } } return acc; }, new Set()); @@ -71,13 +78,19 @@ export const createSetToFilterAgainst = async ({ return matchedListItemsSet; }; -export const filterEventsAgainstList = async ({ +export const filterEventsAgainstList = async ({ listClient, exceptionsList, logger, eventSearchResult, buildRuleMessage, -}: FilterEventsAgainstList): Promise => { +}: { + listClient: ListClient; + exceptionsList: ExceptionListItemSchema[]; + logger: Logger; + eventSearchResult: SearchResponse; + buildRuleMessage: BuildRuleMessage; +}): Promise> => { try { if (exceptionsList == null || exceptionsList.length === 0) { logger.debug(buildRuleMessage('about to return original search result')); @@ -108,9 +121,9 @@ export const filterEventsAgainstList = async ({ }); // now that we have all the exception items which are value lists (whether single entry or have multiple entries) - const res = await valueListExceptionItems.reduce>( + const res = await valueListExceptionItems.reduce['hits']['hits']>>( async ( - filteredAccum: Promise, + filteredAccum: Promise['hits']['hits']>, exceptionItem: ExceptionListItemSchema ) => { // 1. acquire the values from the specified fields to check @@ -152,15 +165,23 @@ export const filterEventsAgainstList = async ({ const vals = fieldAndSetTuples.map((tuple) => { const eventItem = get(tuple.field, item._source); if (tuple.operator === 'included') { - // only create a signal if the event is not in the value list + // only create a signal if the field value is not in the value list if (eventItem != null) { - return !tuple.matchedSet.has(eventItem); + if (isStringableType(eventItem)) { + return !tuple.matchedSet.has(eventItem); + } else if (isStringableArray(eventItem)) { + return !eventItem.some((val) => tuple.matchedSet.has(val)); + } } return true; } else if (tuple.operator === 'excluded') { - // only create a signal if the event is in the value list + // only create a signal if the field value is in the value list if (eventItem != null) { - return tuple.matchedSet.has(eventItem); + if (isStringableType(eventItem)) { + return tuple.matchedSet.has(eventItem); + } else if (isStringableArray(eventItem)) { + return eventItem.some((val) => tuple.matchedSet.has(val)); + } } return true; } @@ -175,10 +196,10 @@ export const filterEventsAgainstList = async ({ const toReturn = filteredEvents; return toReturn; }, - Promise.resolve(eventSearchResult.hits.hits) + Promise.resolve['hits']['hits']>(eventSearchResult.hits.hits) ); - const toReturn: SignalSearchResponse = { + const toReturn: SearchResponse = { took: eventSearchResult.took, timed_out: eventSearchResult.timed_out, _shards: eventSearchResult._shards, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts index 94b73fce79f0c..ec653f088b523 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts @@ -5,6 +5,7 @@ */ import dateMath from '@elastic/datemath'; +import { ExceptionListItemSchema } from '../../../../../lists/common'; import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../../ml/server'; @@ -18,6 +19,7 @@ export const findMlSignals = async ({ anomalyThreshold, from, to, + exceptionItems, }: { ml: MlPluginSetup; request: KibanaRequest; @@ -26,6 +28,7 @@ export const findMlSignals = async ({ anomalyThreshold: number; from: string; to: string; + exceptionItems: ExceptionListItemSchema[]; }): Promise => { const { mlAnomalySearch } = ml.mlSystemProvider(request, savedObjectsClient); const params = { @@ -33,6 +36,7 @@ export const findMlSignals = async ({ threshold: anomalyThreshold, earliestMs: dateMath.parse(from)?.valueOf() ?? 0, latestMs: dateMath.parse(to)?.valueOf() ?? 0, + exceptionItems, }; return getAnomalies(params, mlAnomalySearch); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index f0b1825c7cc99..d6bdc14a92b40 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -66,6 +66,7 @@ import { buildSignalFromEvent, buildSignalGroupFromSequence } from './build_bulk import { createThreatSignals } from './threat_mapping/create_threat_signals'; import { getIndexVersion } from '../routes/index/get_index_version'; import { MIN_EQL_RULE_INDEX_VERSION } from '../routes/index/get_signals_template'; +import { filterEventsAgainstList } from './filter_events_with_list'; export const signalRulesAlertType = ({ logger, @@ -242,9 +243,18 @@ export const signalRulesAlertType = ({ anomalyThreshold, from, to, + exceptionItems: exceptionItems ?? [], + }); + + const filteredAnomalyResults = await filterEventsAgainstList({ + listClient, + exceptionsList: exceptionItems ?? [], + logger, + eventSearchResult: anomalyResults, + buildRuleMessage, }); - const anomalyCount = anomalyResults.hits.hits.length; + const anomalyCount = filteredAnomalyResults.hits.hits.length; if (anomalyCount) { logger.info(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`)); } @@ -257,7 +267,7 @@ export const signalRulesAlertType = ({ } = await bulkCreateMlSignals({ actions, throttle, - someResult: anomalyResults, + someResult: filteredAnomalyResults, ruleParams: params, services, logger, @@ -276,15 +286,16 @@ export const signalRulesAlertType = ({ }); // The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] } const shardFailures = - (anomalyResults._shards as typeof anomalyResults._shards & { failures: [] }).failures ?? - []; + (filteredAnomalyResults._shards as typeof filteredAnomalyResults._shards & { + failures: []; + }).failures ?? []; const searchErrors = createErrorsFromShard({ errors: shardFailures, }); result = mergeReturns([ result, createSearchAfterReturnType({ - success: success && anomalyResults._shards.failed === 0, + success: success && filteredAnomalyResults._shards.failed === 0, errors: [...errors, ...searchErrors], createdSignalsCount: createdItemsCount, bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts index 63e3f3487e482..d08b5e649451c 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getExceptionListItemSchemaMock } from '../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getAnomalies, AnomaliesSearchParams } from '.'; const getFiltersFromMock = (mock: jest.Mock) => { @@ -23,6 +24,7 @@ describe('getAnomalies', () => { threshold: 5, earliestMs: 1588517231429, latestMs: 1588617231429, + exceptionItems: [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()], }; }); diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index 34e004d817fe7..ec801f6c49ae7 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; import { RequestParams } from '@elastic/elasticsearch'; +import { ExceptionListItemSchema } from '../../../../lists/common'; +import { buildExceptionFilter } from '../../../common/detection_engine/get_query_filter'; import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; +import { SearchResponse } from '../types'; export { Anomaly }; export type AnomalyResults = SearchResponse; @@ -21,6 +23,7 @@ export interface AnomaliesSearchParams { threshold: number; earliestMs: number; latestMs: number; + exceptionItems: ExceptionListItemSchema[]; maxRecords?: number; } @@ -49,6 +52,17 @@ export const getAnomalies = async ( }, }, ], + must_not: buildExceptionFilter({ + lists: params.exceptionItems, + config: { + allowLeadingWildcards: true, + queryStringOptions: { analyze_wildcard: true }, + ignoreFilterIfFieldNotInIndex: false, + dateFormatTZ: 'Zulu', + }, + excludeExceptions: true, + chunkSize: 1024, + })?.query, }, }, sort: [{ record_score: { order: 'desc' } }], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts index 488da5025531d..c230e36e4c896 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts @@ -39,7 +39,7 @@ export const getReadables = (dataPath: string): Promise => const readable = fs.createReadStream(dataPath, { encoding: 'utf-8' }); readable.on('data', (stream) => { - contents.push(stream); + contents.push(stream as string); }); readable.on('end', () => { diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 088af40a84ae0..10e817bea0282 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -75,6 +75,7 @@ import { TelemetryPluginSetup, } from '../../../../src/plugins/telemetry/server'; import { licenseService } from './lib/license/license'; +import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; export interface SetupPlugins { alerts: AlertingSetup; @@ -127,6 +128,7 @@ export class Plugin implements IPlugin; + private policyWatcher?: PolicyWatcher; private manifestTask: ManifestTask | undefined; private exceptionsCache: LRU; @@ -370,7 +372,12 @@ export class Plugin implements IPlugin', () => { const { snapshotName } = POLICY_EDIT; // Complete step 1, change snapshot name - form.setInputValue('snapshotNameInput', `${snapshotName}-edited`); + const editedSnapshotName = `${snapshotName}-edited`; + form.setInputValue('snapshotNameInput', editedSnapshotName); actions.clickNextButton(); // Complete step 2, enable ignore unavailable indices switch @@ -143,20 +144,24 @@ describe('', () => { const latestRequest = server.requests[server.requests.length - 1]; + const { name, isManagedPolicy, schedule, repository, retention } = POLICY_EDIT; + const expected = { - ...POLICY_EDIT, - ...{ - config: { - ignoreUnavailable: true, - }, - retention: { - ...POLICY_EDIT.retention, - expireAfterValue: Number(EXPIRE_AFTER_VALUE), - expireAfterUnit: EXPIRE_AFTER_UNIT, - }, - snapshotName: `${POLICY_EDIT.snapshotName}-edited`, + name, + isManagedPolicy, + schedule, + repository, + config: { + ignoreUnavailable: true, + }, + retention: { + ...retention, + expireAfterValue: Number(EXPIRE_AFTER_VALUE), + expireAfterUnit: EXPIRE_AFTER_UNIT, }, + snapshotName: editedSnapshotName, }; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); }); @@ -180,10 +185,25 @@ describe('', () => { const latestRequest = server.requests[server.requests.length - 1]; + const { + name, + isManagedPolicy, + schedule, + repository, + retention, + config, + snapshotName, + } = POLICY_EDIT; + const expected = { - ...POLICY_EDIT, + name, + isManagedPolicy, + schedule, + repository, + config, + snapshotName, retention: { - ...POLICY_EDIT.retention, + ...retention, expireAfterValue: Number(EXPIRE_AFTER_VALUE), expireAfterUnit: TIME_UNITS.DAY, // default value }, diff --git a/x-pack/plugins/snapshot_restore/jest.config.js b/x-pack/plugins/snapshot_restore/jest.config.js new file mode 100644 index 0000000000000..e485eff0fb355 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/snapshot_restore'], +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx index 7af663b29957d..a119c96e0a1ec 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx @@ -64,8 +64,22 @@ export const PolicyEdit: React.FunctionComponent { - if (policyData && policyData.policy) { - setPolicy(policyData.policy); + if (policyData?.policy) { + const { policy: policyToEdit } = policyData; + + // The policy response includes data not pertinent to the form + // that we need to remove, e.g., lastSuccess, lastFailure, stats + const policyFormData: SlmPolicyPayload = { + name: policyToEdit.name, + snapshotName: policyToEdit.snapshotName, + schedule: policyToEdit.schedule, + repository: policyToEdit.repository, + config: policyToEdit.config, + retention: policyToEdit.retention, + isManagedPolicy: policyToEdit.isManagedPolicy, + }; + + setPolicy(policyFormData); } }, [policyData]); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts index e5df0ec33db0b..7a13b4ac27caa 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts @@ -26,20 +26,12 @@ const snapshotRetentionSchema = schema.object({ export const policySchema = schema.object({ name: schema.string(), - version: schema.maybe(schema.number()), - modifiedDate: schema.maybe(schema.string()), - modifiedDateMillis: schema.maybe(schema.number()), snapshotName: schema.string(), schedule: schema.string(), repository: schema.string(), - nextExecution: schema.maybe(schema.string()), - nextExecutionMillis: schema.maybe(schema.number()), config: schema.maybe(snapshotConfigSchema), retention: schema.maybe(snapshotRetentionSchema), isManagedPolicy: schema.boolean(), - stats: schema.maybe(schema.object({}, { unknowns: 'allow' })), - lastFailure: schema.maybe(schema.object({}, { unknowns: 'allow' })), - lastSuccess: schema.maybe(schema.object({}, { unknowns: 'allow' })), }); const fsRepositorySettings = schema.object({ diff --git a/x-pack/plugins/spaces/README.md b/x-pack/plugins/spaces/README.md new file mode 100644 index 0000000000000..89194253dce4a --- /dev/null +++ b/x-pack/plugins/spaces/README.md @@ -0,0 +1,10 @@ +# Kibana Spaces Plugin + +See [Configuring Kibana Spaces](https://www.elastic.co/guide/en/kibana/current/spaces-settings-kb.html). + +The spaces plugin enables Kibana Spaces, which provide isolation and organization +for saved objects. + +Spaces also allow for a creating a curated Kibana experience, by hiding features that aren't relevant to your users. + +Spaces can be combined with Security to further enhance the authorization model. diff --git a/x-pack/plugins/spaces/jest.config.js b/x-pack/plugins/spaces/jest.config.js new file mode 100644 index 0000000000000..c3e7db9a4c7c3 --- /dev/null +++ b/x-pack/plugins/spaces/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/spaces'], +}; diff --git a/x-pack/plugins/spaces/server/config.test.ts b/x-pack/plugins/spaces/server/config.test.ts new file mode 100644 index 0000000000000..d27498eb6e3fc --- /dev/null +++ b/x-pack/plugins/spaces/server/config.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; +import { deepFreeze } from '@kbn/std'; +import { spacesConfigDeprecationProvider } from './config'; + +const applyConfigDeprecations = (settings: Record = {}) => { + const deprecations = spacesConfigDeprecationProvider(configDeprecationFactory); + const deprecationMessages: string[] = []; + const migrated = applyDeprecations( + settings, + deprecations.map((deprecation) => ({ + deprecation, + path: '', + })), + (msg) => deprecationMessages.push(msg) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('spaces config', () => { + describe('deprecations', () => { + describe('enabled', () => { + it('logs a warning if xpack.spaces.enabled is set to false', () => { + const originalConfig = deepFreeze({ xpack: { spaces: { enabled: false } } }); + + const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); + + expect(messages).toMatchInlineSnapshot(` + Array [ + "Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)", + ] + `); + expect(migrated).toEqual(originalConfig); + }); + + it('does not log a warning if no settings are explicitly set', () => { + const originalConfig = deepFreeze({}); + + const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); + + expect(messages).toMatchInlineSnapshot(`Array []`); + expect(migrated).toEqual(originalConfig); + }); + + it('does not log a warning if xpack.spaces.enabled is set to true', () => { + const originalConfig = deepFreeze({ xpack: { spaces: { enabled: true } } }); + + const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); + + expect(messages).toMatchInlineSnapshot(`Array []`); + expect(migrated).toEqual(originalConfig); + }); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/config.ts b/x-pack/plugins/spaces/server/config.ts index a28624fb82c15..671b725ac1092 100644 --- a/x-pack/plugins/spaces/server/config.ts +++ b/x-pack/plugins/spaces/server/config.ts @@ -5,7 +5,11 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import type { + PluginInitializerContext, + ConfigDeprecationProvider, + ConfigDeprecation, +} from 'src/core/server'; import { Observable } from 'rxjs'; export const ConfigSchema = schema.object({ @@ -17,6 +21,19 @@ export function createConfig$(context: PluginInitializerContext) { return context.config.create>(); } +const disabledDeprecation: ConfigDeprecation = (config, fromPath, log) => { + if (config.xpack?.spaces?.enabled === false) { + log( + `Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)` + ); + } + return config; +}; + +export const spacesConfigDeprecationProvider: ConfigDeprecationProvider = () => { + return [disabledDeprecation]; +}; + export type ConfigType = ReturnType extends Observable ? P : ReturnType; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 85f1facf6131c..4d3d184ec41a3 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; -import { ConfigSchema } from './config'; +import type { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; +import { ConfigSchema, spacesConfigDeprecationProvider } from './config'; import { Plugin } from './plugin'; // These exports are part of public Spaces plugin contract, any change in signature of exported @@ -22,6 +22,9 @@ export { SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; export { ISpacesClient } from './spaces_client'; export { Space } from '../common/model/space'; -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + schema: ConfigSchema, + deprecations: spacesConfigDeprecationProvider, +}; export const plugin = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/x-pack/plugins/stack_alerts/jest.config.js b/x-pack/plugins/stack_alerts/jest.config.js new file mode 100644 index 0000000000000..a34c1ad828e01 --- /dev/null +++ b/x-pack/plugins/stack_alerts/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/stack_alerts'], +}; diff --git a/x-pack/plugins/task_manager/jest.config.js b/x-pack/plugins/task_manager/jest.config.js new file mode 100644 index 0000000000000..6acb44700921b --- /dev/null +++ b/x-pack/plugins/task_manager/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/task_manager'], +}; diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts index 8a94ae4ed82f5..8b0a8323d9452 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts @@ -50,6 +50,7 @@ describe('mark_available_tasks_as_claimed', () => { update: updateFieldsAndMarkAsFailed( fieldUpdates, claimTasksById || [], + definitions.getAllTypes(), Array.from(definitions).reduce((accumulator, [type, { maxAttempts }]) => { return { ...accumulator, [type]: maxAttempts || defaultMaxAttempts }; }, {}) @@ -114,12 +115,16 @@ if (doc['task.runAt'].size()!=0) { seq_no_primary_term: true, script: { source: ` - if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { - ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) - .join(' ')} + if (params.registeredTaskTypes.contains(ctx._source.task.taskType)) { + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } } else { - ctx._source.task.status = "failed"; + ctx._source.task.status = "unrecognized"; } `, lang: 'painless', @@ -129,6 +134,7 @@ if (doc['task.runAt'].size()!=0) { retryAt: claimOwnershipUntil, }, claimTasksById: [], + registeredTaskTypes: ['sampleTask', 'otherTask'], taskMaxAttempts: { sampleTask: 5, otherTask: 1, diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 072ec4648201a..ecd8107eef915 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -105,21 +105,27 @@ export const updateFieldsAndMarkAsFailed = ( [field: string]: string | number | Date; }, claimTasksById: string[], + registeredTaskTypes: string[], taskMaxAttempts: { [field: string]: number } ): ScriptClause => ({ source: ` - if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { - ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) - .join(' ')} + if (params.registeredTaskTypes.contains(ctx._source.task.taskType)) { + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } } else { - ctx._source.task.status = "failed"; + ctx._source.task.status = "unrecognized"; } `, lang: 'painless', params: { fieldUpdates, claimTasksById, + registeredTaskTypes, taskMaxAttempts, }, }); diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index 8b7870550040f..e832a95ac3caa 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -166,6 +166,7 @@ export enum TaskStatus { Claiming = 'claiming', Running = 'running', Failed = 'failed', + Unrecognized = 'unrecognized', } export enum TaskLifecycleResult { diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 46e55df4ee1e6..81d72c68b3a9e 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -578,12 +578,16 @@ if (doc['task.runAt'].size()!=0) { expect(script).toMatchObject({ source: ` - if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { - ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) - .join(' ')} + if (params.registeredTaskTypes.contains(ctx._source.task.taskType)) { + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } } else { - ctx._source.task.status = "failed"; + ctx._source.task.status = "unrecognized"; } `, lang: 'painless', @@ -593,6 +597,7 @@ if (doc['task.runAt'].size()!=0) { 'task:33c6977a-ed6d-43bd-98d9-3f827f7b7cd8', 'task:a208b22c-14ec-4fb4-995f-d2ff7a3b03b8', ], + registeredTaskTypes: ['foo', 'bar'], taskMaxAttempts: { bar: customMaxAttempts, foo: maxAttempts, @@ -644,18 +649,23 @@ if (doc['task.runAt'].size()!=0) { }); expect(script).toMatchObject({ source: ` - if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { - ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) - .join(' ')} + if (params.registeredTaskTypes.contains(ctx._source.task.taskType)) { + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } } else { - ctx._source.task.status = "failed"; + ctx._source.task.status = "unrecognized"; } `, lang: 'painless', params: { fieldUpdates, claimTasksById: [], + registeredTaskTypes: ['report', 'dernstraight', 'yawn'], taskMaxAttempts: { dernstraight: 2, report: 2, @@ -1218,7 +1228,7 @@ if (doc['task.runAt'].size()!=0) { describe('getLifecycle', () => { test('returns the task status if the task exists ', async () => { - expect.assertions(4); + expect.assertions(5); return Promise.all( Object.values(TaskStatus).map(async (status) => { const task = { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 04ee3529bcc0b..0d5d2431e227f 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -260,6 +260,7 @@ export class TaskStore { claimTasksById: OwnershipClaimingOpts['claimTasksById'], size: OwnershipClaimingOpts['size'] ): Promise { + const registeredTaskTypes = this.definitions.getAllTypes(); const taskMaxAttempts = [...this.definitions].reduce((accumulator, [type, { maxAttempts }]) => { return { ...accumulator, [type]: maxAttempts || this.maxAttempts }; }, {}); @@ -297,6 +298,7 @@ export class TaskStore { retryAt: claimOwnershipUntil, }, claimTasksById || [], + registeredTaskTypes, taskMaxAttempts ), sort, diff --git a/x-pack/plugins/task_manager/server/task_type_dictionary.ts b/x-pack/plugins/task_manager/server/task_type_dictionary.ts index cb7cda6dfa845..451b5dd7cad52 100644 --- a/x-pack/plugins/task_manager/server/task_type_dictionary.ts +++ b/x-pack/plugins/task_manager/server/task_type_dictionary.ts @@ -32,6 +32,10 @@ export class TaskTypeDictionary { return this.definitions.entries(); } + public getAllTypes() { + return [...this.definitions.keys()]; + } + public has(type: string) { return this.definitions.has(type); } @@ -44,9 +48,7 @@ export class TaskTypeDictionary { public ensureHas(type: string) { if (!this.has(type)) { throw new Error( - `Unsupported task type "${type}". Supported types are ${[...this.definitions.keys()].join( - ', ' - )}` + `Unsupported task type "${type}". Supported types are ${this.getAllTypes().join(', ')}` ); } } diff --git a/x-pack/plugins/telemetry_collection_xpack/jest.config.js b/x-pack/plugins/telemetry_collection_xpack/jest.config.js new file mode 100644 index 0000000000000..341be31243db8 --- /dev/null +++ b/x-pack/plugins/telemetry_collection_xpack/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/telemetry_collection_xpack'], +}; diff --git a/x-pack/plugins/transform/jest.config.js b/x-pack/plugins/transform/jest.config.js new file mode 100644 index 0000000000000..b752d071e4909 --- /dev/null +++ b/x-pack/plugins/transform/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/transform'], +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4991c4d16099c..7b84c62264c83 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12012,10 +12012,8 @@ "xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle": "クラス名", "xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText": "ベースライン(学習データセットのすべてのデータポイントの予測の平均)", "xpack.ml.dataframe.analytics.explorationResults.decisionPathJSONTab": "JSON", - "xpack.ml.dataframe.analytics.explorationResults.decisionPathLineTitle": "予測", "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotHelpText": "SHAP決定プロットは{linkedFeatureImportanceValues}を使用して、モデルがどのように「{predictionFieldName}」の予測値に到達するのかを示します。", "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotTab": "決定プロット", - "xpack.ml.dataframe.analytics.explorationResults.decisionPathXAxisTitle": "「{predictionFieldName}」の予測", "xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText": "予測があるドキュメントを示す", "xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", "xpack.ml.dataframe.analytics.explorationResults.linkedFeatureImportanceValues": "特徴量の重要度値", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 59ab890cfd9db..55071303a1b36 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12025,10 +12025,8 @@ "xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle": "类名称", "xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText": "基线(训练数据集中所有数据点的预测平均值)", "xpack.ml.dataframe.analytics.explorationResults.decisionPathJSONTab": "JSON", - "xpack.ml.dataframe.analytics.explorationResults.decisionPathLineTitle": "预测", "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotHelpText": "SHAP 决策图使用 {linkedFeatureImportanceValues} 说明模型如何达到“{predictionFieldName}”的预测值。", "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotTab": "决策图", - "xpack.ml.dataframe.analytics.explorationResults.decisionPathXAxisTitle": "“{predictionFieldName}”的预测", "xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText": "正在显示有相关预测存在的文档", "xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", "xpack.ml.dataframe.analytics.explorationResults.linkedFeatureImportanceValues": "特征重要性值", diff --git a/x-pack/plugins/triggers_actions_ui/jest.config.js b/x-pack/plugins/triggers_actions_ui/jest.config.js new file mode 100644 index 0000000000000..63f3b24da4f56 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/triggers_actions_ui'], +}; diff --git a/x-pack/plugins/ui_actions_enhanced/README.md b/x-pack/plugins/ui_actions_enhanced/README.md index a4a37b559ff8d..cd2a34a2f7536 100644 --- a/x-pack/plugins/ui_actions_enhanced/README.md +++ b/x-pack/plugins/ui_actions_enhanced/README.md @@ -3,3 +3,66 @@ Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. - [__Dashboard drilldown user docs__](https://www.elastic.co/guide/en/kibana/master/drilldowns.html) + +## Dynamic Actions Telemetry + +Dynamic actions (drilldowns) report telemetry. Below is the summary of dynamic action metrics that are reported using telemetry. + +### Dynamic action count + +Total count of dynamic actions (drilldowns) on a saved object. + +``` +dynamicActions.count +``` + +### Count by factory ID + +Count of active dynamic actions (drilldowns) on a saved object by factory ID (drilldown type). + +``` +dynamicActions.actions..count +``` + +For example: + +``` +dynamicActions.actions.DASHBOARD_TO_DASHBOARD_DRILLDOWN.count +dynamicActions.actions.URL_DRILLDOWN.count +``` + +### Count by trigger + +Count of active dynamic actions (drilldowns) on a saved object by a trigger to which they are attached. + +``` +dynamicActions.triggers..count +``` + +For example: + +``` +dynamicActions.triggers.VALUE_CLICK_TRIGGER.count +dynamicActions.triggers.RANGE_SELECT_TRIGGER.count +``` + +### Count by factory and trigger + +Count of active dynamic actions (drilldowns) on a saved object by a factory ID and trigger ID. + +``` +dynamicActions.action_triggers._.count +``` + +For example: + +``` +dynamicActions.action_triggers.DASHBOARD_TO_DASHBOARD_DRILLDOWN_VALUE_CLICK_TRIGGER.count +dynamicActions.action_triggers.DASHBOARD_TO_DASHBOARD_DRILLDOWN_RANGE_SELECT_TRIGGER.count +dynamicActions.action_triggers.URL_DRILLDOWN_VALUE_CLICK_TRIGGER.count +``` + +### Factory metrics + +Each dynamic action factory (drilldown type) can report its own stats, which is +done using the `.telemetry()` method on dynamic action factories. diff --git a/x-pack/plugins/ui_actions_enhanced/jest.config.js b/x-pack/plugins/ui_actions_enhanced/jest.config.js new file mode 100644 index 0000000000000..a68fc82413583 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/ui_actions_enhanced'], +}; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index 4cea7ddf4854a..16e7e7967838d 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -8,19 +8,20 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddabl import { SavedObjectReference } from '../../../../src/core/types'; import { ActionFactory, DynamicActionsState, SerializedEvent } from './types'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import { dynamicActionsCollector } from './telemetry/dynamic_actions_collector'; +import { dynamicActionFactoriesCollector } from './telemetry/dynamic_action_factories_collector'; export const dynamicActionEnhancement = ( getActionFactory: (id: string) => undefined | ActionFactory ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (state: SerializableState, telemetry: Record) => { - let telemetryData = telemetry; - (state as DynamicActionsState).events.forEach((event: SerializedEvent) => { - const factory = getActionFactory(event.action.factoryId); - if (factory) telemetryData = factory.telemetry(event, telemetryData); - }); - return telemetryData; + telemetry: (serializableState: SerializableState, stats: Record) => { + const state = serializableState as DynamicActionsState; + stats = dynamicActionsCollector(state, stats); + stats = dynamicActionFactoriesCollector(getActionFactory, state, stats); + + return stats; }, extract: (state: SerializableState) => { const references: SavedObjectReference[] = []; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts new file mode 100644 index 0000000000000..9d38fd9d302a4 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { dynamicActionFactoriesCollector } from './dynamic_action_factories_collector'; +import { DynamicActionsState } from '../../common'; +import { ActionFactory } from '../types'; + +type GetActionFactory = (id: string) => undefined | ActionFactory; + +const factories: Record = { + FACTORY_ID_1: ({ + id: 'FACTORY_ID_1', + telemetry: jest.fn((state: DynamicActionsState, stats: Record) => { + stats.myStat_1 = 1; + stats.myStat_2 = 123; + return stats; + }), + } as unknown) as ActionFactory, + FACTORY_ID_2: ({ + id: 'FACTORY_ID_2', + telemetry: jest.fn((state: DynamicActionsState, stats: Record) => stats), + } as unknown) as ActionFactory, + FACTORY_ID_3: ({ + id: 'FACTORY_ID_3', + telemetry: jest.fn((state: DynamicActionsState, stats: Record) => { + stats.myStat_1 = 2; + stats.stringStat = 'abc'; + return stats; + }), + } as unknown) as ActionFactory, +}; + +const getActionFactory: GetActionFactory = (id: string) => factories[id]; + +const state: DynamicActionsState = { + events: [ + { + eventId: 'eventId-1', + triggers: ['TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Click me!', + config: {}, + }, + }, + { + eventId: 'eventId-2', + triggers: ['TRIGGER_2', 'TRIGGER_3'], + action: { + factoryId: 'FACTORY_ID_2', + name: 'Click me, too!', + config: { + doCleanup: true, + }, + }, + }, + { + eventId: 'eventId-3', + triggers: ['TRIGGER_4', 'TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_3', + name: 'Go to documentation', + config: { + url: 'http://google.com', + iamFeelingLucky: true, + }, + }, + }, + ], +}; + +beforeEach(() => { + Object.values(factories).forEach((factory) => { + ((factory.telemetry as unknown) as jest.SpyInstance).mockClear(); + }); +}); + +describe('dynamicActionFactoriesCollector', () => { + test('returns empty stats when there are not dynamic actions', () => { + const stats = dynamicActionFactoriesCollector( + getActionFactory, + { + events: [], + }, + {} + ); + + expect(stats).toEqual({}); + }); + + test('calls .telemetry() method of a supplied factory', () => { + const currentState = { + events: [state.events[0]], + }; + dynamicActionFactoriesCollector(getActionFactory, currentState, {}); + + const spy1 = (factories.FACTORY_ID_1.telemetry as unknown) as jest.SpyInstance; + const spy2 = (factories.FACTORY_ID_2.telemetry as unknown) as jest.SpyInstance; + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(0); + + expect(spy1.mock.calls[0][0]).toEqual(currentState.events[0]); + expect(typeof spy1.mock.calls[0][1]).toBe('object'); + expect(!!spy1.mock.calls[0][1]).toBe(true); + }); + + test('returns stats received from factory', () => { + const currentState = { + events: [state.events[0]], + }; + const stats = dynamicActionFactoriesCollector(getActionFactory, currentState, {}); + + expect(stats).toEqual({ + myStat_1: 1, + myStat_2: 123, + }); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts new file mode 100644 index 0000000000000..2ece6102c27a4 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DynamicActionsState } from '../../common'; +import { ActionFactory } from '../types'; + +export const dynamicActionFactoriesCollector = ( + getActionFactory: (id: string) => undefined | ActionFactory, + state: DynamicActionsState, + stats: Record +): Record => { + for (const event of state.events) { + const factory = getActionFactory(event.action.factoryId); + + if (factory) { + stats = factory.telemetry(event, stats); + } + } + + return stats; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts new file mode 100644 index 0000000000000..99217cd98fa01 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts @@ -0,0 +1,271 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { dynamicActionsCollector } from './dynamic_actions_collector'; +import { DynamicActionsState } from '../../common'; + +const state: DynamicActionsState = { + events: [ + { + eventId: 'eventId-1', + triggers: ['TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Click me!', + config: {}, + }, + }, + { + eventId: 'eventId-2', + triggers: ['TRIGGER_2', 'TRIGGER_3'], + action: { + factoryId: 'FACTORY_ID_2', + name: 'Click me, too!', + config: { + doCleanup: true, + }, + }, + }, + { + eventId: 'eventId-3', + triggers: ['TRIGGER_4', 'TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Go to documentation', + config: { + url: 'http://google.com', + iamFeelingLucky: true, + }, + }, + }, + ], +}; + +describe('dynamicActionsCollector', () => { + describe('dynamic action count', () => { + test('equal to zero when there are no dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 0, + }); + }); + + test('does not update existing count if there are no dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [], + }, + { + 'dynamicActions.count': 25, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 25, + }); + }); + + test('equal to one when there is one dynamic action', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 1, + }); + }); + + test('adds one to the current dynamic action count', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.count': 2, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 3, + }); + }); + + test('equal to three when there are three dynamic action', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[1], state.events[2]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 3, + }); + }); + }); + + describe('registered action counts', () => { + test('for single action sets count to one', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 1, + }); + }); + + test('adds count to existing action counts', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.actions.FACTORY_ID_1.count': 5, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 6, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + }); + }); + + test('aggregates count factory count', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 2, + }); + }); + + test('returns counts for every factory type', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 2, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + }); + }); + }); + + describe('action trigger counts', () => { + test('for single action sets count to one', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 1, + }); + }); + + test('adds count to existing stats', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.triggers.TRIGGER_1.count': 123, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 124, + }); + }); + + test('aggregates trigger counts from all dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 2, + 'dynamicActions.triggers.TRIGGER_2.count': 1, + 'dynamicActions.triggers.TRIGGER_3.count': 1, + 'dynamicActions.triggers.TRIGGER_4.count': 1, + }); + }); + }); + + describe('action x trigger counts', () => { + test('returns single action (factoryId x trigger) stat', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 1, + }); + }); + + test('adds count to existing stats', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 3, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 4, + }); + }); + + test('aggregates actions x triggers counts for all events', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 2, + 'dynamicActions.action_triggers.FACTORY_ID_2_TRIGGER_2.count': 1, + 'dynamicActions.action_triggers.FACTORY_ID_2_TRIGGER_3.count': 1, + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_4.count': 1, + }); + }); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts new file mode 100644 index 0000000000000..ae595776fda58 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DynamicActionsState } from '../../common'; +import { getMetricKey } from './get_metric_key'; + +export const dynamicActionsCollector = ( + state: DynamicActionsState, + stats: Record +): Record => { + const countMetricKey = getMetricKey('count'); + + stats[countMetricKey] = state.events.length + (stats[countMetricKey] || 0); + + for (const event of state.events) { + const factoryId = event.action.factoryId; + const actionCountMetric = getMetricKey(`actions.${factoryId}.count`); + + stats[actionCountMetric] = 1 + (stats[actionCountMetric] || 0); + + for (const trigger of event.triggers) { + const triggerCountMetric = getMetricKey(`triggers.${trigger}.count`); + const actionXTriggerCountMetric = getMetricKey( + `action_triggers.${factoryId}_${trigger}.count` + ); + + stats[triggerCountMetric] = 1 + (stats[triggerCountMetric] || 0); + stats[actionXTriggerCountMetric] = 1 + (stats[actionXTriggerCountMetric] || 0); + } + } + + return stats; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts new file mode 100644 index 0000000000000..6d3ae370c5200 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const prefix = 'dynamicActions.'; + +/** Returns prefixed telemetry metric key for all dynamic action metrics. */ +export const getMetricKey = (path: string) => `${prefix}${path}`; diff --git a/x-pack/plugins/upgrade_assistant/jest.config.js b/x-pack/plugins/upgrade_assistant/jest.config.js new file mode 100644 index 0000000000000..ad0ea1741822c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/upgrade_assistant'], +}; diff --git a/x-pack/plugins/uptime/jest.config.js b/x-pack/plugins/uptime/jest.config.js new file mode 100644 index 0000000000000..85da90927af17 --- /dev/null +++ b/x-pack/plugins/uptime/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/uptime'], +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx index 9a3e045017f9a..0c47e4c73e976 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx @@ -57,29 +57,31 @@ interface ExecutedJourneyProps { journey: JourneyState; } -export const ExecutedJourney: FC = ({ journey }) => ( -
- -

- -

-

- {statusMessage( - journey.steps - .filter(isStepEnd) - .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) - )} -

-
- - - {journey.steps.filter(isStepEnd).map((step, index) => ( - - ))} - - -
-); +export const ExecutedJourney: FC = ({ journey }) => { + return ( +
+ +

+ +

+

+ {statusMessage( + journey.steps + .filter(isStepEnd) + .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) + )} +

+
+ + + {journey.steps.filter(isStepEnd).map((step, index) => ( + + ))} + + +
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md new file mode 100644 index 0000000000000..cf8d3b5345eaa --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md @@ -0,0 +1,123 @@ +# Waterfall chart + +## Introduction + +The waterfall chart component aims to be agnostic in it's approach, so that a variety of consumers / solutions can use it. Some of Elastic Chart's features are used in a non-standard way to facilitate this flexibility, this README aims to cover some of the things that might be less obvious, and also provides a high level overview of implementation. + +## Requirements for usage + +The waterfall chart component asssumes that the consumer is making use of `KibanaReactContext`, and as such things like `useKibana` can be called. + +Consumers are also expected to be using the `` so that the waterfall chart can apply styled-component styles based on the EUI theme. + +These are the two hard requirements, but almost all plugins will be using these. + +## Rendering + +At it's core the watefall chart is a stacked bar chart that has been rotated through 90 degrees. As such it's important to understand that `x` is now represented as `y` and vice versa. + +## Flexibility + +This section aims to cover some things that are non-standard. + +### Tooltip + +By default the formatting of tooltip values is very basic, but for a waterfall chart there needs to be a great deal of flexibility to represent whatever breakdown you're trying to show. + +As such a custom tooltip component is used. This custom component would usually only have access to some basic props that pertain to the values of the hovered bar. The waterfall chart component extends this by making us of a waterfall chart context. + +The custom tooltip component can use the context to access the full set of chart data, find the relevant items (those with the same `x` value) and call a custom `renderTooltipItem` for each item, `renderTooltipItem` will be passed `item.config.tooltipProps`. Every consumer can choose what they use for their `tooltipProps`. + +Some consumers might need colours, some might need iconography and so on. The waterfall chart doesn't make assumptions, and will render out the React content returned by `renderTooltipItem`. + +IMPORTANT: `renderTooltipItem` is provided via context and not as a direct prop due to the fact the custom tooltip component would usually only have access to the props provided directly to it from Elastic Charts. + +### Colours + +The easiest way to facilitate specific colours for each stack (let's say your colours are mapped to a constraint like mime type) is to assign the colour directly on your datum `config` property, and then access this directly in the `barStyleAccessor` function, e.g. + +``` +barStyleAccessor={(datum) => { + return datum.datum.config.colour; +}) +``` + +### Config + +The notion of `config` has been mentioned already. But this is a place that consumers can store their solution specific properties. `renderTooltipItem` will make use of `config.tooltipProps`, and `barStyleAccessor` can make use of anything on `config`. + +### Sticky top axis + +By default there is no "sticky" axis functionality in Elastic Charts, therefore a second chart is rendered, this contains a replica of the top axis, and renders one empty data point (as a chart can't only have an axis). This second chart is then positioned in such a way that it covers the top of the real axis, and remains fixed. + +## Data + +The waterfall chart expects data in a relatively simple format, there are the usual plot properties (`x`, `y0`, and `y`) and then `config`. E.g. + +``` +const series = [ + {x: 0, y: 0, y: 100, config: { tooltipProps: { type: 'dns' }}}, + {x: 0, y0: 300, y: 500, config: { tooltipProps: { type: 'ssl' }}}, + {x: 1, y0: 250, y: 300, config: { tooltipProps: { propA: 'somethingBreakdownRelated' }}}, + {x: 1, y0: 500, y: 600, config: { tooltipProps: { propA: 'anotherBreakdown' }}}, +] +``` + +Gaps in bars are fine, and to be expected for certain solutions. + +## Sidebar items + +The waterfall chart component again doesn't make assumptions about consumer's sidebar items' content, but the waterfall chart does handle the rendering so the sidebar can be aligned and rendered properly alongside the chart itself. + +`sidebarItems` should be provided to the context, and a `renderSidebarItem` prop should be provided to the chart. + +A sidebar is optional. + +There is a great deal of flexibility here so that solutions can make use of this in the way they need. For example, if you'd like to add a toggle functionality, so that clicking an item shows / hides it's children, this would involve rendering your toggle in `renderSidebarItem` and then when clicked you can handle adjusting your data as necessary. + +IMPORTANT: It is important to understand that the chart itself makes use of a fixed height. The sidebar will create a space that has a matching height. Each item is assigned equal space vertically via Flexbox, so that the items align with the relevant bar to the right (these are two totally different rendering contexts, with the chart itself sitting within a `canvas` element). So it's important that whatever content you choose to render here doesn't exceed the available height available to each item. The chart's height is calculated as `numberOfBars * 32`, so content should be kept within that `32px` threshold. + +## Legend items + +Much the same as with the sidebar items, no assumptions are made here, solutions will have different aims. + +`legendItems` should be provided to the context, and a `renderLegendItem` prop should be provided to the chart. + +A legend is optional. + +## Overall usage + +Pulling all of this together, things look like this (for a specific solution): + +``` +const renderSidebarItem: RenderItem = (item, index) => { + return ; +}; + +const renderLegendItem: RenderItem = (item) => { + return {item.name}; +}; + + { + return {tooltipProps.value}; + }} +> + `${Number(d).toFixed(0)} ms`} + domain={{ min: domain.min, max: domain.max }} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={renderSidebarItem} + renderLegendItem={renderLegendItem} + /> + +``` + +A solution could easily forego a sidebar and legend for a more minimalistic view, e.g. maybe a mini waterfall within a table column. + + diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts new file mode 100644 index 0000000000000..ac650c5ef0ddd --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Pixel value +export const BAR_HEIGHT = 32; +// Flex grow value +export const MAIN_GROW_SIZE = 8; +// Flex grow value +export const SIDEBAR_GROW_SIZE = 2; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx new file mode 100644 index 0000000000000..85a205a7256f3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { IWaterfallContext } from '../context/waterfall_chart'; +import { WaterfallChartLegendContainer } from './styles'; +import { WaterfallChartProps } from './waterfall_chart'; + +interface LegendProps { + items: Required['legendItems']; + render: Required['renderLegendItem']; +} + +export const Legend: React.FC = ({ items, render }) => { + return ( + + + {items.map((item, index) => { + return {render(item, index)}; + })} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx new file mode 100644 index 0000000000000..4f54a347d22d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getChunks, MiddleTruncatedText } from './middle_truncated_text'; +import { shallowWithIntl } from '@kbn/test/jest'; +import React from 'react'; + +const longString = + 'this-is-a-really-really-really-really-really-really-really-really-long-string.madeup.extension'; + +describe('getChunks', () => { + it('Calculates chunks correctly', () => { + const result = getChunks(longString); + expect(result).toEqual({ + first: 'this-is-a-really-really-really-really-really-really-really-really-long-string.made', + last: 'up.extension', + }); + }); +}); + +describe('Component', () => { + it('Renders correctly', () => { + expect(shallowWithIntl()).toMatchInlineSnapshot(` + + + + this-is-a-really-really-really-really-really-really-really-really-long-string.made + + + up.extension + + + + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx new file mode 100644 index 0000000000000..519927d7db28b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +const OuterContainer = styled.div` + width: 100%; + height: 100%; + position: relative; +`; + +const InnerContainer = styled.div` + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; + display: flex; + min-width: 0; +`; // NOTE: min-width: 0 ensures flexbox and no-wrap children can co-exist + +const FirstChunk = styled.span` + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; + +const LastChunk = styled.span` + flex-shrink: 0; +`; + +export const getChunks = (text: string) => { + const END_CHARS = 12; + const chars = text.split(''); + const splitPoint = chars.length - END_CHARS > 0 ? chars.length - END_CHARS : null; + const endChars = splitPoint ? chars.splice(splitPoint) : []; + return { first: chars.join(''), last: endChars.join('') }; +}; + +// Helper component for adding middle text truncation, e.g. +// really-really-really-long....ompressed.js +// Can be used to accomodate content in sidebar item rendering. +export const MiddleTruncatedText = ({ text }: { text: string }) => { + const chunks = useMemo(() => { + return getChunks(text); + }, [text]); + + return ( + + + {chunks.first} + {chunks.last} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx new file mode 100644 index 0000000000000..9ff544fc1946b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import { SIDEBAR_GROW_SIZE } from './constants'; +import { IWaterfallContext } from '../context/waterfall_chart'; +import { + WaterfallChartSidebarContainer, + WaterfallChartSidebarContainerInnerPanel, + WaterfallChartSidebarContainerFlexGroup, + WaterfallChartSidebarFlexItem, +} from './styles'; +import { WaterfallChartProps } from './waterfall_chart'; + +interface SidebarProps { + items: Required['sidebarItems']; + height: number; + render: Required['renderSidebarItem']; +} + +export const Sidebar: React.FC = ({ items, height, render }) => { + return ( + + + + + {items.map((item, index) => { + return ( + + {render(item, index)} + + ); + })} + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts new file mode 100644 index 0000000000000..25f5e5f8f5cc9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../observability/public'; + +// NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here. +const FIXED_AXIS_HEIGHT = 33; + +interface WaterfallChartOuterContainerProps { + height?: number; +} + +export const WaterfallChartOuterContainer = euiStyled.div` + height: ${(props) => (props.height ? `${props.height}px` : 'auto')}; + overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; + overflow-x: hidden; +`; + +export const WaterfallChartFixedTopContainer = euiStyled.div` + position: sticky; + top: 0; + z-index: ${(props) => props.theme.eui.euiZLevel4}; +`; + +export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` + height: 100%; + border-radius: 0 !important; + border: none; +`; // NOTE: border-radius !important is here as the "border" prop isn't working + +export const WaterfallChartFixedAxisContainer = euiStyled.div` + height: ${FIXED_AXIS_HEIGHT}px; +`; + +interface WaterfallChartSidebarContainer { + height: number; +} + +export const WaterfallChartSidebarContainer = euiStyled.div` + height: ${(props) => `${props.height - FIXED_AXIS_HEIGHT}px`}; + overflow-y: hidden; +`; + +export const WaterfallChartSidebarContainerInnerPanel = euiStyled(EuiPanel)` + border: 0; + height: 100%; +`; + +export const WaterfallChartSidebarContainerFlexGroup = euiStyled(EuiFlexGroup)` + height: 100%; +`; + +// Ensures flex items honour no-wrap of children, rather than trying to extend to the full width of children. +export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` + min-width: 0; + padding-left: ${(props) => props.theme.eui.paddingSizes.m}; + padding-right: ${(props) => props.theme.eui.paddingSizes.m}; +`; + +interface WaterfallChartChartContainer { + height: number; +} + +export const WaterfallChartChartContainer = euiStyled.div` + width: 100%; + height: ${(props) => `${props.height}px`}; + margin-top: -${FIXED_AXIS_HEIGHT}px; +`; + +export const WaterfallChartLegendContainer = euiStyled.div` + position: sticky; + bottom: 0; + z-index: ${(props) => props.theme.eui.euiZLevel4}; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + padding: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${(props) => props.theme.eui.euiFontSizeXS}; + box-shadow: 0px -1px 4px 0px ${(props) => props.theme.eui.euiColorLightShade}; +`; // NOTE: EuiShadowColor is a little too dark to work with the background-color + +export const WaterfallChartTooltip = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiColorDarkestShade}; + border-radius: ${(props) => props.theme.eui.euiBorderRadius}; + color: ${(props) => props.theme.eui.euiColorLightestShade}; + padding: ${(props) => props.theme.eui.paddingSizes.s}; +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx new file mode 100644 index 0000000000000..de4be0ea34b2c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + Axis, + BarSeries, + Chart, + Position, + ScaleType, + Settings, + TickFormatter, + DomainRange, + BarStyleAccessor, + TooltipInfo, + TooltipType, +} from '@elastic/charts'; +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +// NOTE: The WaterfallChart has a hard requirement that consumers / solutions are making use of KibanaReactContext, and useKibana etc +// can therefore be accessed. +import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { useWaterfallContext } from '../context/waterfall_chart'; +import { + WaterfallChartOuterContainer, + WaterfallChartFixedTopContainer, + WaterfallChartFixedTopContainerSidebarCover, + WaterfallChartFixedAxisContainer, + WaterfallChartChartContainer, + WaterfallChartTooltip, +} from './styles'; +import { WaterfallData } from '../types'; +import { BAR_HEIGHT, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { Sidebar } from './sidebar'; +import { Legend } from './legend'; + +const Tooltip = ({ header }: TooltipInfo) => { + const { data, renderTooltipItem } = useWaterfallContext(); + const relevantItems = data.filter((item) => { + return item.x === header?.value; + }); + return ( + + + {relevantItems.map((item, index) => { + return ( + {renderTooltipItem(item.config.tooltipProps)} + ); + })} + + + ); +}; + +export type RenderItem = (item: I, index: number) => JSX.Element; + +export interface WaterfallChartProps { + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; + renderSidebarItem?: RenderItem; + renderLegendItem?: RenderItem; + maxHeight?: number; +} + +const getUniqueBars = (data: WaterfallData) => { + return data.reduce>((acc, item) => { + if (!acc.has(item.x)) { + acc.add(item.x); + return acc; + } else { + return acc; + } + }, new Set()); +}; + +const getChartHeight = (data: WaterfallData): number => getUniqueBars(data).size * BAR_HEIGHT; + +export const WaterfallChart = ({ + tickFormat, + domain, + barStyleAccessor, + renderSidebarItem, + renderLegendItem, + maxHeight = 600, +}: WaterfallChartProps) => { + const { data, sidebarItems, legendItems } = useWaterfallContext(); + + const generatedHeight = useMemo(() => { + return getChartHeight(data); + }, [data]); + + const [darkMode] = useUiSetting$('theme:darkMode'); + + const theme = useMemo(() => { + return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + }, [darkMode]); + + const shouldRenderSidebar = + sidebarItems && sidebarItems.length > 0 && renderSidebarItem ? true : false; + const shouldRenderLegend = + legendItems && legendItems.length > 0 && renderLegendItem ? true : false; + + return ( + + <> + + + {shouldRenderSidebar && ( + + + + )} + + + + + + + + ''} /> + + + + + + + + + {shouldRenderSidebar && ( + + )} + + + + + + + + ''} /> + + + + + + + {shouldRenderLegend && } + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts new file mode 100644 index 0000000000000..698e6b4be0c4c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts @@ -0,0 +1,687 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { colourPalette } from './data_formatting'; + +// const TEST_DATA = [ +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// }, +// synthetics: { +// index: 7, +// payload: { +// request: { +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// method: 'GET', +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// }, +// status: 200, +// method: 'GET', +// end: 13902.944973, +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// type: 'Script', +// is_navigation_request: false, +// start: 13902.752946, +// response: { +// encoded_data_length: 179, +// protocol: 'h2', +// headers: { +// content_encoding: 'br', +// server: 'cloudflare', +// age: '94838', +// cf_cache_status: 'HIT', +// x_content_type_options: 'nosniff', +// last_modified: 'Wed, 04 Feb 2015 03:25:28 GMT', +// cf_ray: '5e9dbc2bdda2e5a7-MAN', +// content_type: 'application/javascript; charset=utf-8', +// x_cloud_trace_context: 'eec7acc7a6f96b5353ef0d648bf437ac', +// expect_ct: +// 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', +// access_control_allow_origin: '*', +// vary: 'Accept-Encoding', +// cache_control: 'public, max-age=31536000', +// date: 'Thu, 29 Oct 2020 14:55:00 GMT', +// cf_request_id: '061673ef6b0000e5a7cd07a000000001', +// etag: 'W/"4f70-NHpXdyWxnckEaeiXalAnXQ+oh4Q"', +// strict_transport_security: 'max-age=31536000; includeSubDomains; preload', +// }, +// remote_i_p_address: '104.16.125.175', +// connection_reused: true, +// timing: { +// dns_start: -1, +// push_end: 0, +// worker_fetch_start: -1, +// worker_respond_with_settled: -1, +// proxy_end: -1, +// worker_start: -1, +// worker_ready: -1, +// send_end: 158.391, +// connect_end: -1, +// connect_start: -1, +// send_start: 157.876, +// proxy_start: -1, +// push_start: 0, +// ssl_end: -1, +// receive_headers_end: 186.885, +// ssl_start: -1, +// request_time: 13902.757525, +// dns_end: -1, +// }, +// connection_id: 17, +// status_text: '', +// remote_port: 443, +// status: 200, +// security_details: { +// valid_to: 1627905600, +// certificate_id: 0, +// key_exchange_group: 'X25519', +// valid_from: 1596326400, +// protocol: 'TLS 1.3', +// issuer: 'Cloudflare Inc ECC CA-3', +// key_exchange: '', +// san_list: ['unpkg.com', '*.unpkg.com', 'sni.cloudflaressl.com'], +// signed_certificate_timestamp_list: [], +// certificate_transparency_compliance: 'unknown', +// cipher: 'AES_128_GCM', +// subject_name: 'sni.cloudflaressl.com', +// }, +// mime_type: 'application/javascript', +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// from_prefetch_cache: false, +// from_disk_cache: false, +// security_state: 'secure', +// response_time: 1.603983300513211e12, +// from_service_worker: false, +// }, +// }, +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// }, +// monitor: { +// status: 'up', +// duration: { +// us: 24, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// }, +// event: { +// dataset: 'uptime', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// }, +// monitor: { +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 13, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// }, +// synthetics: { +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// index: 9, +// payload: { +// start: 13902.76168, +// url: 'file:///opt/examples/todos/app/app.js', +// method: 'GET', +// is_navigation_request: false, +// end: 13902.770133, +// request: { +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/app.js', +// method: 'GET', +// }, +// status: 0, +// type: 'Script', +// response: { +// protocol: 'file', +// connection_reused: false, +// mime_type: 'text/javascript', +// security_state: 'secure', +// from_disk_cache: false, +// url: 'file:///opt/examples/todos/app/app.js', +// status_text: '', +// connection_id: 0, +// from_prefetch_cache: false, +// encoded_data_length: -1, +// headers: {}, +// status: 0, +// from_service_worker: false, +// }, +// }, +// }, +// event: { +// dataset: 'uptime', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.000Z', +// monitor: { +// timespan: { +// lt: '2020-10-29T14:56:01.000Z', +// gte: '2020-10-29T14:55:01.000Z', +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 44365, +// }, +// type: 'browser', +// }, +// synthetics: { +// journey: { +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// index: 5, +// payload: { +// status: 0, +// url: 'file:///opt/examples/todos/app/index.html', +// end: 13902.730261, +// request: { +// method: 'GET', +// headers: {}, +// mixed_content_type: 'none', +// initial_priority: 'VeryHigh', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/index.html', +// }, +// method: 'GET', +// response: { +// status: 0, +// connection_id: 0, +// from_disk_cache: false, +// headers: {}, +// encoded_data_length: -1, +// status_text: '', +// from_service_worker: false, +// connection_reused: false, +// url: 'file:///opt/examples/todos/app/index.html', +// remote_port: 0, +// security_state: 'secure', +// protocol: 'file', +// mime_type: 'text/html', +// remote_i_p_address: '', +// from_prefetch_cache: false, +// }, +// start: 13902.726626, +// type: 'Document', +// is_navigation_request: true, +// }, +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.044Z', +// monitor: { +// type: 'browser', +// timespan: { +// lt: '2020-10-29T14:56:01.044Z', +// gte: '2020-10-29T14:55:01.044Z', +// }, +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 10524, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// synthetics: { +// package_version: '0.0.1', +// index: 6, +// payload: { +// status: 200, +// type: 'Stylesheet', +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// method: 'GET', +// start: 13902.75266, +// is_navigation_request: false, +// end: 13902.943835, +// response: { +// remote_i_p_address: '104.16.125.175', +// response_time: 1.603983300511892e12, +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// mime_type: 'text/css', +// protocol: 'h2', +// security_state: 'secure', +// encoded_data_length: 414, +// remote_port: 443, +// status_text: '', +// timing: { +// proxy_start: -1, +// worker_ready: -1, +// worker_fetch_start: -1, +// receive_headers_end: 189.169, +// worker_respond_with_settled: -1, +// connect_end: 160.311, +// worker_start: -1, +// send_start: 161.275, +// dns_start: 0.528, +// send_end: 161.924, +// ssl_end: 160.267, +// proxy_end: -1, +// ssl_start: 29.726, +// request_time: 13902.753988, +// dns_end: 5.212, +// push_end: 0, +// push_start: 0, +// connect_start: 5.212, +// }, +// connection_reused: false, +// from_service_worker: false, +// security_details: { +// san_list: ['unpkg.com', '*.unpkg.com', 'sni.cloudflaressl.com'], +// valid_from: 1596326400, +// cipher: 'AES_128_GCM', +// protocol: 'TLS 1.3', +// issuer: 'Cloudflare Inc ECC CA-3', +// valid_to: 1627905600, +// certificate_id: 0, +// key_exchange_group: 'X25519', +// certificate_transparency_compliance: 'unknown', +// key_exchange: '', +// subject_name: 'sni.cloudflaressl.com', +// signed_certificate_timestamp_list: [], +// }, +// connection_id: 17, +// status: 200, +// from_disk_cache: false, +// from_prefetch_cache: false, +// headers: { +// date: 'Thu, 29 Oct 2020 14:55:00 GMT', +// x_cloud_trace_context: '76a4f7b8be185f2ac9aa839de3d6f893', +// cache_control: 'public, max-age=31536000', +// expect_ct: +// 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', +// content_type: 'text/css; charset=utf-8', +// age: '627638', +// x_content_type_options: 'nosniff', +// last_modified: 'Sat, 09 Jan 2016 00:57:37 GMT', +// access_control_allow_origin: '*', +// cf_request_id: '061673ef6a0000e5a75a309000000001', +// vary: 'Accept-Encoding', +// strict_transport_security: 'max-age=31536000; includeSubDomains; preload', +// cf_ray: '5e9dbc2bdda1e5a7-MAN', +// content_encoding: 'br', +// etag: 'W/"1921-kYwbQVnRAA2V/L9Gr4SCtUE5LHQ"', +// server: 'cloudflare', +// cf_cache_status: 'HIT', +// }, +// }, +// request: { +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'VeryHigh', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// method: 'GET', +// }, +// }, +// journey: { +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// type: 'journey/network_info', +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// agent: { +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// }, +// synthetics: { +// index: 8, +// payload: { +// method: 'GET', +// type: 'Script', +// response: { +// url: 'file:///opt/examples/todos/app/vue.min.js', +// protocol: 'file', +// connection_id: 0, +// headers: {}, +// mime_type: 'text/javascript', +// from_service_worker: false, +// status_text: '', +// connection_reused: false, +// encoded_data_length: -1, +// from_disk_cache: false, +// security_state: 'secure', +// from_prefetch_cache: false, +// status: 0, +// }, +// is_navigation_request: false, +// request: { +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/vue.min.js', +// method: 'GET', +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// }, +// end: 13902.772783, +// status: 0, +// start: 13902.760644, +// url: 'file:///opt/examples/todos/app/vue.min.js', +// }, +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// }, +// monitor: { +// status: 'up', +// duration: { +// us: 82, +// }, +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// id: 'check that title is present', +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// }, +// ]; + +// const toMillis = (seconds: number) => seconds * 1000; + +// describe('getTimings', () => { +// it('Calculates timings for network events correctly', () => { +// // NOTE: Uses these timings as the file protocol events don't have timing information +// const eventOneTimings = getTimings( +// TEST_DATA[0].synthetics.payload.response.timing!, +// toMillis(TEST_DATA[0].synthetics.payload.start), +// toMillis(TEST_DATA[0].synthetics.payload.end) +// ); +// expect(eventOneTimings).toEqual({ +// blocked: 162.4549999999106, +// connect: -1, +// dns: -1, +// receive: 0.5629999989271255, +// send: 0.5149999999999864, +// ssl: undefined, +// wait: 28.494, +// }); + +// const eventFourTimings = getTimings( +// TEST_DATA[3].synthetics.payload.response.timing!, +// toMillis(TEST_DATA[3].synthetics.payload.start), +// toMillis(TEST_DATA[3].synthetics.payload.end) +// ); +// expect(eventFourTimings).toEqual({ +// blocked: 1.8559999997466803, +// connect: 25.52200000000002, +// dns: 4.683999999999999, +// receive: 0.6780000009983667, +// send: 0.6490000000000009, +// ssl: 130.541, +// wait: 27.245000000000005, +// }); +// }); +// }); + +// describe('getSeriesAndDomain', () => { +// let seriesAndDomain: any; +// let NetworkItems: any; + +// beforeAll(() => { +// NetworkItems = extractItems(TEST_DATA); +// seriesAndDomain = getSeriesAndDomain(NetworkItems); +// }); + +// it('Correctly calculates the domain', () => { +// expect(seriesAndDomain.domain).toEqual({ max: 218.34699999913573, min: 0 }); +// }); + +// it('Correctly calculates the series', () => { +// expect(seriesAndDomain.series).toEqual([ +// { +// config: { colour: '#f3b3a6', tooltipProps: { colour: '#f3b3a6', value: '3.635ms' } }, +// x: 0, +// y: 3.6349999997764826, +// y0: 0, +// }, +// { +// config: { +// colour: '#b9a888', +// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 1.856ms' }, +// }, +// x: 1, +// y: 27.889999999731778, +// y0: 26.0339999999851, +// }, +// { +// config: { colour: '#54b399', tooltipProps: { colour: '#54b399', value: 'DNS: 4.684ms' } }, +// x: 1, +// y: 32.573999999731775, +// y0: 27.889999999731778, +// }, +// { +// config: { +// colour: '#da8b45', +// tooltipProps: { colour: '#da8b45', value: 'Connecting: 25.522ms' }, +// }, +// x: 1, +// y: 58.095999999731795, +// y0: 32.573999999731775, +// }, +// { +// config: { colour: '#edc5a2', tooltipProps: { colour: '#edc5a2', value: 'SSL: 130.541ms' } }, +// x: 1, +// y: 188.63699999973178, +// y0: 58.095999999731795, +// }, +// { +// config: { +// colour: '#d36086', +// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.649ms' }, +// }, +// x: 1, +// y: 189.28599999973179, +// y0: 188.63699999973178, +// }, +// { +// config: { +// colour: '#b0c9e0', +// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 27.245ms' }, +// }, +// x: 1, +// y: 216.5309999997318, +// y0: 189.28599999973179, +// }, +// { +// config: { +// colour: '#ca8eae', +// tooltipProps: { colour: '#ca8eae', value: 'Content downloading: 0.678ms' }, +// }, +// x: 1, +// y: 217.20900000073016, +// y0: 216.5309999997318, +// }, +// { +// config: { +// colour: '#b9a888', +// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 162.455ms' }, +// }, +// x: 2, +// y: 188.77500000020862, +// y0: 26.320000000298023, +// }, +// { +// config: { +// colour: '#d36086', +// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.515ms' }, +// }, +// x: 2, +// y: 189.2900000002086, +// y0: 188.77500000020862, +// }, +// { +// config: { +// colour: '#b0c9e0', +// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 28.494ms' }, +// }, +// x: 2, +// y: 217.7840000002086, +// y0: 189.2900000002086, +// }, +// { +// config: { +// colour: '#9170b8', +// tooltipProps: { colour: '#9170b8', value: 'Content downloading: 0.563ms' }, +// }, +// x: 2, +// y: 218.34699999913573, +// y0: 217.7840000002086, +// }, +// { +// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '12.139ms' } }, +// x: 3, +// y: 46.15699999965727, +// y0: 34.01799999922514, +// }, +// { +// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '8.453ms' } }, +// x: 4, +// y: 43.506999999284744, +// y0: 35.053999999538064, +// }, +// ]); +// }); +// }); + +describe('Palettes', () => { + it('A colour palette comprising timing and mime type colours is correctly generated', () => { + expect(colourPalette).toEqual({ + blocked: '#b9a888', + connect: '#da8b45', + dns: '#54b399', + font: '#aa6556', + html: '#f3b3a6', + media: '#d6bf57', + other: '#e7664c', + receive: '#54b399', + script: '#9170b8', + send: '#d36086', + ssl: '#edc5a2', + stylesheet: '#ca8eae', + wait: '#b0c9e0', + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts new file mode 100644 index 0000000000000..9c66ea638c942 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts @@ -0,0 +1,336 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { euiPaletteColorBlind } from '@elastic/eui'; + +import { + PayloadTimings, + CalculatedTimings, + NetworkItems, + FriendlyTimingLabels, + FriendlyMimetypeLabels, + MimeType, + MimeTypesMap, + Timings, + TIMING_ORDER, + SidebarItems, + LegendItems, +} from './types'; +import { WaterfallData } from '../../../waterfall'; + +const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); + +// The timing calculations here are based off several sources: +// https://github.com/ChromeDevTools/devtools-frontend/blob/2fe91adefb2921b4deb2b4b125370ef9ccdb8d1b/front_end/sdk/HARLog.js#L307 +// and +// https://chromium.googlesource.com/chromium/blink.git/+/master/Source/devtools/front_end/sdk/HAREntry.js#131 +// and +// https://github.com/cyrus-and/chrome-har-capturer/blob/master/lib/har.js#L195 +// Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end + +export const getTimings = ( + timings: PayloadTimings, + requestSentTime: number, + responseReceivedTime: number +): CalculatedTimings => { + if (!timings) return { blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1 }; + + const getLeastNonNegative = (values: number[]) => + values.reduce((best, value) => (value >= 0 && value < best ? value : best), Infinity); + const getOptionalTiming = (_timings: PayloadTimings, key: keyof PayloadTimings) => + _timings[key] >= 0 ? _timings[key] : -1; + + // NOTE: Request sent and request start can differ due to queue times + const requestStartTime = microToMillis(timings.request_time); + + // Queued + const queuedTime = requestSentTime < requestStartTime ? requestStartTime - requestSentTime : -1; + + // Blocked + // "blocked" represents both queued time + blocked/stalled time + proxy time (ie: anything before the request was actually started). + let blocked = queuedTime; + + const blockedStart = getLeastNonNegative([ + timings.dns_start, + timings.connect_start, + timings.send_start, + ]); + + if (blockedStart !== Infinity) { + blocked += blockedStart; + } + + // Proxy + // Proxy is part of blocked, but it can be quirky in that blocked can be -1 even though there are proxy timings. This can happen with + // protocols like Quic. + if (timings.proxy_end !== -1) { + const blockedProxy = timings.proxy_end - timings.proxy_start; + + if (blockedProxy && blockedProxy > blocked) { + blocked = blockedProxy; + } + } + + // DNS + const dnsStart = timings.dns_end >= 0 ? blockedStart : 0; + const dnsEnd = getOptionalTiming(timings, 'dns_end'); + const dns = dnsEnd - dnsStart; + + // SSL + const sslStart = getOptionalTiming(timings, 'ssl_start'); + const sslEnd = getOptionalTiming(timings, 'ssl_end'); + let ssl; + + if (sslStart >= 0 && sslEnd >= 0) { + ssl = timings.ssl_end - timings.ssl_start; + } + + // Connect + let connect = -1; + if (timings.connect_start >= 0) { + connect = timings.send_start - timings.connect_start; + } + + // Send + const send = timings.send_end - timings.send_start; + + // Wait + const wait = timings.receive_headers_end - timings.send_end; + + // Receive + const receive = responseReceivedTime - (requestStartTime + timings.receive_headers_end); + + // SSL connection is a part of the overall connection time + if (connect && ssl) { + connect = connect - ssl; + } + + return { blocked, dns, connect, send, wait, receive, ssl }; +}; + +// TODO: Switch to real API data, and type data as the payload response (if server response isn't preformatted) +export const extractItems = (data: any): NetworkItems => { + const items = data + .map((entry: any) => { + const requestSentTime = microToMillis(entry.synthetics.payload.start); + const responseReceivedTime = microToMillis(entry.synthetics.payload.end); + const requestStartTime = + entry.synthetics.payload.response && entry.synthetics.payload.response.timing + ? microToMillis(entry.synthetics.payload.response.timing.request_time) + : null; + + return { + timestamp: entry['@timestamp'], + method: entry.synthetics.payload.method, + url: entry.synthetics.payload.url, + status: entry.synthetics.payload.status, + mimeType: entry.synthetics.payload?.response?.mime_type, + requestSentTime, + responseReceivedTime, + earliestRequestTime: requestStartTime + ? Math.min(requestSentTime, requestStartTime) + : requestSentTime, + timings: + entry.synthetics.payload.response && entry.synthetics.payload.response.timing + ? getTimings( + entry.synthetics.payload.response.timing, + requestSentTime, + responseReceivedTime + ) + : null, + }; + }) + .sort((a: any, b: any) => { + return a.earliestRequestTime - b.earliestRequestTime; + }); + + return items; +}; + +const formatValueForDisplay = (value: number, points: number = 3) => { + return Number(value).toFixed(points); +}; + +const getColourForMimeType = (mimeType?: string) => { + const key = mimeType && MimeTypesMap[mimeType] ? MimeTypesMap[mimeType] : MimeType.Other; + return colourPalette[key]; +}; + +export const getSeriesAndDomain = (items: NetworkItems) => { + // The earliest point in time a request is sent or started. This will become our notion of "0". + const zeroOffset = items.reduce((acc, item) => { + const { earliestRequestTime } = item; + return earliestRequestTime < acc ? earliestRequestTime : acc; + }, Infinity); + + const series = items.reduce((acc, item, index) => { + const { earliestRequestTime } = item; + + // Entries without timings should be handled differently: + // https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L140 + // If there are no concrete timings just plot one block via start and end + if (!item.timings || item.timings === null) { + const duration = item.responseReceivedTime - item.earliestRequestTime; + const colour = getColourForMimeType(item.mimeType); + return [ + ...acc, + { + x: index, + y0: item.earliestRequestTime - zeroOffset, + y: item.responseReceivedTime - zeroOffset, + config: { + colour, + tooltipProps: { + value: `${formatValueForDisplay(duration)}ms`, + colour, + }, + }, + }, + ]; + } + + let currentOffset = earliestRequestTime - zeroOffset; + + TIMING_ORDER.forEach((timing) => { + const value = item.timings![timing]; + const colour = + timing === Timings.Receive ? getColourForMimeType(item.mimeType) : colourPalette[timing]; + if (value && value >= 0) { + const y = currentOffset + value; + + acc.push({ + x: index, + y0: currentOffset, + y, + config: { + colour, + tooltipProps: { + value: `${FriendlyTimingLabels[timing]}: ${formatValueForDisplay( + y - currentOffset + )}ms`, + colour, + }, + }, + }); + currentOffset = y; + } + }); + return acc; + }, []); + + const yValues = series.map((serie) => serie.y); + const domain = { min: 0, max: Math.max(...yValues) }; + return { series, domain }; +}; + +export const getSidebarItems = (items: NetworkItems): SidebarItems => { + return items.map((item) => { + const { url, status, method } = item; + return { url, status, method }; + }); +}; + +export const getLegendItems = (): LegendItems => { + let timingItems: LegendItems = []; + Object.values(Timings).forEach((timing) => { + // The "receive" timing is mapped to a mime type colour, so we don't need to show this in the legend + if (timing === Timings.Receive) { + return; + } + timingItems = [ + ...timingItems, + { name: FriendlyTimingLabels[timing], colour: TIMING_PALETTE[timing] }, + ]; + }); + + let mimeTypeItems: LegendItems = []; + Object.values(MimeType).forEach((mimeType) => { + mimeTypeItems = [ + ...mimeTypeItems, + { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, + ]; + }); + return [...timingItems, ...mimeTypeItems]; +}; + +// Timing colour palette +type TimingColourPalette = { + [K in Timings]: string; +}; + +const SAFE_PALETTE = euiPaletteColorBlind({ rotations: 2 }); + +const buildTimingPalette = (): TimingColourPalette => { + const palette = Object.values(Timings).reduce>((acc, value) => { + switch (value) { + case Timings.Blocked: + acc[value] = SAFE_PALETTE[6]; + break; + case Timings.Dns: + acc[value] = SAFE_PALETTE[0]; + break; + case Timings.Connect: + acc[value] = SAFE_PALETTE[7]; + break; + case Timings.Ssl: + acc[value] = SAFE_PALETTE[17]; + break; + case Timings.Send: + acc[value] = SAFE_PALETTE[2]; + break; + case Timings.Wait: + acc[value] = SAFE_PALETTE[11]; + break; + case Timings.Receive: + acc[value] = SAFE_PALETTE[0]; + break; + } + return acc; + }, {}); + + return palette as TimingColourPalette; +}; + +const TIMING_PALETTE = buildTimingPalette(); + +// MimeType colour palette +type MimeTypeColourPalette = { + [K in MimeType]: string; +}; + +const buildMimeTypePalette = (): MimeTypeColourPalette => { + const palette = Object.values(MimeType).reduce>((acc, value) => { + switch (value) { + case MimeType.Html: + acc[value] = SAFE_PALETTE[19]; + break; + case MimeType.Script: + acc[value] = SAFE_PALETTE[3]; + break; + case MimeType.Stylesheet: + acc[value] = SAFE_PALETTE[4]; + break; + case MimeType.Media: + acc[value] = SAFE_PALETTE[5]; + break; + case MimeType.Font: + acc[value] = SAFE_PALETTE[8]; + break; + case MimeType.Other: + acc[value] = SAFE_PALETTE[9]; + break; + } + return acc; + }, {}); + + return palette as MimeTypeColourPalette; +}; + +const MIME_TYPE_PALETTE = buildMimeTypePalette(); + +type ColourPalette = TimingColourPalette & MimeTypeColourPalette; + +export const colourPalette: ColourPalette = { ...TIMING_PALETTE, ...MIME_TYPE_PALETTE }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts new file mode 100644 index 0000000000000..1dd58b4f86db3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export enum Timings { + Blocked = 'blocked', + Dns = 'dns', + Connect = 'connect', + Ssl = 'ssl', + Send = 'send', + Wait = 'wait', + Receive = 'receive', +} + +export const FriendlyTimingLabels = { + [Timings.Blocked]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.blocked', + { + defaultMessage: 'Queued / Blocked', + } + ), + [Timings.Dns]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.dns', { + defaultMessage: 'DNS', + }), + [Timings.Connect]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.connect', + { + defaultMessage: 'Connecting', + } + ), + [Timings.Ssl]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.ssl', { + defaultMessage: 'SSL', + }), + [Timings.Send]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.send', { + defaultMessage: 'Sending request', + }), + [Timings.Wait]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.wait', { + defaultMessage: 'Waiting (TTFB)', + }), + [Timings.Receive]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.receive', + { + defaultMessage: 'Content downloading', + } + ), +}; + +export const TIMING_ORDER = [ + Timings.Blocked, + Timings.Dns, + Timings.Connect, + Timings.Ssl, + Timings.Send, + Timings.Wait, + Timings.Receive, +] as const; + +export type CalculatedTimings = { + [K in Timings]?: number; +}; + +export enum MimeType { + Html = 'html', + Script = 'script', + Stylesheet = 'stylesheet', + Media = 'media', + Font = 'font', + Other = 'other', +} + +export const FriendlyMimetypeLabels = { + [MimeType.Html]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.html', { + defaultMessage: 'HTML', + }), + [MimeType.Script]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.script', + { + defaultMessage: 'JS', + } + ), + [MimeType.Stylesheet]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.stylesheet', + { + defaultMessage: 'CSS', + } + ), + [MimeType.Media]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.media', + { + defaultMessage: 'Media', + } + ), + [MimeType.Font]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.font', { + defaultMessage: 'Font', + }), + [MimeType.Other]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.other', + { + defaultMessage: 'Other', + } + ), +}; + +// NOTE: This list tries to cover the standard spec compliant mime types, +// and a few popular non-standard ones, but it isn't exhaustive. +export const MimeTypesMap: Record = { + 'text/html': MimeType.Html, + 'application/javascript': MimeType.Script, + 'text/javascript': MimeType.Script, + 'text/css': MimeType.Stylesheet, + // Images + 'image/apng': MimeType.Media, + 'image/bmp': MimeType.Media, + 'image/gif': MimeType.Media, + 'image/x-icon': MimeType.Media, + 'image/jpeg': MimeType.Media, + 'image/png': MimeType.Media, + 'image/svg+xml': MimeType.Media, + 'image/tiff': MimeType.Media, + 'image/webp': MimeType.Media, + // Common audio / video formats + 'audio/wave': MimeType.Media, + 'audio/wav': MimeType.Media, + 'audio/x-wav': MimeType.Media, + 'audio/x-pn-wav': MimeType.Media, + 'audio/webm': MimeType.Media, + 'video/webm': MimeType.Media, + 'audio/ogg': MimeType.Media, + 'video/ogg': MimeType.Media, + 'application/ogg': MimeType.Media, + // Fonts + 'font/otf': MimeType.Font, + 'font/ttf': MimeType.Font, + 'font/woff': MimeType.Font, + 'font/woff2': MimeType.Font, + 'application/x-font-opentype': MimeType.Font, + 'application/font-woff': MimeType.Font, + 'application/font-woff2': MimeType.Font, + 'application/vnd.ms-fontobject': MimeType.Font, + 'application/font-sfnt': MimeType.Font, +}; + +export interface NetworkItem { + timestamp: string; + method: string; + url: string; + status: number; + mimeType?: string; + // NOTE: This is the time the request was actually issued. timing.request_time might be later if the request was queued. + requestSentTime: number; + responseReceivedTime: number; + // NOTE: Denotes the earlier figure out of request sent time and request start time (part of timings). This can vary based on queue times, and + // also whether an entry actually has timings available. + // Ref: https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L154 + earliestRequestTime: number; + timings: CalculatedTimings | null; +} +export type NetworkItems = NetworkItem[]; + +// NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. +export interface PayloadTimings { + dns_start: number; + push_end: number; + worker_fetch_start: number; + worker_respond_with_settled: number; + proxy_end: number; + worker_start: number; + worker_ready: number; + send_end: number; + connect_end: number; + connect_start: number; + send_start: number; + proxy_start: number; + push_start: number; + ssl_end: number; + receive_headers_end: number; + ssl_start: number; + request_time: number; + dns_end: number; +} + +export interface ExtraSeriesConfig { + colour: string; +} + +export type SidebarItem = Pick; +export type SidebarItems = SidebarItem[]; + +export interface LegendItem { + name: string; + colour: string; +} +export type LegendItems = LegendItem[]; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx new file mode 100644 index 0000000000000..434b44a94b79f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useState } from 'react'; +import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; +import { SidebarItem, LegendItem, NetworkItems } from './types'; +import { + WaterfallProvider, + WaterfallChart, + MiddleTruncatedText, + RenderItem, +} from '../../../waterfall'; + +const renderSidebarItem: RenderItem = (item, index) => { + const { status } = item; + + const isErrorStatusCode = (statusCode: number) => { + const is400 = statusCode >= 400 && statusCode <= 499; + const is500 = statusCode >= 500 && statusCode <= 599; + const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; + return is400 || is500 || isSpecific300; + }; + + return ( + <> + {!isErrorStatusCode(status) ? ( + + ) : ( + + + + + + {status} + + + )} + + ); +}; + +const renderLegendItem: RenderItem = (item) => { + return {item.name}; +}; + +export const WaterfallChartWrapper = () => { + // TODO: Will be sourced via an API + const [networkData] = useState([]); + + const { series, domain } = useMemo(() => { + return getSeriesAndDomain(networkData); + }, [networkData]); + + const sidebarItems = useMemo(() => { + return getSidebarItems(networkData); + }, [networkData]); + + const legendItems = getLegendItems(); + + return ( + { + return {tooltipProps.value}; + }} + > + `${Number(d).toFixed(0)} ms`} + domain={domain} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={renderSidebarItem} + renderLegendItem={renderLegendItem} + /> + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx new file mode 100644 index 0000000000000..ccee9d7994c80 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext, useContext, Context } from 'react'; +import { WaterfallData, WaterfallDataEntry } from '../types'; + +export interface IWaterfallContext { + data: WaterfallData; + sidebarItems?: unknown[]; + legendItems?: unknown[]; + renderTooltipItem: ( + item: WaterfallDataEntry['config']['tooltipProps'], + index?: number + ) => JSX.Element; +} + +export const WaterfallContext = createContext>({}); + +interface ProviderProps { + data: IWaterfallContext['data']; + sidebarItems?: IWaterfallContext['sidebarItems']; + legendItems?: IWaterfallContext['legendItems']; + renderTooltipItem: IWaterfallContext['renderTooltipItem']; +} + +export const WaterfallProvider: React.FC = ({ + children, + data, + sidebarItems, + legendItems, + renderTooltipItem, +}) => { + return ( + + {children} + + ); +}; + +export const useWaterfallContext = () => + useContext((WaterfallContext as unknown) as Context); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx new file mode 100644 index 0000000000000..c3ea39a9ace6e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WaterfallChart, RenderItem, WaterfallChartProps } from './components/waterfall_chart'; +export { WaterfallProvider, useWaterfallContext } from './context/waterfall_chart'; +export { MiddleTruncatedText } from './components/middle_truncated_text'; +export { WaterfallData, WaterfallDataEntry } from './types'; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts new file mode 100644 index 0000000000000..d6901fb482599 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface PlotProperties { + x: number; + y: number; + y0: number; +} + +export interface WaterfallDataSeriesConfigProperties { + tooltipProps: Record; +} + +export type WaterfallDataEntry = PlotProperties & { + config: WaterfallDataSeriesConfigProperties & Record; +}; + +export type WaterfallData = WaterfallDataEntry[]; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js b/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js new file mode 100644 index 0000000000000..17c5c87e3ccc2 --- /dev/null +++ b/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/vis_type_timeseries_enhanced'], +}; diff --git a/x-pack/plugins/watcher/jest.config.js b/x-pack/plugins/watcher/jest.config.js new file mode 100644 index 0000000000000..11ddd8bedc80c --- /dev/null +++ b/x-pack/plugins/watcher/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/watcher'], +}; diff --git a/x-pack/plugins/xpack_legacy/jest.config.js b/x-pack/plugins/xpack_legacy/jest.config.js new file mode 100644 index 0000000000000..16126ca0fa567 --- /dev/null +++ b/x-pack/plugins/xpack_legacy/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/xpack_legacy'], +}; diff --git a/x-pack/scripts/jest.js b/x-pack/scripts/jest.js index f807610fd60de..68cfcf082f818 100644 --- a/x-pack/scripts/jest.js +++ b/x-pack/scripts/jest.js @@ -4,5 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -require('../../src/setup_node_env'); -require('../dev-tools/jest').runJest(); +if (process.argv.indexOf('--config') === -1) { + // append correct jest.config if none is provided + const configPath = require('path').resolve(__dirname, '../jest.config.js'); + process.argv.push('--config', configPath); + console.log('Running Jest with --config', configPath); +} + +if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; +} + +require('jest').run(); diff --git a/x-pack/scripts/jest_integration.js b/x-pack/scripts/jest_integration.js deleted file mode 100644 index 8311a9d283cbd..0000000000000 --- a/x-pack/scripts/jest_integration.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// # Run Jest integration tests -// -// All args will be forwarded directly to Jest, e.g. to watch tests run: -// -// node scripts/jest_integration --watch -// -// or to build code coverage: -// -// node scripts/jest_integration --coverage -// -// See all cli options in https://facebook.github.io/jest/docs/cli.html - -const resolve = require('path').resolve; -process.argv.push('--config', resolve(__dirname, '../test_utils/jest/config.integration.js')); -process.argv.push('--runInBand'); - -require('../../src/setup_node_env'); -require('../../src/dev/jest/cli'); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index cb78e76bdd697..866dd0581b548 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -143,7 +143,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) (pluginDir) => `--plugin-path=${path.resolve(__dirname, 'fixtures', 'plugins', pluginDir)}` ), - `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + `--server.xsrf.allowlist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 5525a82b02ee8..d352d250aee69 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -6,8 +6,7 @@ import moment from 'moment'; import expect from '@kbn/expect'; -import { Client, SearchParams } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { Client } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -20,10 +19,7 @@ const COMMON_HEADERS = { export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es: Client = getService('legacyEs'); - const callCluster: LegacyAPICaller = (((path: 'search', searchParams: SearchParams) => { - return es[path].call(es, searchParams); - }) as unknown) as LegacyAPICaller; + const es: Client = getService('es'); async function assertExpectedSavedObjects(num: number) { // Make sure that new/deleted docs are available to search @@ -31,7 +27,9 @@ export default ({ getService }: FtrProviderContext) => { index: '.kibana', }); - const { count } = await es.count({ + const { + body: { count }, + } = await es.count({ index: '.kibana', q: 'type:lens-ui-telemetry', }); @@ -44,8 +42,9 @@ export default ({ getService }: FtrProviderContext) => { await es.deleteByQuery({ index: '.kibana', q: 'type:lens-ui-telemetry', - waitForCompletion: true, - refresh: 'wait_for', + wait_for_completion: true, + refresh: true, + body: {}, }); }); @@ -53,8 +52,9 @@ export default ({ getService }: FtrProviderContext) => { await es.deleteByQuery({ index: '.kibana', q: 'type:lens-ui-telemetry', - waitForCompletion: true, - refresh: 'wait_for', + wait_for_completion: true, + refresh: true, + body: {}, }); }); @@ -107,7 +107,7 @@ export default ({ getService }: FtrProviderContext) => { refresh: 'wait_for', }); - const result = await getDailyEvents('.kibana', callCluster); + const result = await getDailyEvents('.kibana', () => Promise.resolve(es)); expect(result).to.eql({ byDate: {}, @@ -150,7 +150,7 @@ export default ({ getService }: FtrProviderContext) => { ], }); - const result = await getDailyEvents('.kibana', callCluster); + const result = await getDailyEvents('.kibana', () => Promise.resolve(es)); expect(result).to.eql({ byDate: { @@ -177,7 +177,7 @@ export default ({ getService }: FtrProviderContext) => { await esArchiver.loadIfNeeded('lens/basic'); - const results = await getVisualizationCounts(callCluster, '.kibana'); + const results = await getVisualizationCounts(() => Promise.resolve(es), '.kibana'); expect(results).to.have.keys([ 'saved_overall', diff --git a/x-pack/test/api_integration/apis/management/index.js b/x-pack/test/api_integration/apis/management/index.js index 5afb9cfba9f5f..7b6deb0c3892b 100644 --- a/x-pack/test/api_integration/apis/management/index.js +++ b/x-pack/test/api_integration/apis/management/index.js @@ -13,5 +13,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./index_lifecycle_management')); loadTestFile(require.resolve('./ingest_pipelines')); + loadTestFile(require.resolve('./snapshot_restore')); }); } diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts new file mode 100644 index 0000000000000..f0eea0f960b4b --- /dev/null +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Snapshot and Restore', () => { + loadTestFile(require.resolve('./snapshot_restore')); + }); +} diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts new file mode 100644 index 0000000000000..932df405dde12 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +interface SlmPolicy { + name: string; + snapshotName: string; + schedule: string; + repository: string; + isManagedPolicy: boolean; + config?: { + indices?: string | string[]; + ignoreUnavailable?: boolean; + includeGlobalState?: boolean; + partial?: boolean; + metadata?: Record; + }; + retention?: { + expireAfterValue?: number | ''; + expireAfterUnit?: string; + maxCount?: number | ''; + minCount?: number | ''; + }; +} + +/** + * Helpers to create and delete SLM policies on the Elasticsearch instance + * during our tests. + * @param {ElasticsearchClient} es The Elasticsearch client instance + */ +export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { + let policiesCreated: string[] = []; + + const es = getService('legacyEs'); + + const createRepository = (repoName: string) => { + return es.snapshot.createRepository({ + repository: repoName, + body: { + type: 'fs', + settings: { + location: '/tmp/', + }, + }, + verify: false, + }); + }; + + const createPolicy = (policy: SlmPolicy, cachePolicy?: boolean) => { + if (cachePolicy) { + policiesCreated.push(policy.name); + } + + return es.sr.updatePolicy({ + name: policy.name, + body: policy, + }); + }; + + const getPolicy = (policyName: string) => { + return es.sr.policy({ + name: policyName, + human: true, + }); + }; + + const deletePolicy = (policyName: string) => es.sr.deletePolicy({ name: policyName }); + + const cleanupPolicies = () => + Promise.all(policiesCreated.map(deletePolicy)) + .then(() => { + policiesCreated = []; + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.log(`[Cleanup error] Error deleting ES resources: ${err.message}`); + }); + + return { + createRepository, + createPolicy, + deletePolicy, + cleanupPolicies, + getPolicy, + }; +}; diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts new file mode 100644 index 0000000000000..66ea0fe40c4ce --- /dev/null +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerEsHelpers } from './elasticsearch'; diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts new file mode 100644 index 0000000000000..575da0db2a759 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { registerEsHelpers } from './lib'; + +const API_BASE_PATH = '/api/snapshot_restore'; +const REPO_NAME = 'test_repo'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const { + createRepository, + createPolicy, + deletePolicy, + cleanupPolicies, + getPolicy, + } = registerEsHelpers(getService); + + describe('Snapshot Lifecycle Management', function () { + before(async () => { + try { + await createRepository(REPO_NAME); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error creating repository'); + throw err; + } + }); + + after(async () => { + await cleanupPolicies(); + }); + + describe('Create', () => { + const POLICY_NAME = 'test_create_policy'; + const REQUIRED_FIELDS_POLICY_NAME = 'test_create_required_fields_policy'; + + after(async () => { + // Clean up any policies created in test cases + await Promise.all([POLICY_NAME, REQUIRED_FIELDS_POLICY_NAME].map(deletePolicy)).catch( + (err) => { + // eslint-disable-next-line no-console + console.log(`[Cleanup error] Error deleting policies: ${err.message}`); + throw err; + } + ); + }); + + it('should create a SLM policy', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/policies`) + .set('kbn-xsrf', 'xxx') + .send({ + name: POLICY_NAME, + snapshotName: 'my_snapshot', + schedule: '0 30 1 * * ?', + repository: REPO_NAME, + config: { + indices: ['my_index'], + ignoreUnavailable: true, + partial: false, + metadata: { + meta: 'my_meta', + }, + }, + retention: { + expireAfterValue: 1, + expireAfterUnit: 'd', + maxCount: 10, + minCount: 5, + }, + isManagedPolicy: false, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + + const policyFromEs = await getPolicy(POLICY_NAME); + expect(policyFromEs[POLICY_NAME]).to.be.ok(); + expect(policyFromEs[POLICY_NAME].policy).to.eql({ + name: 'my_snapshot', + schedule: '0 30 1 * * ?', + repository: REPO_NAME, + config: { + indices: ['my_index'], + ignore_unavailable: true, + partial: false, + metadata: { + meta: 'my_meta', + }, + }, + retention: { + expire_after: '1d', + max_count: 10, + min_count: 5, + }, + }); + }); + + it('should create a policy with only required fields', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/policies`) + .set('kbn-xsrf', 'xxx') + // Exclude config and retention + .send({ + name: REQUIRED_FIELDS_POLICY_NAME, + snapshotName: 'my_snapshot', + repository: REPO_NAME, + schedule: '0 30 1 * * ?', + isManagedPolicy: false, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + + const policyFromEs = await getPolicy(REQUIRED_FIELDS_POLICY_NAME); + expect(policyFromEs[REQUIRED_FIELDS_POLICY_NAME]).to.be.ok(); + expect(policyFromEs[REQUIRED_FIELDS_POLICY_NAME].policy).to.eql({ + name: 'my_snapshot', + repository: REPO_NAME, + schedule: '0 30 1 * * ?', + }); + }); + }); + + describe('Update', () => { + const POLICY_NAME = 'test_update_policy'; + const POLICY = { + name: POLICY_NAME, + snapshotName: 'my_snapshot', + schedule: '0 30 1 * * ?', + repository: REPO_NAME, + config: { + indices: ['my_index'], + ignoreUnavailable: true, + partial: false, + metadata: { + meta: 'my_meta', + }, + }, + retention: { + expireAfterValue: 1, + expireAfterUnit: 'd', + maxCount: 10, + minCount: 5, + }, + isManagedPolicy: false, + }; + + before(async () => { + // Create SLM policy that can be used to test PUT request + try { + await createPolicy(POLICY, true); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error creating policy'); + throw err; + } + }); + + it('should allow an existing policy to be updated', async () => { + const uri = `${API_BASE_PATH}/policies/${POLICY_NAME}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...POLICY, + schedule: '0 0 0 ? * 7', + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + + const policyFromEs = await getPolicy(POLICY_NAME); + expect(policyFromEs[POLICY_NAME]).to.be.ok(); + expect(policyFromEs[POLICY_NAME].policy).to.eql({ + name: 'my_snapshot', + schedule: '0 0 0 ? * 7', + repository: REPO_NAME, + config: { + indices: ['my_index'], + ignore_unavailable: true, + partial: false, + metadata: { + meta: 'my_meta', + }, + }, + retention: { + expire_after: '1d', + max_count: 10, + min_count: 5, + }, + }); + }); + + it('should allow optional fields to be removed', async () => { + const uri = `${API_BASE_PATH}/policies/${POLICY_NAME}`; + const { retention, config, ...requiredFields } = POLICY; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send(requiredFields) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + + const policyFromEs = await getPolicy(POLICY_NAME); + expect(policyFromEs[POLICY_NAME]).to.be.ok(); + expect(policyFromEs[POLICY_NAME].policy).to.eql({ + name: 'my_snapshot', + schedule: '0 30 1 * * ?', + repository: REPO_NAME, + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap deleted file mode 100644 index 93abfaf67a009..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap +++ /dev/null @@ -1,371 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`monitor states endpoint will fetch monitor state data for the given down filters 1`] = ` -Object { - "nextPagePagination": "{\\"cursorDirection\\":\\"AFTER\\",\\"sortOrder\\":\\"ASC\\",\\"cursorKey\\":{\\"monitor_id\\":\\"0020-down\\"}}", - "prevPagePagination": null, - "summaries": Array [ - Object { - "histogram": Object { - "points": Array [ - Object { - "down": 1, - "timestamp": 1568172624744, - }, - Object { - "down": 2, - "timestamp": 1568172677247, - }, - Object { - "down": 1, - "timestamp": 1568172729750, - }, - Object { - "down": 2, - "timestamp": 1568172782253, - }, - Object { - "down": 2, - "timestamp": 1568172834756, - }, - Object { - "down": 2, - "timestamp": 1568172887259, - }, - Object { - "down": 1, - "timestamp": 1568172939762, - }, - Object { - "down": 2, - "timestamp": 1568172992265, - }, - Object { - "down": 2, - "timestamp": 1568173044768, - }, - Object { - "down": 2, - "timestamp": 1568173097271, - }, - Object { - "down": 1, - "timestamp": 1568173149774, - }, - Object { - "down": 2, - "timestamp": 1568173202277, - }, - ], - }, - "minInterval": 52503, - "monitor_id": "0010-down", - "state": Object { - "monitor": Object { - "name": "", - "type": "http", - }, - "observer": Object { - "geo": Object { - "name": Array [ - "mpls", - ], - }, - }, - "summary": Object { - "down": 1, - "status": "down", - "up": 0, - }, - "summaryPings": Array [ - Object { - "@timestamp": "2019-09-11T03:40:34.371Z", - "agent": Object { - "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", - "hostname": "avc-x1x", - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", - "type": "heartbeat", - "version": "8.0.0", - }, - "docId": "rZtoHm0B0I9WX_CznN_V", - "ecs": Object { - "version": "1.1.0", - }, - "error": Object { - "message": "400 Bad Request", - "type": "validate", - }, - "event": Object { - "dataset": "uptime", - }, - "host": Object { - "name": "avc-x1x", - }, - "http": Object { - "response": Object { - "body": Object { - "bytes": 3, - "content": "400", - "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", - }, - "status_code": 400, - }, - "rtt": Object { - "content": Object { - "us": 41, - }, - "response_header": Object { - "us": 36777, - }, - "total": Object { - "us": 37821, - }, - "validate": Object { - "us": 36818, - }, - "write_request": Object { - "us": 53, - }, - }, - }, - "monitor": Object { - "check_group": "d76f07d1-d445-11e9-88e3-3e80641b9c71", - "duration": Object { - "us": 37926, - }, - "id": "0010-down", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "http", - }, - "observer": Object { - "geo": Object { - "location": "37.926868, -78.024902", - "name": "mpls", - }, - "hostname": "avc-x1x", - }, - "resolve": Object { - "ip": "127.0.0.1", - "rtt": Object { - "us": 56, - }, - }, - "summary": Object { - "down": 1, - "up": 0, - }, - "tcp": Object { - "rtt": Object { - "connect": Object { - "us": 890, - }, - }, - }, - "timestamp": "2019-09-11T03:40:34.371Z", - "url": Object { - "domain": "localhost", - "full": "http://localhost:5678/pattern?r=400x1", - "path": "/pattern", - "port": 5678, - "query": "r=400x1", - "scheme": "http", - }, - }, - ], - "timestamp": "2019-09-11T03:40:34.371Z", - "url": Object { - "domain": "localhost", - "full": "http://localhost:5678/pattern?r=400x1", - "path": "/pattern", - "port": 5678, - "query": "r=400x1", - "scheme": "http", - }, - }, - }, - Object { - "histogram": Object { - "points": Array [ - Object { - "down": 1, - "timestamp": 1568172624744, - }, - Object { - "down": 2, - "timestamp": 1568172677247, - }, - Object { - "down": 1, - "timestamp": 1568172729750, - }, - Object { - "down": 2, - "timestamp": 1568172782253, - }, - Object { - "down": 2, - "timestamp": 1568172834756, - }, - Object { - "down": 2, - "timestamp": 1568172887259, - }, - Object { - "down": 1, - "timestamp": 1568172939762, - }, - Object { - "down": 2, - "timestamp": 1568172992265, - }, - Object { - "down": 2, - "timestamp": 1568173044768, - }, - Object { - "down": 2, - "timestamp": 1568173097271, - }, - Object { - "down": 1, - "timestamp": 1568173149774, - }, - Object { - "down": 2, - "timestamp": 1568173202277, - }, - ], - }, - "minInterval": 52503, - "monitor_id": "0020-down", - "state": Object { - "monitor": Object { - "name": "", - "type": "http", - }, - "observer": Object { - "geo": Object { - "name": Array [ - "mpls", - ], - }, - }, - "summary": Object { - "down": 1, - "status": "down", - "up": 0, - }, - "summaryPings": Array [ - Object { - "@timestamp": "2019-09-11T03:40:34.372Z", - "agent": Object { - "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", - "hostname": "avc-x1x", - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", - "type": "heartbeat", - "version": "8.0.0", - }, - "docId": "X5toHm0B0I9WX_CznN-6", - "ecs": Object { - "version": "1.1.0", - }, - "error": Object { - "message": "400 Bad Request", - "type": "validate", - }, - "event": Object { - "dataset": "uptime", - }, - "host": Object { - "name": "avc-x1x", - }, - "http": Object { - "response": Object { - "body": Object { - "bytes": 3, - "content": "400", - "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", - }, - "status_code": 400, - }, - "rtt": Object { - "content": Object { - "us": 54, - }, - "response_header": Object { - "us": 180, - }, - "total": Object { - "us": 555, - }, - "validate": Object { - "us": 234, - }, - "write_request": Object { - "us": 63, - }, - }, - }, - "monitor": Object { - "check_group": "d7712ecb-d445-11e9-88e3-3e80641b9c71", - "duration": Object { - "us": 14900, - }, - "id": "0020-down", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "http", - }, - "observer": Object { - "geo": Object { - "location": "37.926868, -78.024902", - "name": "mpls", - }, - "hostname": "avc-x1x", - }, - "resolve": Object { - "ip": "127.0.0.1", - "rtt": Object { - "us": 14294, - }, - }, - "summary": Object { - "down": 1, - "up": 0, - }, - "tcp": Object { - "rtt": Object { - "connect": Object { - "us": 105, - }, - }, - }, - "timestamp": "2019-09-11T03:40:34.372Z", - "url": Object { - "domain": "localhost", - "full": "http://localhost:5678/pattern?r=400x1", - "path": "/pattern", - "port": 5678, - "query": "r=400x1", - "scheme": "http", - }, - }, - ], - "timestamp": "2019-09-11T03:40:34.372Z", - "url": Object { - "domain": "localhost", - "full": "http://localhost:5678/pattern?r=400x1", - "path": "/pattern", - "port": 5678, - "query": "r=400x1", - "scheme": "http", - }, - }, - }, - ], - "totalSummaryCount": 2000, -} -`; diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 04b991151034a..c184a87365977 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -10,6 +10,7 @@ import * as legacyElasticsearch from 'elasticsearch'; import { elasticsearchClientPlugin as securityEsClientPlugin } from '../../../plugins/security/server/elasticsearch/elasticsearch_client_plugin'; import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; +import { elasticsearchJsPlugin as snapshotRestoreEsClientPlugin } from '../../../plugins/snapshot_restore/server/client/elasticsearch_sr'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -20,6 +21,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [securityEsClientPlugin, indexManagementEsClientPlugin], + plugins: [securityEsClientPlugin, indexManagementEsClientPlugin, snapshotRestoreEsClientPlugin], }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap b/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap deleted file mode 100644 index 434660cdc2c62..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap +++ /dev/null @@ -1,250 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Throughput when data is loaded returns the service throughput has the correct throughput 1`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 6, - }, - Object { - "x": 1601389830000, - "y": 0, - }, - Object { - "x": 1601389860000, - "y": 0, - }, - Object { - "x": 1601389890000, - "y": 0, - }, - Object { - "x": 1601389920000, - "y": 3, - }, - Object { - "x": 1601389950000, - "y": 1, - }, - Object { - "x": 1601389980000, - "y": 0, - }, - Object { - "x": 1601390010000, - "y": 0, - }, - Object { - "x": 1601390040000, - "y": 3, - }, - Object { - "x": 1601390070000, - "y": 2, - }, - Object { - "x": 1601390100000, - "y": 0, - }, - Object { - "x": 1601390130000, - "y": 0, - }, - Object { - "x": 1601390160000, - "y": 7, - }, - Object { - "x": 1601390190000, - "y": 3, - }, - Object { - "x": 1601390220000, - "y": 2, - }, - Object { - "x": 1601390250000, - "y": 0, - }, - Object { - "x": 1601390280000, - "y": 0, - }, - Object { - "x": 1601390310000, - "y": 8, - }, - Object { - "x": 1601390340000, - "y": 0, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": 3, - }, - Object { - "x": 1601390430000, - "y": 0, - }, - Object { - "x": 1601390460000, - "y": 0, - }, - Object { - "x": 1601390490000, - "y": 0, - }, - Object { - "x": 1601390520000, - "y": 4, - }, - Object { - "x": 1601390550000, - "y": 3, - }, - Object { - "x": 1601390580000, - "y": 2, - }, - Object { - "x": 1601390610000, - "y": 0, - }, - Object { - "x": 1601390640000, - "y": 1, - }, - Object { - "x": 1601390670000, - "y": 2, - }, - Object { - "x": 1601390700000, - "y": 0, - }, - Object { - "x": 1601390730000, - "y": 0, - }, - Object { - "x": 1601390760000, - "y": 4, - }, - Object { - "x": 1601390790000, - "y": 1, - }, - Object { - "x": 1601390820000, - "y": 1, - }, - Object { - "x": 1601390850000, - "y": 0, - }, - Object { - "x": 1601390880000, - "y": 6, - }, - Object { - "x": 1601390910000, - "y": 0, - }, - Object { - "x": 1601390940000, - "y": 3, - }, - Object { - "x": 1601390970000, - "y": 0, - }, - Object { - "x": 1601391000000, - "y": 4, - }, - Object { - "x": 1601391030000, - "y": 0, - }, - Object { - "x": 1601391060000, - "y": 1, - }, - Object { - "x": 1601391090000, - "y": 0, - }, - Object { - "x": 1601391120000, - "y": 2, - }, - Object { - "x": 1601391150000, - "y": 1, - }, - Object { - "x": 1601391180000, - "y": 2, - }, - Object { - "x": 1601391210000, - "y": 0, - }, - Object { - "x": 1601391240000, - "y": 1, - }, - Object { - "x": 1601391270000, - "y": 0, - }, - Object { - "x": 1601391300000, - "y": 1, - }, - Object { - "x": 1601391330000, - "y": 0, - }, - Object { - "x": 1601391360000, - "y": 1, - }, - Object { - "x": 1601391390000, - "y": 0, - }, - Object { - "x": 1601391420000, - "y": 0, - }, - Object { - "x": 1601391450000, - "y": 0, - }, - Object { - "x": 1601391480000, - "y": 10, - }, - Object { - "x": 1601391510000, - "y": 3, - }, - Object { - "x": 1601391540000, - "y": 1, - }, - Object { - "x": 1601391570000, - "y": 0, - }, - Object { - "x": 1601391600000, - "y": 0, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap deleted file mode 100644 index 9cecb0b3b1dd7..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap +++ /dev/null @@ -1,774 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Top traces when data is loaded returns the correct buckets 1`] = ` -Array [ - Object { - "averageResponseTime": 1756, - "impact": 0, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doPost", - }, - "serviceName": "opbeans-java", - "transactionName": "DispatcherServlet#doPost", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 3251, - "impact": 0.00224063647384788, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/types", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/types", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 3813, - "impact": 0.00308293593759538, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "ResourceHttpRequestHandler", - }, - "serviceName": "opbeans-java", - "transactionName": "ResourceHttpRequestHandler", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 7741, - "impact": 0.0089700396628626, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/top", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/top", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 7994, - "impact": 0.00934922429689839, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "POST /api/orders", - }, - "serviceName": "opbeans-go", - "transactionName": "POST /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 10317, - "impact": 0.0128308286639543, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/orders/:id", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 10837, - "impact": 0.0136101804809449, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#topProducts", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#topProducts", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 6495, - "impact": 0.0168369967539847, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 13952, - "impact": 0.0182787976154172, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#stats", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#stats", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 7324.5, - "impact": 0.0193234288008834, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customerWhoBought", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#customerWhoBought", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 7089.66666666667, - "impact": 0.0292451769325711, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/customers/:id", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/customers/:id", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 11759.5, - "impact": 0.0326173722945495, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/customers/:id", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 8109.33333333333, - "impact": 0.0338298638713675, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customer", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#customer", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 8677.33333333333, - "impact": 0.0363837398255058, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#order", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#order", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 26624, - "impact": 0.0372710018940797, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/customers", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 5687.8, - "impact": 0.0399912394860756, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/products", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/products", - "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, - }, - Object { - "averageResponseTime": 9496.33333333333, - "impact": 0.0400661771607863, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 10717.3333333333, - "impact": 0.0455561112100871, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#products", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#products", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 8438.75, - "impact": 0.04795861306131, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/orders", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 17322.5, - "impact": 0.0492925036711592, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customers", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#customers", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 34696, - "impact": 0.0493689400993641, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 7321.4, - "impact": 0.0522330580268044, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/types/:id", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/types/:id", - "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, - }, - Object { - "averageResponseTime": 9663.5, - "impact": 0.0553010064294577, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::OrdersController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::OrdersController#show", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 44819, - "impact": 0.0645408217212785, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.products", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.products", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 14944, - "impact": 0.0645603055167033, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#index", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 24056, - "impact": 0.0694762169777207, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product_types", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product_types", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 8401.33333333333, - "impact": 0.0729173550004329, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/types", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/types", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 13182, - "impact": 0.0763944631070062, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id/customers", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id/customers", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 7923, - "impact": 0.0804905564066893, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::TypesController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::TypesController#index", - "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, - }, - Object { - "averageResponseTime": 19838.6666666667, - "impact": 0.0865680018257216, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::CustomersController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::CustomersController#index", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 7952.33333333333, - "impact": 0.104635475198455, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/orders/:id", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/orders/:id", - "transactionType": "request", - "transactionsPerMinute": 0.3, - }, - Object { - "averageResponseTime": 19666, - "impact": 0.115266133732905, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/stats", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/stats", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 40188.5, - "impact": 0.117833498468491, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.customer", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.customer", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 26802.3333333333, - "impact": 0.117878461073318, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#show", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 14709.3333333333, - "impact": 0.129642177249393, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::StatsController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::StatsController#index", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 15432, - "impact": 0.136140772400299, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::TypesController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::TypesController#show", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 33266.3333333333, - "impact": 0.146942288833089, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.orders", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.orders", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 33445.3333333333, - "impact": 0.147747119459481, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.customers", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.customers", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 107438, - "impact": 0.158391266775379, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.top_products", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.top_products", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 27696.75, - "impact": 0.163410592227497, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#top", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#top", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 55832.5, - "impact": 0.164726497795416, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.stats", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.stats", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 10483.6363636364, - "impact": 0.170204441816763, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.order", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.order", - "transactionType": "request", - "transactionsPerMinute": 0.366666666666667, - }, - Object { - "averageResponseTime": 24524.5, - "impact": 0.217905269277069, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/customers", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/customers", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 14822.3, - "impact": 0.219517928036841, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::CustomersController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::CustomersController#show", - "transactionType": "request", - "transactionsPerMinute": 0.333333333333333, - }, - Object { - "averageResponseTime": 44771.75, - "impact": 0.26577545588222, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/stats", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/stats", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 39421.4285714286, - "impact": 0.410949215592138, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::OrdersController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::OrdersController#index", - "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, - }, - Object { - "averageResponseTime": 33513.3076923077, - "impact": 0.650334619948262, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/products/:id", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/products/:id", - "transactionType": "request", - "transactionsPerMinute": 0.433333333333333, - }, - Object { - "averageResponseTime": 28933.2222222222, - "impact": 0.777916011143112, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api", - "transactionType": "request", - "transactionsPerMinute": 0.6, - }, - Object { - "averageResponseTime": 101613, - "impact": 1.06341806051616, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/products/:id/customers", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/products/:id/customers", - "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, - }, - Object { - "averageResponseTime": 377325, - "impact": 1.12840251327172, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product_customers", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product_customers", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 39452.8333333333, - "impact": 3.54517249775948, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.sync_orders", - }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.sync_orders", - "transactionType": "celery", - "transactionsPerMinute": 2, - }, - Object { - "averageResponseTime": 715444.444444444, - "impact": 9.64784193809929, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/customers", - }, - "serviceName": "opbeans-rum", - "transactionName": "/customers", - "transactionType": "page-load", - "transactionsPerMinute": 0.3, - }, - Object { - "averageResponseTime": 833539.125, - "impact": 9.99152559811767, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/orders", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.266666666666667, - }, - Object { - "averageResponseTime": 7480000, - "impact": 11.2080443255746, - "key": Object { - "service.name": "elastic-co-frontend", - "transaction.name": "/community/security", - }, - "serviceName": "elastic-co-frontend", - "transactionName": "/community/security", - "transactionType": "page-load", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 171383.519230769, - "impact": 13.354173900338, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Rack", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Rack", - "transactionType": "request", - "transactionsPerMinute": 1.73333333333333, - }, - Object { - "averageResponseTime": 1052468.6, - "impact": 15.7712781068549, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doGet", - }, - "serviceName": "opbeans-java", - "transactionName": "DispatcherServlet#doGet", - "transactionType": "request", - "transactionsPerMinute": 0.333333333333333, - }, - Object { - "averageResponseTime": 1413866.66666667, - "impact": 31.7829322941256, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/products", - }, - "serviceName": "opbeans-rum", - "transactionName": "/products", - "transactionType": "page-load", - "transactionsPerMinute": 0.5, - }, - Object { - "averageResponseTime": 996583.333333333, - "impact": 35.8445542634419, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/dashboard", - }, - "serviceName": "opbeans-rum", - "transactionName": "/dashboard", - "transactionType": "page-load", - "transactionsPerMinute": 0.8, - }, - Object { - "averageResponseTime": 1046912.60465116, - "impact": 67.4671169361798, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "Process completed order", - }, - "serviceName": "opbeans-node", - "transactionName": "Process completed order", - "transactionType": "Worker", - "transactionsPerMinute": 1.43333333333333, - }, - Object { - "averageResponseTime": 1142941.8, - "impact": 68.5168888461311, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "Update shipping status", - }, - "serviceName": "opbeans-node", - "transactionName": "Update shipping status", - "transactionType": "Worker", - "transactionsPerMinute": 1.33333333333333, - }, - Object { - "averageResponseTime": 128285.213888889, - "impact": 69.2138167147075, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.update_stats", - }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.update_stats", - "transactionType": "celery", - "transactionsPerMinute": 12, - }, - Object { - "averageResponseTime": 1032979.06666667, - "impact": 69.6655125415468, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "Process payment", - }, - "serviceName": "opbeans-node", - "transactionName": "Process payment", - "transactionType": "Worker", - "transactionsPerMinute": 1.5, - }, - Object { - "averageResponseTime": 4410285.71428571, - "impact": 92.5364039355288, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/orders", - }, - "serviceName": "opbeans-rum", - "transactionName": "/orders", - "transactionType": "page-load", - "transactionsPerMinute": 0.466666666666667, - }, - Object { - "averageResponseTime": 1803347.81081081, - "impact": 100, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.sync_customers", - }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.sync_customers", - "transactionType": "celery", - "transactionsPerMinute": 1.23333333333333, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap deleted file mode 100644 index 5f598ba72cd72..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap +++ /dev/null @@ -1,1016 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Breakdown when data is loaded returns the transaction breakdown for a service 1`] = ` -Object { - "timeseries": Array [ - Object { - "color": "#54b399", - "data": Array [ - Object { - "x": 1601389800000, - "y": 0.0161290322580645, - }, - Object { - "x": 1601389830000, - "y": 0.402597402597403, - }, - Object { - "x": 1601389860000, - "y": 0.0303030303030303, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 0.518072289156627, - }, - Object { - "x": 1601389950000, - "y": 0.120603015075377, - }, - Object { - "x": 1601389980000, - "y": 0.823529411764706, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 0.273381294964029, - }, - Object { - "x": 1601390070000, - "y": 0.39047619047619, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": 0.733333333333333, - }, - Object { - "x": 1601390160000, - "y": 0.144230769230769, - }, - Object { - "x": 1601390190000, - "y": 0.0688524590163934, - }, - Object { - "x": 1601390220000, - "y": null, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": 0.0540540540540541, - }, - Object { - "x": 1601390310000, - "y": null, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": 1, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 0.75, - }, - Object { - "x": 1601390460000, - "y": 0.764705882352941, - }, - Object { - "x": 1601390490000, - "y": 0.117647058823529, - }, - Object { - "x": 1601390520000, - "y": 0.220588235294118, - }, - Object { - "x": 1601390550000, - "y": 0.302325581395349, - }, - Object { - "x": 1601390580000, - "y": null, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": null, - }, - Object { - "x": 1601390670000, - "y": 0.215686274509804, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 0.217391304347826, - }, - Object { - "x": 1601390790000, - "y": 0.253333333333333, - }, - Object { - "x": 1601390820000, - "y": null, - }, - Object { - "x": 1601390850000, - "y": 0.117647058823529, - }, - Object { - "x": 1601390880000, - "y": 0.361111111111111, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": null, - }, - Object { - "x": 1601390970000, - "y": 0.19047619047619, - }, - Object { - "x": 1601391000000, - "y": 0.354430379746835, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": null, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 0.437956204379562, - }, - Object { - "x": 1601391150000, - "y": 0.0175438596491228, - }, - Object { - "x": 1601391180000, - "y": null, - }, - Object { - "x": 1601391210000, - "y": 0.277777777777778, - }, - Object { - "x": 1601391240000, - "y": 1, - }, - Object { - "x": 1601391270000, - "y": 0.885714285714286, - }, - Object { - "x": 1601391300000, - "y": null, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 0.111111111111111, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": 0.764705882352941, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 0.0338983050847458, - }, - Object { - "x": 1601391510000, - "y": 0.293233082706767, - }, - Object { - "x": 1601391540000, - "y": null, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "hideLegend": false, - "legendValue": "25%", - "title": "app", - "type": "areaStacked", - }, - Object { - "color": "#6092c0", - "data": Array [ - Object { - "x": 1601389800000, - "y": 0.983870967741935, - }, - Object { - "x": 1601389830000, - "y": 0.545454545454545, - }, - Object { - "x": 1601389860000, - "y": 0.96969696969697, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 0.156626506024096, - }, - Object { - "x": 1601389950000, - "y": 0.85929648241206, - }, - Object { - "x": 1601389980000, - "y": 0, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 0.482014388489209, - }, - Object { - "x": 1601390070000, - "y": 0.361904761904762, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": 0, - }, - Object { - "x": 1601390160000, - "y": 0.759615384615385, - }, - Object { - "x": 1601390190000, - "y": 0.931147540983607, - }, - Object { - "x": 1601390220000, - "y": null, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": 0.945945945945946, - }, - Object { - "x": 1601390310000, - "y": null, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 0, - }, - Object { - "x": 1601390460000, - "y": 0, - }, - Object { - "x": 1601390490000, - "y": 0.784313725490196, - }, - Object { - "x": 1601390520000, - "y": 0.544117647058823, - }, - Object { - "x": 1601390550000, - "y": 0.558139534883721, - }, - Object { - "x": 1601390580000, - "y": null, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": null, - }, - Object { - "x": 1601390670000, - "y": 0.784313725490196, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 0.536231884057971, - }, - Object { - "x": 1601390790000, - "y": 0.746666666666667, - }, - Object { - "x": 1601390820000, - "y": null, - }, - Object { - "x": 1601390850000, - "y": 0.735294117647059, - }, - Object { - "x": 1601390880000, - "y": 0.416666666666667, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": null, - }, - Object { - "x": 1601390970000, - "y": 0.619047619047619, - }, - Object { - "x": 1601391000000, - "y": 0.518987341772152, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": null, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 0.408759124087591, - }, - Object { - "x": 1601391150000, - "y": 0.982456140350877, - }, - Object { - "x": 1601391180000, - "y": null, - }, - Object { - "x": 1601391210000, - "y": 0.648148148148148, - }, - Object { - "x": 1601391240000, - "y": 0, - }, - Object { - "x": 1601391270000, - "y": 0, - }, - Object { - "x": 1601391300000, - "y": null, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 0.888888888888889, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": 0, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 0.966101694915254, - }, - Object { - "x": 1601391510000, - "y": 0.676691729323308, - }, - Object { - "x": 1601391540000, - "y": null, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "hideLegend": false, - "legendValue": "65%", - "title": "http", - "type": "areaStacked", - }, - Object { - "color": "#d36086", - "data": Array [ - Object { - "x": 1601389800000, - "y": 0, - }, - Object { - "x": 1601389830000, - "y": 0.051948051948052, - }, - Object { - "x": 1601389860000, - "y": 0, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 0.325301204819277, - }, - Object { - "x": 1601389950000, - "y": 0.0201005025125628, - }, - Object { - "x": 1601389980000, - "y": 0.176470588235294, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 0.244604316546763, - }, - Object { - "x": 1601390070000, - "y": 0.247619047619048, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": 0.266666666666667, - }, - Object { - "x": 1601390160000, - "y": 0.0961538461538462, - }, - Object { - "x": 1601390190000, - "y": 0, - }, - Object { - "x": 1601390220000, - "y": null, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": 0, - }, - Object { - "x": 1601390310000, - "y": null, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 0.25, - }, - Object { - "x": 1601390460000, - "y": 0.235294117647059, - }, - Object { - "x": 1601390490000, - "y": 0.0980392156862745, - }, - Object { - "x": 1601390520000, - "y": 0.235294117647059, - }, - Object { - "x": 1601390550000, - "y": 0.13953488372093, - }, - Object { - "x": 1601390580000, - "y": null, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": null, - }, - Object { - "x": 1601390670000, - "y": 0, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 0.246376811594203, - }, - Object { - "x": 1601390790000, - "y": 0, - }, - Object { - "x": 1601390820000, - "y": null, - }, - Object { - "x": 1601390850000, - "y": 0.147058823529412, - }, - Object { - "x": 1601390880000, - "y": 0.222222222222222, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": null, - }, - Object { - "x": 1601390970000, - "y": 0.19047619047619, - }, - Object { - "x": 1601391000000, - "y": 0.126582278481013, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": null, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 0.153284671532847, - }, - Object { - "x": 1601391150000, - "y": 0, - }, - Object { - "x": 1601391180000, - "y": null, - }, - Object { - "x": 1601391210000, - "y": 0.0740740740740741, - }, - Object { - "x": 1601391240000, - "y": 0, - }, - Object { - "x": 1601391270000, - "y": 0.114285714285714, - }, - Object { - "x": 1601391300000, - "y": null, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 0, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": 0.235294117647059, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 0, - }, - Object { - "x": 1601391510000, - "y": 0.0300751879699248, - }, - Object { - "x": 1601391540000, - "y": null, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "hideLegend": false, - "legendValue": "10%", - "title": "postgresql", - "type": "areaStacked", - }, - ], -} -`; - -exports[`Breakdown when data is loaded returns the transaction breakdown for a transaction group 9`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 1, - }, - Object { - "x": 1601389830000, - "y": 1, - }, - Object { - "x": 1601389860000, - "y": 1, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 1, - }, - Object { - "x": 1601389950000, - "y": 1, - }, - Object { - "x": 1601389980000, - "y": null, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 1, - }, - Object { - "x": 1601390070000, - "y": 1, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": null, - }, - Object { - "x": 1601390160000, - "y": 1, - }, - Object { - "x": 1601390190000, - "y": 1, - }, - Object { - "x": 1601390220000, - "y": null, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": 1, - }, - Object { - "x": 1601390310000, - "y": null, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": null, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": null, - }, - Object { - "x": 1601390460000, - "y": null, - }, - Object { - "x": 1601390490000, - "y": 1, - }, - Object { - "x": 1601390520000, - "y": 1, - }, - Object { - "x": 1601390550000, - "y": 1, - }, - Object { - "x": 1601390580000, - "y": null, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": null, - }, - Object { - "x": 1601390670000, - "y": 1, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 1, - }, - Object { - "x": 1601390790000, - "y": 1, - }, - Object { - "x": 1601390820000, - "y": null, - }, - Object { - "x": 1601390850000, - "y": 1, - }, - Object { - "x": 1601390880000, - "y": 1, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": null, - }, - Object { - "x": 1601390970000, - "y": 1, - }, - Object { - "x": 1601391000000, - "y": 1, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": null, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 1, - }, - Object { - "x": 1601391150000, - "y": 1, - }, - Object { - "x": 1601391180000, - "y": null, - }, - Object { - "x": 1601391210000, - "y": 1, - }, - Object { - "x": 1601391240000, - "y": null, - }, - Object { - "x": 1601391270000, - "y": null, - }, - Object { - "x": 1601391300000, - "y": null, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 1, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": null, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 1, - }, - Object { - "x": 1601391510000, - "y": 1, - }, - Object { - "x": 1601391540000, - "y": null, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap deleted file mode 100644 index 1161beb7f06c0..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap +++ /dev/null @@ -1,250 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Error rate when data is loaded returns the transaction error rate has the correct error rate 1`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 0.166666666666667, - }, - Object { - "x": 1601389830000, - "y": null, - }, - Object { - "x": 1601389860000, - "y": null, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 0, - }, - Object { - "x": 1601389950000, - "y": 0, - }, - Object { - "x": 1601389980000, - "y": null, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 0, - }, - Object { - "x": 1601390070000, - "y": 0.5, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": null, - }, - Object { - "x": 1601390160000, - "y": 0.285714285714286, - }, - Object { - "x": 1601390190000, - "y": 0, - }, - Object { - "x": 1601390220000, - "y": 0, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": null, - }, - Object { - "x": 1601390310000, - "y": 0, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": null, - }, - Object { - "x": 1601390400000, - "y": 0, - }, - Object { - "x": 1601390430000, - "y": null, - }, - Object { - "x": 1601390460000, - "y": null, - }, - Object { - "x": 1601390490000, - "y": null, - }, - Object { - "x": 1601390520000, - "y": 0, - }, - Object { - "x": 1601390550000, - "y": 1, - }, - Object { - "x": 1601390580000, - "y": 0, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": 1, - }, - Object { - "x": 1601390670000, - "y": 0.5, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 0.25, - }, - Object { - "x": 1601390790000, - "y": 0, - }, - Object { - "x": 1601390820000, - "y": 0, - }, - Object { - "x": 1601390850000, - "y": null, - }, - Object { - "x": 1601390880000, - "y": 0.166666666666667, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": 0.333333333333333, - }, - Object { - "x": 1601390970000, - "y": null, - }, - Object { - "x": 1601391000000, - "y": 0, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": 1, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 0, - }, - Object { - "x": 1601391150000, - "y": 0, - }, - Object { - "x": 1601391180000, - "y": 0, - }, - Object { - "x": 1601391210000, - "y": null, - }, - Object { - "x": 1601391240000, - "y": 0, - }, - Object { - "x": 1601391270000, - "y": null, - }, - Object { - "x": 1601391300000, - "y": 0, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 0, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": null, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 0, - }, - Object { - "x": 1601391510000, - "y": 0, - }, - Object { - "x": 1601391540000, - "y": 1, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap deleted file mode 100644 index 9ff2294cdb08f..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap +++ /dev/null @@ -1,126 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Top transaction groups when data is loaded returns the correct buckets (when ignoring samples) 1`] = ` -Array [ - Object { - "averageResponseTime": 2292, - "impact": 0, - "key": "GET /*", - "p95": 2288, - "serviceName": "opbeans-node", - "transactionName": "GET /*", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 10317, - "impact": 0.420340829629707, - "key": "GET /api/orders/:id", - "p95": 10304, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 6495, - "impact": 0.560349681667116, - "key": "GET /api/products/:id", - "p95": 6720, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 9825.5, - "impact": 0.909245664989668, - "key": "GET /api/types", - "p95": 16496, - "serviceName": "opbeans-node", - "transactionName": "GET /api/types", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 9516.83333333333, - "impact": 2.87083620326164, - "key": "GET /api/products", - "p95": 17888, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 13962.2, - "impact": 3.53657227112376, - "key": "GET /api/products/:id/customers", - "p95": 23264, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id/customers", - "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, - }, - Object { - "averageResponseTime": 21129.5, - "impact": 4.3069090413872, - "key": "GET /api/customers/:id", - "p95": 32608, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers/:id", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 10137.1111111111, - "impact": 4.65868586528666, - "key": "GET /api/orders", - "p95": 21344, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.3, - }, - Object { - "averageResponseTime": 24206.25, - "impact": 4.95153640465858, - "key": "GET /api/customers", - "p95": 36032, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 17267.0833333333, - "impact": 10.7331215479018, - "key": "GET /api/products/top", - "p95": 26208, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/top", - "transactionType": "request", - "transactionsPerMinute": 0.4, - }, - Object { - "averageResponseTime": 20417.7272727273, - "impact": 11.6439909593985, - "key": "GET /api/stats", - "p95": 24800, - "serviceName": "opbeans-node", - "transactionName": "GET /api/stats", - "transactionType": "request", - "transactionsPerMinute": 0.366666666666667, - }, - Object { - "averageResponseTime": 39822.0208333333, - "impact": 100, - "key": "GET /api", - "p95": 122816, - "serviceName": "opbeans-node", - "transactionName": "GET /api", - "transactionType": "request", - "transactionsPerMinute": 1.6, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap deleted file mode 100644 index a75b8918ed5e4..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap +++ /dev/null @@ -1,1501 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transaction charts when data is loaded returns the correct data 4`] = ` -Object { - "apmTimeseries": Object { - "overallAvgDuration": 600888.274678112, - "responseTimes": Object { - "avg": Array [ - Object { - "x": 1601389800000, - "y": 651784.714285714, - }, - Object { - "x": 1601389830000, - "y": 747797.4, - }, - Object { - "x": 1601389860000, - "y": 567568.333333333, - }, - Object { - "x": 1601389890000, - "y": 1289936, - }, - Object { - "x": 1601389920000, - "y": 79698.6, - }, - Object { - "x": 1601389950000, - "y": 646660.833333333, - }, - Object { - "x": 1601389980000, - "y": 18095, - }, - Object { - "x": 1601390010000, - "y": 543534, - }, - Object { - "x": 1601390040000, - "y": 250234.466666667, - }, - Object { - "x": 1601390070000, - "y": 200435.2, - }, - Object { - "x": 1601390100000, - "y": 1089389.66666667, - }, - Object { - "x": 1601390130000, - "y": 1052697.33333333, - }, - Object { - "x": 1601390160000, - "y": 27908.8333333333, - }, - Object { - "x": 1601390190000, - "y": 1078058.25, - }, - Object { - "x": 1601390220000, - "y": 755843.5, - }, - Object { - "x": 1601390250000, - "y": 1371940.33333333, - }, - Object { - "x": 1601390280000, - "y": 38056, - }, - Object { - "x": 1601390310000, - "y": 1133161.33333333, - }, - Object { - "x": 1601390340000, - "y": 1236497, - }, - Object { - "x": 1601390370000, - "y": 870027, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 800475, - }, - Object { - "x": 1601390460000, - "y": 374597.2, - }, - Object { - "x": 1601390490000, - "y": 657002, - }, - Object { - "x": 1601390520000, - "y": 305164.5, - }, - Object { - "x": 1601390550000, - "y": 274576.4, - }, - Object { - "x": 1601390580000, - "y": 888533, - }, - Object { - "x": 1601390610000, - "y": 1191308, - }, - Object { - "x": 1601390640000, - "y": 1521297, - }, - Object { - "x": 1601390670000, - "y": 373994.4, - }, - Object { - "x": 1601390700000, - "y": 1108442, - }, - Object { - "x": 1601390730000, - "y": 1014666.66666667, - }, - Object { - "x": 1601390760000, - "y": 184717, - }, - Object { - "x": 1601390790000, - "y": 369595.5, - }, - Object { - "x": 1601390820000, - "y": 525805.5, - }, - Object { - "x": 1601390850000, - "y": 583359, - }, - Object { - "x": 1601390880000, - "y": 315244.25, - }, - Object { - "x": 1601390910000, - "y": 1133846, - }, - Object { - "x": 1601390940000, - "y": 312801, - }, - Object { - "x": 1601390970000, - "y": 1135768.33333333, - }, - Object { - "x": 1601391000000, - "y": 199876, - }, - Object { - "x": 1601391030000, - "y": 1508216.66666667, - }, - Object { - "x": 1601391060000, - "y": 1481690.5, - }, - Object { - "x": 1601391090000, - "y": 659469, - }, - Object { - "x": 1601391120000, - "y": 225622.666666667, - }, - Object { - "x": 1601391150000, - "y": 675812.666666667, - }, - Object { - "x": 1601391180000, - "y": 279013.333333333, - }, - Object { - "x": 1601391210000, - "y": 1327234, - }, - Object { - "x": 1601391240000, - "y": 487259, - }, - Object { - "x": 1601391270000, - "y": 686597.333333333, - }, - Object { - "x": 1601391300000, - "y": 1236063.33333333, - }, - Object { - "x": 1601391330000, - "y": 1322639, - }, - Object { - "x": 1601391360000, - "y": 517955.333333333, - }, - Object { - "x": 1601391390000, - "y": 983213.333333333, - }, - Object { - "x": 1601391420000, - "y": 920165.5, - }, - Object { - "x": 1601391450000, - "y": 655826, - }, - Object { - "x": 1601391480000, - "y": 335100.666666667, - }, - Object { - "x": 1601391510000, - "y": 496048.555555556, - }, - Object { - "x": 1601391540000, - "y": 629243, - }, - Object { - "x": 1601391570000, - "y": 796819.4, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "p95": Array [ - Object { - "x": 1601389800000, - "y": 1531888, - }, - Object { - "x": 1601389830000, - "y": 1695616, - }, - Object { - "x": 1601389860000, - "y": 1482496, - }, - Object { - "x": 1601389890000, - "y": 1617920, - }, - Object { - "x": 1601389920000, - "y": 329696, - }, - Object { - "x": 1601389950000, - "y": 1474432, - }, - Object { - "x": 1601389980000, - "y": 18048, - }, - Object { - "x": 1601390010000, - "y": 990720, - }, - Object { - "x": 1601390040000, - "y": 1163232, - }, - Object { - "x": 1601390070000, - "y": 958432, - }, - Object { - "x": 1601390100000, - "y": 1777600, - }, - Object { - "x": 1601390130000, - "y": 1873920, - }, - Object { - "x": 1601390160000, - "y": 55776, - }, - Object { - "x": 1601390190000, - "y": 1752064, - }, - Object { - "x": 1601390220000, - "y": 1136640, - }, - Object { - "x": 1601390250000, - "y": 1523712, - }, - Object { - "x": 1601390280000, - "y": 37888, - }, - Object { - "x": 1601390310000, - "y": 1196032, - }, - Object { - "x": 1601390340000, - "y": 1810304, - }, - Object { - "x": 1601390370000, - "y": 1007616, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 1523584, - }, - Object { - "x": 1601390460000, - "y": 1712096, - }, - Object { - "x": 1601390490000, - "y": 679936, - }, - Object { - "x": 1601390520000, - "y": 1163200, - }, - Object { - "x": 1601390550000, - "y": 1171392, - }, - Object { - "x": 1601390580000, - "y": 901120, - }, - Object { - "x": 1601390610000, - "y": 1355776, - }, - Object { - "x": 1601390640000, - "y": 1515520, - }, - Object { - "x": 1601390670000, - "y": 1097600, - }, - Object { - "x": 1601390700000, - "y": 1363968, - }, - Object { - "x": 1601390730000, - "y": 1290240, - }, - Object { - "x": 1601390760000, - "y": 663488, - }, - Object { - "x": 1601390790000, - "y": 827264, - }, - Object { - "x": 1601390820000, - "y": 1302400, - }, - Object { - "x": 1601390850000, - "y": 978912, - }, - Object { - "x": 1601390880000, - "y": 1482720, - }, - Object { - "x": 1601390910000, - "y": 1306624, - }, - Object { - "x": 1601390940000, - "y": 1179520, - }, - Object { - "x": 1601390970000, - "y": 1347584, - }, - Object { - "x": 1601391000000, - "y": 1122272, - }, - Object { - "x": 1601391030000, - "y": 1835008, - }, - Object { - "x": 1601391060000, - "y": 1572864, - }, - Object { - "x": 1601391090000, - "y": 1343232, - }, - Object { - "x": 1601391120000, - "y": 810880, - }, - Object { - "x": 1601391150000, - "y": 1122048, - }, - Object { - "x": 1601391180000, - "y": 782208, - }, - Object { - "x": 1601391210000, - "y": 1466368, - }, - Object { - "x": 1601391240000, - "y": 1490928, - }, - Object { - "x": 1601391270000, - "y": 1433472, - }, - Object { - "x": 1601391300000, - "y": 1677312, - }, - Object { - "x": 1601391330000, - "y": 1830912, - }, - Object { - "x": 1601391360000, - "y": 950144, - }, - Object { - "x": 1601391390000, - "y": 1265664, - }, - Object { - "x": 1601391420000, - "y": 1408896, - }, - Object { - "x": 1601391450000, - "y": 1178624, - }, - Object { - "x": 1601391480000, - "y": 946048, - }, - Object { - "x": 1601391510000, - "y": 1761248, - }, - Object { - "x": 1601391540000, - "y": 626688, - }, - Object { - "x": 1601391570000, - "y": 1564544, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "p99": Array [ - Object { - "x": 1601389800000, - "y": 1531888, - }, - Object { - "x": 1601389830000, - "y": 1695616, - }, - Object { - "x": 1601389860000, - "y": 1482496, - }, - Object { - "x": 1601389890000, - "y": 1617920, - }, - Object { - "x": 1601389920000, - "y": 329696, - }, - Object { - "x": 1601389950000, - "y": 1474432, - }, - Object { - "x": 1601389980000, - "y": 18048, - }, - Object { - "x": 1601390010000, - "y": 990720, - }, - Object { - "x": 1601390040000, - "y": 1318880, - }, - Object { - "x": 1601390070000, - "y": 958432, - }, - Object { - "x": 1601390100000, - "y": 1777600, - }, - Object { - "x": 1601390130000, - "y": 1873920, - }, - Object { - "x": 1601390160000, - "y": 72160, - }, - Object { - "x": 1601390190000, - "y": 1752064, - }, - Object { - "x": 1601390220000, - "y": 1136640, - }, - Object { - "x": 1601390250000, - "y": 1523712, - }, - Object { - "x": 1601390280000, - "y": 37888, - }, - Object { - "x": 1601390310000, - "y": 1196032, - }, - Object { - "x": 1601390340000, - "y": 1810304, - }, - Object { - "x": 1601390370000, - "y": 1007616, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 1523584, - }, - Object { - "x": 1601390460000, - "y": 1712096, - }, - Object { - "x": 1601390490000, - "y": 679936, - }, - Object { - "x": 1601390520000, - "y": 1163200, - }, - Object { - "x": 1601390550000, - "y": 1171392, - }, - Object { - "x": 1601390580000, - "y": 901120, - }, - Object { - "x": 1601390610000, - "y": 1355776, - }, - Object { - "x": 1601390640000, - "y": 1515520, - }, - Object { - "x": 1601390670000, - "y": 1097600, - }, - Object { - "x": 1601390700000, - "y": 1363968, - }, - Object { - "x": 1601390730000, - "y": 1290240, - }, - Object { - "x": 1601390760000, - "y": 663488, - }, - Object { - "x": 1601390790000, - "y": 827264, - }, - Object { - "x": 1601390820000, - "y": 1302400, - }, - Object { - "x": 1601390850000, - "y": 978912, - }, - Object { - "x": 1601390880000, - "y": 1482720, - }, - Object { - "x": 1601390910000, - "y": 1306624, - }, - Object { - "x": 1601390940000, - "y": 1179520, - }, - Object { - "x": 1601390970000, - "y": 1347584, - }, - Object { - "x": 1601391000000, - "y": 1122272, - }, - Object { - "x": 1601391030000, - "y": 1835008, - }, - Object { - "x": 1601391060000, - "y": 1572864, - }, - Object { - "x": 1601391090000, - "y": 1343232, - }, - Object { - "x": 1601391120000, - "y": 810880, - }, - Object { - "x": 1601391150000, - "y": 1122048, - }, - Object { - "x": 1601391180000, - "y": 782208, - }, - Object { - "x": 1601391210000, - "y": 1466368, - }, - Object { - "x": 1601391240000, - "y": 1490928, - }, - Object { - "x": 1601391270000, - "y": 1433472, - }, - Object { - "x": 1601391300000, - "y": 1677312, - }, - Object { - "x": 1601391330000, - "y": 1830912, - }, - Object { - "x": 1601391360000, - "y": 950144, - }, - Object { - "x": 1601391390000, - "y": 1265664, - }, - Object { - "x": 1601391420000, - "y": 1408896, - }, - Object { - "x": 1601391450000, - "y": 1178624, - }, - Object { - "x": 1601391480000, - "y": 946048, - }, - Object { - "x": 1601391510000, - "y": 1761248, - }, - Object { - "x": 1601391540000, - "y": 626688, - }, - Object { - "x": 1601391570000, - "y": 1564544, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - }, - "tpmBuckets": Array [ - Object { - "avg": 3.3, - "dataPoints": Array [ - Object { - "x": 1601389800000, - "y": 6, - }, - Object { - "x": 1601389830000, - "y": 4, - }, - Object { - "x": 1601389860000, - "y": 2, - }, - Object { - "x": 1601389890000, - "y": 0, - }, - Object { - "x": 1601389920000, - "y": 8, - }, - Object { - "x": 1601389950000, - "y": 2, - }, - Object { - "x": 1601389980000, - "y": 2, - }, - Object { - "x": 1601390010000, - "y": 0, - }, - Object { - "x": 1601390040000, - "y": 22, - }, - Object { - "x": 1601390070000, - "y": 8, - }, - Object { - "x": 1601390100000, - "y": 2, - }, - Object { - "x": 1601390130000, - "y": 0, - }, - Object { - "x": 1601390160000, - "y": 20, - }, - Object { - "x": 1601390190000, - "y": 2, - }, - Object { - "x": 1601390220000, - "y": 0, - }, - Object { - "x": 1601390250000, - "y": 0, - }, - Object { - "x": 1601390280000, - "y": 2, - }, - Object { - "x": 1601390310000, - "y": 0, - }, - Object { - "x": 1601390340000, - "y": 2, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": 0, - }, - Object { - "x": 1601390430000, - "y": 2, - }, - Object { - "x": 1601390460000, - "y": 8, - }, - Object { - "x": 1601390490000, - "y": 0, - }, - Object { - "x": 1601390520000, - "y": 6, - }, - Object { - "x": 1601390550000, - "y": 6, - }, - Object { - "x": 1601390580000, - "y": 0, - }, - Object { - "x": 1601390610000, - "y": 0, - }, - Object { - "x": 1601390640000, - "y": 0, - }, - Object { - "x": 1601390670000, - "y": 4, - }, - Object { - "x": 1601390700000, - "y": 0, - }, - Object { - "x": 1601390730000, - "y": 0, - }, - Object { - "x": 1601390760000, - "y": 4, - }, - Object { - "x": 1601390790000, - "y": 4, - }, - Object { - "x": 1601390820000, - "y": 6, - }, - Object { - "x": 1601390850000, - "y": 2, - }, - Object { - "x": 1601390880000, - "y": 12, - }, - Object { - "x": 1601390910000, - "y": 0, - }, - Object { - "x": 1601390940000, - "y": 6, - }, - Object { - "x": 1601390970000, - "y": 0, - }, - Object { - "x": 1601391000000, - "y": 10, - }, - Object { - "x": 1601391030000, - "y": 0, - }, - Object { - "x": 1601391060000, - "y": 0, - }, - Object { - "x": 1601391090000, - "y": 2, - }, - Object { - "x": 1601391120000, - "y": 8, - }, - Object { - "x": 1601391150000, - "y": 2, - }, - Object { - "x": 1601391180000, - "y": 4, - }, - Object { - "x": 1601391210000, - "y": 0, - }, - Object { - "x": 1601391240000, - "y": 6, - }, - Object { - "x": 1601391270000, - "y": 2, - }, - Object { - "x": 1601391300000, - "y": 0, - }, - Object { - "x": 1601391330000, - "y": 0, - }, - Object { - "x": 1601391360000, - "y": 2, - }, - Object { - "x": 1601391390000, - "y": 0, - }, - Object { - "x": 1601391420000, - "y": 2, - }, - Object { - "x": 1601391450000, - "y": 0, - }, - Object { - "x": 1601391480000, - "y": 4, - }, - Object { - "x": 1601391510000, - "y": 12, - }, - Object { - "x": 1601391540000, - "y": 0, - }, - Object { - "x": 1601391570000, - "y": 2, - }, - Object { - "x": 1601391600000, - "y": 0, - }, - ], - "key": "HTTP 2xx", - }, - Object { - "avg": 0.2, - "dataPoints": Array [ - Object { - "x": 1601389800000, - "y": 0, - }, - Object { - "x": 1601389830000, - "y": 0, - }, - Object { - "x": 1601389860000, - "y": 0, - }, - Object { - "x": 1601389890000, - "y": 0, - }, - Object { - "x": 1601389920000, - "y": 0, - }, - Object { - "x": 1601389950000, - "y": 2, - }, - Object { - "x": 1601389980000, - "y": 0, - }, - Object { - "x": 1601390010000, - "y": 0, - }, - Object { - "x": 1601390040000, - "y": 2, - }, - Object { - "x": 1601390070000, - "y": 0, - }, - Object { - "x": 1601390100000, - "y": 0, - }, - Object { - "x": 1601390130000, - "y": 0, - }, - Object { - "x": 1601390160000, - "y": 4, - }, - Object { - "x": 1601390190000, - "y": 0, - }, - Object { - "x": 1601390220000, - "y": 0, - }, - Object { - "x": 1601390250000, - "y": 0, - }, - Object { - "x": 1601390280000, - "y": 0, - }, - Object { - "x": 1601390310000, - "y": 0, - }, - Object { - "x": 1601390340000, - "y": 0, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": 0, - }, - Object { - "x": 1601390430000, - "y": 0, - }, - Object { - "x": 1601390460000, - "y": 0, - }, - Object { - "x": 1601390490000, - "y": 0, - }, - Object { - "x": 1601390520000, - "y": 0, - }, - Object { - "x": 1601390550000, - "y": 0, - }, - Object { - "x": 1601390580000, - "y": 0, - }, - Object { - "x": 1601390610000, - "y": 0, - }, - Object { - "x": 1601390640000, - "y": 0, - }, - Object { - "x": 1601390670000, - "y": 2, - }, - Object { - "x": 1601390700000, - "y": 0, - }, - Object { - "x": 1601390730000, - "y": 0, - }, - Object { - "x": 1601390760000, - "y": 2, - }, - Object { - "x": 1601390790000, - "y": 0, - }, - Object { - "x": 1601390820000, - "y": 0, - }, - Object { - "x": 1601390850000, - "y": 0, - }, - Object { - "x": 1601390880000, - "y": 0, - }, - Object { - "x": 1601390910000, - "y": 0, - }, - Object { - "x": 1601390940000, - "y": 0, - }, - Object { - "x": 1601390970000, - "y": 0, - }, - Object { - "x": 1601391000000, - "y": 0, - }, - Object { - "x": 1601391030000, - "y": 0, - }, - Object { - "x": 1601391060000, - "y": 0, - }, - Object { - "x": 1601391090000, - "y": 0, - }, - Object { - "x": 1601391120000, - "y": 0, - }, - Object { - "x": 1601391150000, - "y": 0, - }, - Object { - "x": 1601391180000, - "y": 0, - }, - Object { - "x": 1601391210000, - "y": 0, - }, - Object { - "x": 1601391240000, - "y": 0, - }, - Object { - "x": 1601391270000, - "y": 0, - }, - Object { - "x": 1601391300000, - "y": 0, - }, - Object { - "x": 1601391330000, - "y": 0, - }, - Object { - "x": 1601391360000, - "y": 0, - }, - Object { - "x": 1601391390000, - "y": 0, - }, - Object { - "x": 1601391420000, - "y": 0, - }, - Object { - "x": 1601391450000, - "y": 0, - }, - Object { - "x": 1601391480000, - "y": 0, - }, - Object { - "x": 1601391510000, - "y": 0, - }, - Object { - "x": 1601391540000, - "y": 0, - }, - Object { - "x": 1601391570000, - "y": 0, - }, - Object { - "x": 1601391600000, - "y": 0, - }, - ], - "key": "HTTP 4xx", - }, - Object { - "avg": 4.26666666666667, - "dataPoints": Array [ - Object { - "x": 1601389800000, - "y": 8, - }, - Object { - "x": 1601389830000, - "y": 6, - }, - Object { - "x": 1601389860000, - "y": 4, - }, - Object { - "x": 1601389890000, - "y": 4, - }, - Object { - "x": 1601389920000, - "y": 2, - }, - Object { - "x": 1601389950000, - "y": 8, - }, - Object { - "x": 1601389980000, - "y": 0, - }, - Object { - "x": 1601390010000, - "y": 6, - }, - Object { - "x": 1601390040000, - "y": 6, - }, - Object { - "x": 1601390070000, - "y": 2, - }, - Object { - "x": 1601390100000, - "y": 4, - }, - Object { - "x": 1601390130000, - "y": 6, - }, - Object { - "x": 1601390160000, - "y": 0, - }, - Object { - "x": 1601390190000, - "y": 6, - }, - Object { - "x": 1601390220000, - "y": 4, - }, - Object { - "x": 1601390250000, - "y": 6, - }, - Object { - "x": 1601390280000, - "y": 0, - }, - Object { - "x": 1601390310000, - "y": 6, - }, - Object { - "x": 1601390340000, - "y": 6, - }, - Object { - "x": 1601390370000, - "y": 4, - }, - Object { - "x": 1601390400000, - "y": 0, - }, - Object { - "x": 1601390430000, - "y": 6, - }, - Object { - "x": 1601390460000, - "y": 2, - }, - Object { - "x": 1601390490000, - "y": 6, - }, - Object { - "x": 1601390520000, - "y": 2, - }, - Object { - "x": 1601390550000, - "y": 4, - }, - Object { - "x": 1601390580000, - "y": 4, - }, - Object { - "x": 1601390610000, - "y": 4, - }, - Object { - "x": 1601390640000, - "y": 2, - }, - Object { - "x": 1601390670000, - "y": 4, - }, - Object { - "x": 1601390700000, - "y": 4, - }, - Object { - "x": 1601390730000, - "y": 6, - }, - Object { - "x": 1601390760000, - "y": 2, - }, - Object { - "x": 1601390790000, - "y": 4, - }, - Object { - "x": 1601390820000, - "y": 6, - }, - Object { - "x": 1601390850000, - "y": 4, - }, - Object { - "x": 1601390880000, - "y": 4, - }, - Object { - "x": 1601390910000, - "y": 8, - }, - Object { - "x": 1601390940000, - "y": 2, - }, - Object { - "x": 1601390970000, - "y": 6, - }, - Object { - "x": 1601391000000, - "y": 2, - }, - Object { - "x": 1601391030000, - "y": 6, - }, - Object { - "x": 1601391060000, - "y": 4, - }, - Object { - "x": 1601391090000, - "y": 6, - }, - Object { - "x": 1601391120000, - "y": 4, - }, - Object { - "x": 1601391150000, - "y": 4, - }, - Object { - "x": 1601391180000, - "y": 2, - }, - Object { - "x": 1601391210000, - "y": 4, - }, - Object { - "x": 1601391240000, - "y": 4, - }, - Object { - "x": 1601391270000, - "y": 4, - }, - Object { - "x": 1601391300000, - "y": 6, - }, - Object { - "x": 1601391330000, - "y": 4, - }, - Object { - "x": 1601391360000, - "y": 4, - }, - Object { - "x": 1601391390000, - "y": 6, - }, - Object { - "x": 1601391420000, - "y": 6, - }, - Object { - "x": 1601391450000, - "y": 4, - }, - Object { - "x": 1601391480000, - "y": 2, - }, - Object { - "x": 1601391510000, - "y": 6, - }, - Object { - "x": 1601391540000, - "y": 2, - }, - Object { - "x": 1601391570000, - "y": 8, - }, - Object { - "x": 1601391600000, - "y": 0, - }, - ], - "key": "success", - }, - ], - }, -} -`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap deleted file mode 100644 index 4bf242d8f9b6d..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap +++ /dev/null @@ -1,824 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UX page load dist when there is data returns page load distribution 1`] = ` -Object { - "maxDuration": 54.46, - "minDuration": 0, - "pageLoadDistribution": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 16.6666666666667, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 50, - }, - Object { - "x": 5.5, - "y": 0, - }, - Object { - "x": 6, - "y": 0, - }, - Object { - "x": 6.5, - "y": 0, - }, - Object { - "x": 7, - "y": 0, - }, - Object { - "x": 7.5, - "y": 0, - }, - Object { - "x": 8, - "y": 0, - }, - Object { - "x": 8.5, - "y": 0, - }, - Object { - "x": 9, - "y": 0, - }, - Object { - "x": 9.5, - "y": 0, - }, - Object { - "x": 10, - "y": 0, - }, - Object { - "x": 10.5, - "y": 0, - }, - Object { - "x": 11, - "y": 0, - }, - Object { - "x": 11.5, - "y": 0, - }, - Object { - "x": 12, - "y": 0, - }, - Object { - "x": 12.5, - "y": 0, - }, - Object { - "x": 13, - "y": 0, - }, - Object { - "x": 13.5, - "y": 0, - }, - Object { - "x": 14, - "y": 0, - }, - Object { - "x": 14.5, - "y": 0, - }, - Object { - "x": 15, - "y": 0, - }, - Object { - "x": 15.5, - "y": 0, - }, - Object { - "x": 16, - "y": 0, - }, - Object { - "x": 16.5, - "y": 0, - }, - Object { - "x": 17, - "y": 0, - }, - Object { - "x": 17.5, - "y": 0, - }, - Object { - "x": 18, - "y": 0, - }, - Object { - "x": 18.5, - "y": 0, - }, - Object { - "x": 19, - "y": 0, - }, - Object { - "x": 19.5, - "y": 0, - }, - Object { - "x": 20, - "y": 0, - }, - Object { - "x": 20.5, - "y": 0, - }, - Object { - "x": 21, - "y": 0, - }, - Object { - "x": 21.5, - "y": 0, - }, - Object { - "x": 22, - "y": 0, - }, - Object { - "x": 22.5, - "y": 0, - }, - Object { - "x": 23, - "y": 0, - }, - Object { - "x": 23.5, - "y": 0, - }, - Object { - "x": 24, - "y": 0, - }, - Object { - "x": 24.5, - "y": 0, - }, - Object { - "x": 25, - "y": 0, - }, - Object { - "x": 25.5, - "y": 0, - }, - Object { - "x": 26, - "y": 0, - }, - Object { - "x": 26.5, - "y": 0, - }, - Object { - "x": 27, - "y": 0, - }, - Object { - "x": 27.5, - "y": 0, - }, - Object { - "x": 28, - "y": 0, - }, - Object { - "x": 28.5, - "y": 0, - }, - Object { - "x": 29, - "y": 0, - }, - Object { - "x": 29.5, - "y": 0, - }, - Object { - "x": 30, - "y": 0, - }, - Object { - "x": 30.5, - "y": 0, - }, - Object { - "x": 31, - "y": 0, - }, - Object { - "x": 31.5, - "y": 0, - }, - Object { - "x": 32, - "y": 0, - }, - Object { - "x": 32.5, - "y": 0, - }, - Object { - "x": 33, - "y": 0, - }, - Object { - "x": 33.5, - "y": 0, - }, - Object { - "x": 34, - "y": 0, - }, - Object { - "x": 34.5, - "y": 0, - }, - Object { - "x": 35, - "y": 0, - }, - Object { - "x": 35.5, - "y": 0, - }, - Object { - "x": 36, - "y": 0, - }, - Object { - "x": 36.5, - "y": 0, - }, - Object { - "x": 37, - "y": 0, - }, - Object { - "x": 37.5, - "y": 16.6666666666667, - }, - Object { - "x": 38, - "y": 0, - }, - Object { - "x": 38.5, - "y": 0, - }, - Object { - "x": 39, - "y": 0, - }, - Object { - "x": 39.5, - "y": 0, - }, - Object { - "x": 40, - "y": 0, - }, - Object { - "x": 40.5, - "y": 0, - }, - Object { - "x": 41, - "y": 0, - }, - Object { - "x": 41.5, - "y": 0, - }, - Object { - "x": 42, - "y": 0, - }, - Object { - "x": 42.5, - "y": 0, - }, - Object { - "x": 43, - "y": 0, - }, - Object { - "x": 43.5, - "y": 0, - }, - Object { - "x": 44, - "y": 0, - }, - Object { - "x": 44.5, - "y": 0, - }, - Object { - "x": 45, - "y": 0, - }, - Object { - "x": 45.5, - "y": 0, - }, - Object { - "x": 46, - "y": 0, - }, - Object { - "x": 46.5, - "y": 0, - }, - Object { - "x": 47, - "y": 0, - }, - Object { - "x": 47.5, - "y": 0, - }, - Object { - "x": 48, - "y": 0, - }, - Object { - "x": 48.5, - "y": 0, - }, - Object { - "x": 49, - "y": 0, - }, - Object { - "x": 49.5, - "y": 0, - }, - Object { - "x": 50, - "y": 0, - }, - Object { - "x": 50.5, - "y": 0, - }, - Object { - "x": 51, - "y": 0, - }, - Object { - "x": 51.5, - "y": 0, - }, - Object { - "x": 52, - "y": 0, - }, - Object { - "x": 52.5, - "y": 0, - }, - Object { - "x": 53, - "y": 0, - }, - Object { - "x": 53.5, - "y": 0, - }, - Object { - "x": 54, - "y": 0, - }, - Object { - "x": 54.5, - "y": 16.6666666666667, - }, - ], - "percentiles": Object { - "50.0": 4.88, - "75.0": 37.09, - "90.0": 37.09, - "95.0": 54.46, - "99.0": 54.46, - }, -} -`; - -exports[`UX page load dist when there is data returns page load distribution with breakdown 1`] = ` -Array [ - Object { - "data": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 25, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 25, - }, - Object { - "x": 5.5, - "y": 0, - }, - Object { - "x": 6, - "y": 0, - }, - Object { - "x": 6.5, - "y": 0, - }, - Object { - "x": 7, - "y": 0, - }, - Object { - "x": 7.5, - "y": 0, - }, - Object { - "x": 8, - "y": 0, - }, - Object { - "x": 8.5, - "y": 0, - }, - Object { - "x": 9, - "y": 0, - }, - Object { - "x": 9.5, - "y": 0, - }, - Object { - "x": 10, - "y": 0, - }, - Object { - "x": 10.5, - "y": 0, - }, - Object { - "x": 11, - "y": 0, - }, - Object { - "x": 11.5, - "y": 0, - }, - Object { - "x": 12, - "y": 0, - }, - Object { - "x": 12.5, - "y": 0, - }, - Object { - "x": 13, - "y": 0, - }, - Object { - "x": 13.5, - "y": 0, - }, - Object { - "x": 14, - "y": 0, - }, - Object { - "x": 14.5, - "y": 0, - }, - Object { - "x": 15, - "y": 0, - }, - Object { - "x": 15.5, - "y": 0, - }, - Object { - "x": 16, - "y": 0, - }, - Object { - "x": 16.5, - "y": 0, - }, - Object { - "x": 17, - "y": 0, - }, - Object { - "x": 17.5, - "y": 0, - }, - Object { - "x": 18, - "y": 0, - }, - Object { - "x": 18.5, - "y": 0, - }, - Object { - "x": 19, - "y": 0, - }, - Object { - "x": 19.5, - "y": 0, - }, - Object { - "x": 20, - "y": 0, - }, - Object { - "x": 20.5, - "y": 0, - }, - Object { - "x": 21, - "y": 0, - }, - Object { - "x": 21.5, - "y": 0, - }, - Object { - "x": 22, - "y": 0, - }, - Object { - "x": 22.5, - "y": 0, - }, - Object { - "x": 23, - "y": 0, - }, - Object { - "x": 23.5, - "y": 0, - }, - Object { - "x": 24, - "y": 0, - }, - Object { - "x": 24.5, - "y": 0, - }, - Object { - "x": 25, - "y": 0, - }, - Object { - "x": 25.5, - "y": 0, - }, - Object { - "x": 26, - "y": 0, - }, - Object { - "x": 26.5, - "y": 0, - }, - Object { - "x": 27, - "y": 0, - }, - Object { - "x": 27.5, - "y": 0, - }, - Object { - "x": 28, - "y": 0, - }, - Object { - "x": 28.5, - "y": 0, - }, - Object { - "x": 29, - "y": 0, - }, - Object { - "x": 29.5, - "y": 0, - }, - Object { - "x": 30, - "y": 0, - }, - Object { - "x": 30.5, - "y": 0, - }, - Object { - "x": 31, - "y": 0, - }, - Object { - "x": 31.5, - "y": 0, - }, - Object { - "x": 32, - "y": 0, - }, - Object { - "x": 32.5, - "y": 0, - }, - Object { - "x": 33, - "y": 0, - }, - Object { - "x": 33.5, - "y": 0, - }, - Object { - "x": 34, - "y": 0, - }, - Object { - "x": 34.5, - "y": 0, - }, - Object { - "x": 35, - "y": 0, - }, - Object { - "x": 35.5, - "y": 0, - }, - Object { - "x": 36, - "y": 0, - }, - Object { - "x": 36.5, - "y": 0, - }, - Object { - "x": 37, - "y": 0, - }, - Object { - "x": 37.5, - "y": 25, - }, - ], - "name": "Chrome", - }, - Object { - "data": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 0, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 100, - }, - ], - "name": "Chrome Mobile", - }, -] -`; - -exports[`UX page load dist when there is no data returns empty list 1`] = `Object {}`; - -exports[`UX page load dist when there is no data returns empty list with breakdowns 1`] = `Object {}`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap deleted file mode 100644 index 38b009fc73d34..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap +++ /dev/null @@ -1,280 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CSM page views when there is data returns page views 1`] = ` -Object { - "items": Array [ - Object { - "x": 1600149947000, - "y": 1, - }, - Object { - "x": 1600149957000, - "y": 0, - }, - Object { - "x": 1600149967000, - "y": 0, - }, - Object { - "x": 1600149977000, - "y": 0, - }, - Object { - "x": 1600149987000, - "y": 0, - }, - Object { - "x": 1600149997000, - "y": 0, - }, - Object { - "x": 1600150007000, - "y": 0, - }, - Object { - "x": 1600150017000, - "y": 0, - }, - Object { - "x": 1600150027000, - "y": 1, - }, - Object { - "x": 1600150037000, - "y": 0, - }, - Object { - "x": 1600150047000, - "y": 0, - }, - Object { - "x": 1600150057000, - "y": 0, - }, - Object { - "x": 1600150067000, - "y": 0, - }, - Object { - "x": 1600150077000, - "y": 1, - }, - Object { - "x": 1600150087000, - "y": 0, - }, - Object { - "x": 1600150097000, - "y": 0, - }, - Object { - "x": 1600150107000, - "y": 0, - }, - Object { - "x": 1600150117000, - "y": 0, - }, - Object { - "x": 1600150127000, - "y": 0, - }, - Object { - "x": 1600150137000, - "y": 0, - }, - Object { - "x": 1600150147000, - "y": 0, - }, - Object { - "x": 1600150157000, - "y": 0, - }, - Object { - "x": 1600150167000, - "y": 0, - }, - Object { - "x": 1600150177000, - "y": 1, - }, - Object { - "x": 1600150187000, - "y": 0, - }, - Object { - "x": 1600150197000, - "y": 0, - }, - Object { - "x": 1600150207000, - "y": 1, - }, - Object { - "x": 1600150217000, - "y": 0, - }, - Object { - "x": 1600150227000, - "y": 0, - }, - Object { - "x": 1600150237000, - "y": 1, - }, - ], - "topItems": Array [], -} -`; - -exports[`CSM page views when there is data returns page views with breakdown 1`] = ` -Object { - "items": Array [ - Object { - "Chrome": 1, - "x": 1600149947000, - "y": 1, - }, - Object { - "x": 1600149957000, - "y": 0, - }, - Object { - "x": 1600149967000, - "y": 0, - }, - Object { - "x": 1600149977000, - "y": 0, - }, - Object { - "x": 1600149987000, - "y": 0, - }, - Object { - "x": 1600149997000, - "y": 0, - }, - Object { - "x": 1600150007000, - "y": 0, - }, - Object { - "x": 1600150017000, - "y": 0, - }, - Object { - "Chrome": 1, - "x": 1600150027000, - "y": 1, - }, - Object { - "x": 1600150037000, - "y": 0, - }, - Object { - "x": 1600150047000, - "y": 0, - }, - Object { - "x": 1600150057000, - "y": 0, - }, - Object { - "x": 1600150067000, - "y": 0, - }, - Object { - "Chrome": 1, - "x": 1600150077000, - "y": 1, - }, - Object { - "x": 1600150087000, - "y": 0, - }, - Object { - "x": 1600150097000, - "y": 0, - }, - Object { - "x": 1600150107000, - "y": 0, - }, - Object { - "x": 1600150117000, - "y": 0, - }, - Object { - "x": 1600150127000, - "y": 0, - }, - Object { - "x": 1600150137000, - "y": 0, - }, - Object { - "x": 1600150147000, - "y": 0, - }, - Object { - "x": 1600150157000, - "y": 0, - }, - Object { - "x": 1600150167000, - "y": 0, - }, - Object { - "Chrome": 1, - "x": 1600150177000, - "y": 1, - }, - Object { - "x": 1600150187000, - "y": 0, - }, - Object { - "x": 1600150197000, - "y": 0, - }, - Object { - "Chrome Mobile": 1, - "x": 1600150207000, - "y": 1, - }, - Object { - "x": 1600150217000, - "y": 0, - }, - Object { - "x": 1600150227000, - "y": 0, - }, - Object { - "Chrome Mobile": 1, - "x": 1600150237000, - "y": 1, - }, - ], - "topItems": Array [ - "Chrome", - "Chrome Mobile", - ], -} -`; - -exports[`CSM page views when there is no data returns empty list 1`] = ` -Object { - "items": Array [], - "topItems": Array [], -} -`; - -exports[`CSM page views when there is no data returns empty list with breakdowns 1`] = ` -Object { - "items": Array [], - "topItems": Array [], -} -`; diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap deleted file mode 100644 index a7e6ae03b1bdc..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ /dev/null @@ -1,1995 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Service Maps with a trial license /api/apm/service-map when there is data returns service map elements filtering by environment not defined 1`] = ` -Object { - "elements": Array [ - Object { - "data": Object { - "agent.name": "rum-js", - "id": "elastic-co-frontend", - "service.environment": "ENVIRONMENT_NOT_DEFINED", - "service.name": "elastic-co-frontend", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "groupedConnections": Array [ - Object { - "id": ">a18132920325.cdn.optimizely.com:443", - "label": "a18132920325.cdn.optimizely.com:443", - "span.destination.service.resource": "a18132920325.cdn.optimizely.com:443", - "span.subtype": "iframe", - "span.type": "resource", - }, - Object { - "id": ">cdn.optimizely.com:443", - "label": "cdn.optimizely.com:443", - "span.destination.service.resource": "cdn.optimizely.com:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">fonts.googleapis.com:443", - "label": "fonts.googleapis.com:443", - "span.destination.service.resource": "fonts.googleapis.com:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">images.contentstack.io:443", - "label": "images.contentstack.io:443", - "span.destination.service.resource": "images.contentstack.io:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">info.elastic.co:443", - "label": "info.elastic.co:443", - "span.destination.service.resource": "info.elastic.co:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">p.typekit.net:443", - "label": "p.typekit.net:443", - "span.destination.service.resource": "p.typekit.net:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">static-www.elastic.co:443", - "label": "static-www.elastic.co:443", - "span.destination.service.resource": "static-www.elastic.co:443", - "span.subtype": "img", - "span.type": "resource", - }, - Object { - "id": ">use.typekit.net:443", - "label": "use.typekit.net:443", - "span.destination.service.resource": "use.typekit.net:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">www.elastic.co:443", - "label": "www.elastic.co:443", - "span.destination.service.resource": "www.elastic.co:443", - "span.subtype": "browser-timing", - "span.type": "hard-navigation", - }, - ], - "id": "resourceGroup{elastic-co-frontend}", - "label": "9 resources", - "span.type": "external", - }, - }, - Object { - "data": Object { - "id": "elastic-co-frontend~>resourceGroup{elastic-co-frontend}", - "source": "elastic-co-frontend", - "target": "resourceGroup{elastic-co-frontend}", - }, - }, - ], -} -`; - -exports[`Service Maps with a trial license /api/apm/service-map when there is data returns the correct data 3`] = ` -Array [ - Object { - "data": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "elastic-co-frontend", - "service.name": "elastic-co-frontend", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - Object { - "data": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - Object { - "data": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "id": ">elasticsearch", - "label": "elasticsearch", - "span.destination.service.resource": "elasticsearch", - "span.subtype": "elasticsearch", - "span.type": "db", - }, - }, - Object { - "data": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "db", - }, - }, - Object { - "data": Object { - "agent.name": "dotnet", - "id": "opbeans-dotnet", - "service.environment": null, - "service.name": "opbeans-dotnet", - }, - }, - Object { - "data": Object { - "groupedConnections": Array [ - Object { - "id": ">a18132920325.cdn.optimizely.com:443", - "label": "a18132920325.cdn.optimizely.com:443", - "span.destination.service.resource": "a18132920325.cdn.optimizely.com:443", - "span.subtype": "iframe", - "span.type": "resource", - }, - Object { - "id": ">cdn.optimizely.com:443", - "label": "cdn.optimizely.com:443", - "span.destination.service.resource": "cdn.optimizely.com:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">fonts.googleapis.com:443", - "label": "fonts.googleapis.com:443", - "span.destination.service.resource": "fonts.googleapis.com:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">images.contentstack.io:443", - "label": "images.contentstack.io:443", - "span.destination.service.resource": "images.contentstack.io:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">info.elastic.co:443", - "label": "info.elastic.co:443", - "span.destination.service.resource": "info.elastic.co:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">p.typekit.net:443", - "label": "p.typekit.net:443", - "span.destination.service.resource": "p.typekit.net:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">static-www.elastic.co:443", - "label": "static-www.elastic.co:443", - "span.destination.service.resource": "static-www.elastic.co:443", - "span.subtype": "img", - "span.type": "resource", - }, - Object { - "id": ">use.typekit.net:443", - "label": "use.typekit.net:443", - "span.destination.service.resource": "use.typekit.net:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">www.elastic.co:443", - "label": "www.elastic.co:443", - "span.destination.service.resource": "www.elastic.co:443", - "span.subtype": "browser-timing", - "span.type": "hard-navigation", - }, - ], - "id": "resourceGroup{elastic-co-frontend}", - "label": "9 resources", - "span.type": "external", - }, - }, - Object { - "data": Object { - "id": "opbeans-go~>postgresql", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-go~opbeans-node", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-go~opbeans-python", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-java~>postgresql", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-node", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-ruby", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~>postgresql", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-go", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-node~opbeans-python", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-node~opbeans-ruby", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>elasticsearch", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">elasticsearch", - "targetData": Object { - "id": ">elasticsearch", - "label": "elasticsearch", - "span.destination.service.resource": "elasticsearch", - "span.subtype": "elasticsearch", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>postgresql", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>redis", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">redis", - "targetData": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~opbeans-go", - "isInverseEdge": true, - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~opbeans-node", - "isInverseEdge": true, - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-python~opbeans-ruby", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~>postgresql", - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-go", - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-node", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-python", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-go", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-java", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-node", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-python", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-ruby", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "elastic-co-frontend~>resourceGroup{elastic-co-frontend}", - "source": "elastic-co-frontend", - "target": "resourceGroup{elastic-co-frontend}", - }, - }, -] -`; - -exports[`Service Maps with a trial license when there is data with anomalies with the default apm user returns the correct anomaly stats 3`] = ` -Object { - "elements": Array [ - Object { - "data": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "elastic-co-frontend", - "service.name": "elastic-co-frontend", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - Object { - "data": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - Object { - "data": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "id": ">elasticsearch", - "label": "elasticsearch", - "span.destination.service.resource": "elasticsearch", - "span.subtype": "elasticsearch", - "span.type": "db", - }, - }, - Object { - "data": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "db", - }, - }, - Object { - "data": Object { - "agent.name": "dotnet", - "id": "opbeans-dotnet", - "service.environment": null, - "service.name": "opbeans-dotnet", - }, - }, - Object { - "data": Object { - "groupedConnections": Array [ - Object { - "id": ">a18132920325.cdn.optimizely.com:443", - "label": "a18132920325.cdn.optimizely.com:443", - "span.destination.service.resource": "a18132920325.cdn.optimizely.com:443", - "span.subtype": "iframe", - "span.type": "resource", - }, - Object { - "id": ">cdn.optimizely.com:443", - "label": "cdn.optimizely.com:443", - "span.destination.service.resource": "cdn.optimizely.com:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">fonts.googleapis.com:443", - "label": "fonts.googleapis.com:443", - "span.destination.service.resource": "fonts.googleapis.com:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">images.contentstack.io:443", - "label": "images.contentstack.io:443", - "span.destination.service.resource": "images.contentstack.io:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">info.elastic.co:443", - "label": "info.elastic.co:443", - "span.destination.service.resource": "info.elastic.co:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">p.typekit.net:443", - "label": "p.typekit.net:443", - "span.destination.service.resource": "p.typekit.net:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">static-www.elastic.co:443", - "label": "static-www.elastic.co:443", - "span.destination.service.resource": "static-www.elastic.co:443", - "span.subtype": "img", - "span.type": "resource", - }, - Object { - "id": ">use.typekit.net:443", - "label": "use.typekit.net:443", - "span.destination.service.resource": "use.typekit.net:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">www.elastic.co:443", - "label": "www.elastic.co:443", - "span.destination.service.resource": "www.elastic.co:443", - "span.subtype": "browser-timing", - "span.type": "hard-navigation", - }, - ], - "id": "resourceGroup{elastic-co-frontend}", - "label": "9 resources", - "span.type": "external", - }, - }, - Object { - "data": Object { - "id": "opbeans-go~>postgresql", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-go~opbeans-node", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-go~opbeans-python", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-java~>postgresql", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-node", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-ruby", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~>postgresql", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-go", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-node~opbeans-python", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-node~opbeans-ruby", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>elasticsearch", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">elasticsearch", - "targetData": Object { - "id": ">elasticsearch", - "label": "elasticsearch", - "span.destination.service.resource": "elasticsearch", - "span.subtype": "elasticsearch", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>postgresql", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>redis", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">redis", - "targetData": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~opbeans-go", - "isInverseEdge": true, - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~opbeans-node", - "isInverseEdge": true, - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-python~opbeans-ruby", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~>postgresql", - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-go", - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-node", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-python", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-go", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-java", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-node", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-python", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-ruby", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "elastic-co-frontend~>resourceGroup{elastic-co-frontend}", - "source": "elastic-co-frontend", - "target": "resourceGroup{elastic-co-frontend}", - }, - }, - ], -} -`; diff --git a/x-pack/test/apm_api_integration/trial/tests/services/__snapshots__/transaction_groups_charts.snap b/x-pack/test/apm_api_integration/trial/tests/services/__snapshots__/transaction_groups_charts.snap deleted file mode 100644 index 8169e73202fbc..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/services/__snapshots__/transaction_groups_charts.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`APM Transaction Overview when data is loaded and fetching transaction groups charts with uiFilters when not defined environments selected should return the correct anomaly boundaries 1`] = `Array []`; - -exports[`APM Transaction Overview when data is loaded and fetching transaction groups charts with uiFilters with environment selected and empty kuery filter should return a non-empty anomaly series 1`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 1206111.33487531, - "y0": 10555.1290143587, - }, - Object { - "x": 1601390700000, - "y": 1223987.49321778, - "y0": 10177.4677901726, - }, - Object { - "x": 1601391600000, - "y": 1223987.49321778, - "y0": 10177.4677901726, - }, -] -`; - -exports[`APM Transaction Overview when data is loaded and fetching transaction groups charts with uiFilters with environment selected in uiFilters should return a non-empty anomaly series 1`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 1206111.33487531, - "y0": 10555.1290143587, - }, - Object { - "x": 1601390700000, - "y": 1223987.49321778, - "y0": 10177.4677901726, - }, - Object { - "x": 1601391600000, - "y": 1223987.49321778, - "y0": 10177.4677901726, - }, -] -`; diff --git a/x-pack/test/fleet_api_integration/apis/epm/file.ts b/x-pack/test/fleet_api_integration/apis/epm/file.ts index ab89fceeb5b49..2823b236c0321 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/file.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/file.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import fs from 'fs'; +import path from 'path'; +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { warnAndSkipTest } from '../../helpers'; @@ -14,79 +17,175 @@ export default function ({ getService }: FtrProviderContext) { const server = dockerServers.get('registry'); describe('EPM - package file', () => { - it('fetches a .png screenshot image', async function () { - if (server.enabled) { - await supertest - .get('/api/fleet/epm/packages/filetest/0.1.0/img/screenshots/metricbeat_dashboard.png') - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'image/png') - .expect(200); - } else { - warnAndSkipTest(this, log); - } - }); + describe('it gets files from registry', () => { + it('fetches a .png screenshot image', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/filetest/0.1.0/img/screenshots/metricbeat_dashboard.png') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/png') + .expect(200); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); - it('fetches an .svg icon image', async function () { - if (server.enabled) { - await supertest - .get('/api/fleet/epm/packages/filetest/0.1.0/img/logo.svg') - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'image/svg+xml') - .expect(200); - } else { - warnAndSkipTest(this, log); - } - }); + it('fetches an .svg icon image', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/filetest/0.1.0/img/logo.svg') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/svg+xml') + .expect(200); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); - it('fetches a .json kibana visualization file', async function () { - if (server.enabled) { - await supertest - .get( - '/api/fleet/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' - ) - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(200); - } else { - warnAndSkipTest(this, log); - } - }); + it('fetches a .json kibana visualization file', async function () { + if (server.enabled) { + const res = await supertest + .get( + '/api/fleet/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' + ) + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + expect(typeof res.body).to.equal('object'); + } else { + warnAndSkipTest(this, log); + } + }); - it('fetches a .json kibana dashboard file', async function () { - if (server.enabled) { - await supertest - .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json') - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(200); - } else { - warnAndSkipTest(this, log); - } - }); + it('fetches a .json kibana dashboard file', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + expect(typeof res.body).to.equal('object'); + } else { + warnAndSkipTest(this, log); + } + }); - it('fetches a .json search file', async function () { - if (server.enabled) { + it('fetches a .json search file', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + expect(typeof res.body).to.equal('object'); + } else { + warnAndSkipTest(this, log); + } + }); + }); + describe('it gets files from an uploaded package', () => { + before(async () => { + if (!server.enabled) return; + const testPkgArchiveTgz = path.join( + path.dirname(__filename), + '../fixtures/direct_upload_packages/apache_0.1.4.tar.gz' + ); + const buf = fs.readFileSync(testPkgArchiveTgz); await supertest - .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') + .post(`/api/fleet/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/gzip') + .send(buf) .expect(200); - } else { - warnAndSkipTest(this, log); - } + }); + after(async () => { + if (!server.enabled) return; + await supertest.delete(`/api/fleet/epm/packages/apache-0.1.4`).set('kbn-xsrf', 'xxxx'); + }); + it('fetches a .png screenshot image', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/apache/0.1.4/img/kibana-apache-test.png') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/png') + .expect(200); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); + it('fetches the logo', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/apache/0.1.4/img/logo_apache_test.svg') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/svg+xml') + .expect(200); + await supertest + .get('/api/fleet/epm/packages/apache/0.1.4/img/logo_apache.svg') + .set('kbn-xsrf', 'xxx') + .expect(404); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); + + it('fetches a .json kibana dashboard file', async function () { + if (server.enabled) { + const res = await supertest + .get( + '/api/fleet/epm/packages/apache/0.1.4/kibana/dashboard/apache-Logs-Apache-Dashboard-ecs-new.json' + ) + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + expect(typeof res.body).to.equal('object'); + } else { + warnAndSkipTest(this, log); + } + }); + + it('fetches a README file', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/apache/0.1.4/docs/README.md') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'text/markdown; charset=utf-8') + .expect(200); + expect(res.text).to.equal('# Apache Uploaded Test Integration'); + } else { + warnAndSkipTest(this, log); + } + }); + + it('fetches the logo of a not uploaded (and installed) version from the registry when another version is uploaded (and installed)', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/apache/0.1.3/img/logo_apache.svg') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/svg+xml') + .expect(200); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); }); - }); - // Disabled for now as we don't serve prebuilt index patterns in current packages. - // it('fetches an .json index pattern file', async function () { - // if (server.enabled) { - // await supertest - // .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/index-pattern/sample-*.json') - // .set('kbn-xsrf', 'xxx') - // .expect('Content-Type', 'application/json; charset=utf-8') - // .expect(200); - // } else { - // warnAndSkipTest(this, log); - // } - // }); + // Disabled for now as we don't serve prebuilt index patterns in current packages. + // it('fetches an .json index pattern file', async function () { + // if (server.enabled) { + // await supertest + // .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/index-pattern/sample-*.json') + // .set('kbn-xsrf', 'xxx') + // .expect('Content-Type', 'application/json; charset=utf-8') + // .expect(200); + // } else { + // warnAndSkipTest(this, log); + // } + // }); + }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/get.ts b/x-pack/test/fleet_api_integration/apis/epm/get.ts index 53982affa128c..a6be50804aa5e 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/get.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/get.ts @@ -71,6 +71,25 @@ export default function (providerContext: FtrProviderContext) { warnAndSkipTest(this, log); } }); + it('returns correct package info from registry if a different version is installed by upload', async function () { + if (server.enabled) { + const buf = fs.readFileSync(testPkgArchiveZip); + await supertest + .post(`/api/fleet/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/zip') + .send(buf) + .expect(200); + + const res = await supertest.get(`/api/fleet/epm/packages/apache-0.1.3`).expect(200); + const packageInfo = res.body.response; + expect(packageInfo.description).to.equal('Apache Integration'); + expect(packageInfo.download).to.not.equal(undefined); + await uninstallPackage(testPkgKey); + } else { + warnAndSkipTest(this, log); + } + }); it('returns a 500 for a package key without a proper name', async function () { if (server.enabled) { await supertest.get('/api/fleet/epm/packages/-0.1.0').expect(500); diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz index c5d3607e05cb8..4dbd2f223d506 100644 Binary files a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz differ diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip index 6a8a12b2f2d49..a1c396370a937 100644 Binary files a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip differ diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts index b2eb645c8372c..b4cd9c361778f 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { promisify } from 'bluebird'; -import fs from 'fs'; +import { promises as fs } from 'fs'; import path from 'path'; import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs'; -const mkdirAsync = promisify(fs.mkdir); - export async function checkIfPngsMatch( actualpngPath: string, baselinepngPath: string, @@ -23,8 +20,8 @@ export async function checkIfPngsMatch( const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); - await mkdirAsync(sessionDirectoryPath, { recursive: true }); - await mkdirAsync(failureDirectoryPath, { recursive: true }); + await fs.mkdir(sessionDirectoryPath, { recursive: true }); + await fs.mkdir(failureDirectoryPath, { recursive: true }); const actualpngFileName = path.basename(actualpngPath, '.png'); const baselinepngFileName = path.basename(baselinepngPath, '.png'); @@ -39,14 +36,14 @@ export async function checkIfPngsMatch( // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have // mac and linux covered which is better than nothing for now. try { - log.debug(`writeFileSync: ${baselineCopyPath}`); - fs.writeFileSync(baselineCopyPath, fs.readFileSync(baselinepngPath)); + log.debug(`writeFile: ${baselineCopyPath}`); + await fs.writeFile(baselineCopyPath, await fs.readFile(baselinepngPath)); } catch (error) { log.error(`No baseline png found at ${baselinepngPath}`); return 0; } - log.debug(`writeFileSync: ${actualCopyPath}`); - fs.writeFileSync(actualCopyPath, fs.readFileSync(actualpngPath)); + log.debug(`writeFile: ${actualCopyPath}`); + await fs.writeFile(actualCopyPath, await fs.readFile(actualpngPath)); let diffTotal = 0; diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 17b70b8510f04..c332d05039255 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -140,5 +140,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hasGeoSrcFilter = await filterBar.hasFilter('geo.src', 'US', true, true); expect(hasGeoSrcFilter).to.be(true); }); + + it('CSV export action exists in panel context menu', async () => { + const ACTION_ID = 'ACTION_EXPORT_CSV'; + const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + await panelActions.openContextMenu(); + await panelActions.clickContextMenuMoreItem(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); }); } diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 0c8a208e92ece..c5c02135ea976 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -16,9 +16,19 @@ export default function ({ getPageObjects, getService }) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); + const security = getService('security'); describe('embed in dashboard', () => { before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'geoshape_data_reader', + 'meta_for_geoshape_data_reader', + 'global_dashboard_read', + ], + false + ); await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, @@ -31,6 +41,7 @@ export default function ({ getPageObjects, getService }) { await kibanaServer.uiSettings.replace({ [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: false, }); + await security.testUser.restoreDefaults(); }); async function getRequestTimestamp() { diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js index b5640eb4ec2ea..697f6cc251b13 100644 --- a/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js @@ -9,11 +9,14 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['common', 'dashboard', 'maps']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const dashboardAddPanel = getService('dashboardAddPanel'); const DASHBOARD_NAME = 'verify_map_embeddable_state'; describe('embeddable state', () => { before(async () => { + await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); + await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); @@ -26,6 +29,10 @@ export default function ({ getPageObjects, getService }) { await PageObjects.dashboard.loadSavedDashboard(DASHBOARD_NAME); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should render map with center and zoom from embeddable state', async () => { const { lat, lon, zoom } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(0); diff --git a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js index 4aa44799db1f4..40af8ddb9d44b 100644 --- a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js +++ b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js @@ -12,8 +12,25 @@ export default function ({ getPageObjects, getService }) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardVisualizations = getService('dashboardVisualizations'); const testSubjects = getService('testSubjects'); + const security = getService('security'); describe('save and return work flow', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); describe('new map', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index d612a3776d211..f66104fc6a175 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -11,12 +11,24 @@ export default function ({ getPageObjects, getService }) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); + const security = getService('security'); describe('tooltip filter actions', () => { + before(async () => { + await security.testUser.setRoles([ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + 'global_discover_read', + ]); + }); async function loadDashboardAndOpenTooltip() { await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); + await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('dash for tooltip filter action test'); @@ -24,6 +36,10 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.lockTooltipAtPosition(200, -200); } + after(async () => { + await security.testUser.restoreDefaults(); + }); + describe('apply filter to current view', () => { before(async () => { await loadDashboardAndOpenTooltip(); diff --git a/x-pack/test/functional/es_archives/task_manager_removed_types/data.json b/x-pack/test/functional/es_archives/task_manager_removed_types/data.json new file mode 100644 index 0000000000000..8594e9d567b8a --- /dev/null +++ b/x-pack/test/functional/es_archives/task_manager_removed_types/data.json @@ -0,0 +1,30 @@ +{ + "type": "doc", + "value": { + "id": "task:be7e1250-3322-11eb-94c1-db6995e83f6a", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.6.0" + }, + "references": [ + ], + "task": { + "attempts": 0, + "params": "{\"originalParams\":{},\"superFly\":\"My middleware param!\"}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "sampleTaskRemovedType" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json b/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json new file mode 100644 index 0000000000000..c3a10064e905e --- /dev/null +++ b/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json @@ -0,0 +1,225 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "0359d7fcc04da9878ee9aadbda38ba55", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "background-session": "721df406dbb7e35ac22e4df6c3ad2b2a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "8a50736330e953bca91747723a319593", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", + "event_log_test": "bef808d4a9c27f204ffbda3359233931", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "task": "235412e52d09e7165fac8a67a43ad6b4", + "type": "2f4316de49999235636386fe51dc06c1", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0" + } + }, + "dynamic": "strict", + "properties": { + "migrationVersion": { + "dynamic": "true", + "properties": { + "task": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts index b542bff3a4aa9..c1e7aad8ac36f 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags('ciGroup2'); loadTestFile(require.resolve('./health_route')); loadTestFile(require.resolve('./task_management')); + loadTestFile(require.resolve('./task_management_removed_types')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts new file mode 100644 index 0000000000000..a0ca970bac844 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import expect from '@kbn/expect'; +import url from 'url'; +import supertestAsPromised from 'supertest-as-promised'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; + +export interface RawDoc { + _id: string; + _source: any; + _type?: string; +} +export interface SearchResults { + hits: { + hits: RawDoc[]; + }; +} + +type DeprecatedConcreteTaskInstance = Omit & { + interval: string; +}; + +type SerializedConcreteTaskInstance = Omit< + ConcreteTaskInstance, + 'state' | 'params' | 'scheduledAt' | 'startedAt' | 'retryAt' | 'runAt' +> & { + state: State; + params: Params; + scheduledAt: string; + startedAt: string | null; + retryAt: string | null; + runAt: string; +}; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const config = getService('config'); + const supertest = supertestAsPromised(url.format(config.get('servers.kibana'))); + + const REMOVED_TASK_TYPE_ID = 'be7e1250-3322-11eb-94c1-db6995e83f6a'; + + describe('removed task types', () => { + before(async () => { + await esArchiver.load('task_manager_removed_types'); + }); + + after(async () => { + await esArchiver.unload('task_manager_removed_types'); + }); + + function scheduleTask( + task: Partial + ): Promise { + return supertest + .post('/api/sample_tasks/schedule') + .set('kbn-xsrf', 'xxx') + .send({ task }) + .expect(200) + .then((response: { body: SerializedConcreteTaskInstance }) => response.body); + } + + function currentTasks(): Promise<{ + docs: Array>; + }> { + return supertest + .get('/api/sample_tasks') + .expect(200) + .then((response) => response.body); + } + + it('should successfully schedule registered tasks and mark unregistered tasks as unrecognized', async () => { + const scheduledTask = await scheduleTask({ + taskType: 'sampleTask', + schedule: { interval: `1s` }, + params: {}, + }); + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + expect(tasks.length).to.eql(2); + + const taskIds = tasks.map((task) => task.id); + expect(taskIds).to.contain(scheduledTask.id); + expect(taskIds).to.contain(REMOVED_TASK_TYPE_ID); + + const scheduledTaskInstance = tasks.find((task) => task.id === scheduledTask.id); + const removedTaskInstance = tasks.find((task) => task.id === REMOVED_TASK_TYPE_ID); + + expect(scheduledTaskInstance?.status).to.eql('claiming'); + expect(removedTaskInstance?.status).to.eql('unrecognized'); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz new file mode 100644 index 0000000000000..ab63f9a47a7ba Binary files /dev/null and b/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz differ diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json new file mode 100644 index 0000000000000..3ccdee6bdb5eb --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json @@ -0,0 +1,3577 @@ +{ + "type": "index", + "value": { + "aliases": { + "thread-data": { + "is_write_index": false + }, + "beats": { + }, + "siem-read-alias": { + } + }, + "index": "threat-data-001", + "mappings": { + "_meta": { + "beat": "auditbeat", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "auditd": { + "properties": { + "data": { + "properties": { + "a0": { + "ignore_above": 1024, + "type": "keyword" + }, + "a1": { + "ignore_above": 1024, + "type": "keyword" + }, + "a2": { + "ignore_above": 1024, + "type": "keyword" + }, + "a3": { + "ignore_above": 1024, + "type": "keyword" + }, + "a[0-3]": { + "ignore_above": 1024, + "type": "keyword" + }, + "acct": { + "ignore_above": 1024, + "type": "keyword" + }, + "acl": { + "ignore_above": 1024, + "type": "keyword" + }, + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "added": { + "ignore_above": 1024, + "type": "keyword" + }, + "addr": { + "ignore_above": 1024, + "type": "keyword" + }, + "apparmor": { + "ignore_above": 1024, + "type": "keyword" + }, + "arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "argc": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_backlog_limit": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_backlog_wait_time": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_failure": { + "ignore_above": 1024, + "type": "keyword" + }, + "banners": { + "ignore_above": 1024, + "type": "keyword" + }, + "bool": { + "ignore_above": 1024, + "type": "keyword" + }, + "bus": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "capability": { + "ignore_above": 1024, + "type": "keyword" + }, + "cgroup": { + "ignore_above": 1024, + "type": "keyword" + }, + "changed": { + "ignore_above": 1024, + "type": "keyword" + }, + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "cmd": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "compat": { + "ignore_above": 1024, + "type": "keyword" + }, + "daddr": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "default-context": { + "ignore_above": 1024, + "type": "keyword" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "dir": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "dmac": { + "ignore_above": 1024, + "type": "keyword" + }, + "dport": { + "ignore_above": 1024, + "type": "keyword" + }, + "enforcing": { + "ignore_above": 1024, + "type": "keyword" + }, + "entries": { + "ignore_above": 1024, + "type": "keyword" + }, + "exit": { + "ignore_above": 1024, + "type": "keyword" + }, + "fam": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "fd": { + "ignore_above": 1024, + "type": "keyword" + }, + "fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "feature": { + "ignore_above": 1024, + "type": "keyword" + }, + "fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "file": { + "ignore_above": 1024, + "type": "keyword" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "format": { + "ignore_above": 1024, + "type": "keyword" + }, + "fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "grantors": { + "ignore_above": 1024, + "type": "keyword" + }, + "grp": { + "ignore_above": 1024, + "type": "keyword" + }, + "hook": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "icmp_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "igid": { + "ignore_above": 1024, + "type": "keyword" + }, + "img-ctx": { + "ignore_above": 1024, + "type": "keyword" + }, + "info": { + "ignore_above": 1024, + "type": "keyword" + }, + "inif": { + "ignore_above": 1024, + "type": "keyword" + }, + "ino": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode_uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "invalid_context": { + "ignore_above": 1024, + "type": "keyword" + }, + "ioctlcmd": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "ignore_above": 1024, + "type": "keyword" + }, + "ipid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ipx-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "items": { + "ignore_above": 1024, + "type": "keyword" + }, + "iuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "ksize": { + "ignore_above": 1024, + "type": "keyword" + }, + "laddr": { + "ignore_above": 1024, + "type": "keyword" + }, + "len": { + "ignore_above": 1024, + "type": "keyword" + }, + "list": { + "ignore_above": 1024, + "type": "keyword" + }, + "lport": { + "ignore_above": 1024, + "type": "keyword" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "macproto": { + "ignore_above": 1024, + "type": "keyword" + }, + "maj": { + "ignore_above": 1024, + "type": "keyword" + }, + "major": { + "ignore_above": 1024, + "type": "keyword" + }, + "minor": { + "ignore_above": 1024, + "type": "keyword" + }, + "model": { + "ignore_above": 1024, + "type": "keyword" + }, + "msg": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "nargs": { + "ignore_above": 1024, + "type": "keyword" + }, + "net": { + "ignore_above": 1024, + "type": "keyword" + }, + "new": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-chardev": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-disk": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-fs": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-level": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-log_passwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-mem": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-range": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-rng": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-role": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-vcpu": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_lock": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-fam": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-grp": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-pid": { + "ignore_above": 1024, + "type": "keyword" + }, + "oauid": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ocomm": { + "ignore_above": 1024, + "type": "keyword" + }, + "oflag": { + "ignore_above": 1024, + "type": "keyword" + }, + "old": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-auid": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-chardev": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-disk": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-fs": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-level": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-log_passwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-mem": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-range": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-rng": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-role": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-ses": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-vcpu": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_enforcing": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_lock": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pa": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_prom": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_val": { + "ignore_above": 1024, + "type": "keyword" + }, + "op": { + "ignore_above": 1024, + "type": "keyword" + }, + "operation": { + "ignore_above": 1024, + "type": "keyword" + }, + "opid": { + "ignore_above": 1024, + "type": "keyword" + }, + "oses": { + "ignore_above": 1024, + "type": "keyword" + }, + "outif": { + "ignore_above": 1024, + "type": "keyword" + }, + "pa": { + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "per": { + "ignore_above": 1024, + "type": "keyword" + }, + "perm": { + "ignore_above": 1024, + "type": "keyword" + }, + "perm_mask": { + "ignore_above": 1024, + "type": "keyword" + }, + "permissive": { + "ignore_above": 1024, + "type": "keyword" + }, + "pfs": { + "ignore_above": 1024, + "type": "keyword" + }, + "pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "printer": { + "ignore_above": 1024, + "type": "keyword" + }, + "profile": { + "ignore_above": 1024, + "type": "keyword" + }, + "prom": { + "ignore_above": 1024, + "type": "keyword" + }, + "proto": { + "ignore_above": 1024, + "type": "keyword" + }, + "qbytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "range": { + "ignore_above": 1024, + "type": "keyword" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" + }, + "removed": { + "ignore_above": 1024, + "type": "keyword" + }, + "res": { + "ignore_above": 1024, + "type": "keyword" + }, + "resrc": { + "ignore_above": 1024, + "type": "keyword" + }, + "rport": { + "ignore_above": 1024, + "type": "keyword" + }, + "sauid": { + "ignore_above": 1024, + "type": "keyword" + }, + "scontext": { + "ignore_above": 1024, + "type": "keyword" + }, + "selected-context": { + "ignore_above": 1024, + "type": "keyword" + }, + "seperm": { + "ignore_above": 1024, + "type": "keyword" + }, + "seperms": { + "ignore_above": 1024, + "type": "keyword" + }, + "seqno": { + "ignore_above": 1024, + "type": "keyword" + }, + "seresult": { + "ignore_above": 1024, + "type": "keyword" + }, + "ses": { + "ignore_above": 1024, + "type": "keyword" + }, + "seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "sig": { + "ignore_above": 1024, + "type": "keyword" + }, + "sigev_signo": { + "ignore_above": 1024, + "type": "keyword" + }, + "smac": { + "ignore_above": 1024, + "type": "keyword" + }, + "socket": { + "properties": { + "addr": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "ignore_above": 1024, + "type": "keyword" + }, + "saddr": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "spid": { + "ignore_above": 1024, + "type": "keyword" + }, + "sport": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "subj": { + "ignore_above": 1024, + "type": "keyword" + }, + "success": { + "ignore_above": 1024, + "type": "keyword" + }, + "syscall": { + "ignore_above": 1024, + "type": "keyword" + }, + "table": { + "ignore_above": 1024, + "type": "keyword" + }, + "tclass": { + "ignore_above": 1024, + "type": "keyword" + }, + "tcontext": { + "ignore_above": 1024, + "type": "keyword" + }, + "terminal": { + "ignore_above": 1024, + "type": "keyword" + }, + "tty": { + "ignore_above": 1024, + "type": "keyword" + }, + "unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "uri": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "val": { + "ignore_above": 1024, + "type": "keyword" + }, + "ver": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm-ctx": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm-pid": { + "ignore_above": 1024, + "type": "keyword" + }, + "watch": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "paths": { + "properties": { + "cap_fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "dev": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "item": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "nametype": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_role": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_user": { + "ignore_above": 1024, + "type": "keyword" + }, + "objtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "ogid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ouid": { + "ignore_above": 1024, + "type": "keyword" + }, + "rdev": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "session": { + "ignore_above": 1024, + "type": "keyword" + }, + "summary": { + "properties": { + "actor": { + "properties": { + "primary": { + "ignore_above": 1024, + "type": "keyword" + }, + "secondary": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "how": { + "ignore_above": 1024, + "type": "keyword" + }, + "object": { + "properties": { + "primary": { + "ignore_above": 1024, + "type": "keyword" + }, + "secondary": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "origin": { + "fields": { + "raw": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "selinux": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "setgid": { + "type": "boolean" + }, + "setuid": { + "type": "boolean" + }, + "size": { + "type": "long" + }, + "target_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "flow": { + "properties": { + "complete": { + "type": "boolean" + }, + "final": { + "type": "boolean" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geoip": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "blake2b_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "xxh64": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jolokia": { + "properties": { + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secured": { + "type": "boolean" + }, + "server": { + "properties": { + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "properties": { + "annotations": { + "type": "object" + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "observer": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "sha1": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "ip": { + "type": "ip" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socket": { + "properties": { + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "system": { + "properties": { + "audit": { + "properties": { + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "boottime": { + "type": "date" + }, + "containerized": { + "type": "boolean" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timezone": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "offset": { + "properties": { + "sec": { + "type": "long" + } + } + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "newsocket": { + "properties": { + "egid": { + "type": "long" + }, + "euid": { + "type": "long" + }, + "gid": { + "type": "long" + }, + "internal_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel_sock_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "type": "long" + } + } + }, + "package": { + "properties": { + "arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "installtime": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "release": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "summary": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socket": { + "properties": { + "egid": { + "type": "long" + }, + "euid": { + "type": "long" + }, + "gid": { + "type": "long" + }, + "internal_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel_sock_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "type": "long" + } + } + }, + "user": { + "properties": { + "dir": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "properties": { + "last_changed": { + "type": "date" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "shell": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "user_information": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "audit": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "effective": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "filesystem": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "name_map": { + "type": "object" + }, + "saved": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "selinux": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "terminal": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "indexing_complete": "true", + "name": "auditbeat-8.0.0", + "rollover_alias": "auditbeat-8.0.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "query": { + "default_field": [ + "message", + "tags", + "agent.ephemeral_id", + "agent.id", + "agent.name", + "agent.type", + "agent.version", + "client.address", + "client.domain", + "client.geo.city_name", + "client.geo.continent_name", + "client.geo.country_iso_code", + "client.geo.country_name", + "client.geo.name", + "client.geo.region_iso_code", + "client.geo.region_name", + "client.mac", + "client.user.email", + "client.user.full_name", + "client.user.group.id", + "client.user.group.name", + "client.user.hash", + "client.user.id", + "client.user.name", + "cloud.account.id", + "cloud.availability_zone", + "cloud.instance.id", + "cloud.instance.name", + "cloud.machine.type", + "cloud.provider", + "cloud.region", + "container.id", + "container.image.name", + "container.image.tag", + "container.name", + "container.runtime", + "destination.address", + "destination.domain", + "destination.geo.city_name", + "destination.geo.continent_name", + "destination.geo.country_iso_code", + "destination.geo.country_name", + "destination.geo.name", + "destination.geo.region_iso_code", + "destination.geo.region_name", + "destination.mac", + "destination.user.email", + "destination.user.full_name", + "destination.user.group.id", + "destination.user.group.name", + "destination.user.hash", + "destination.user.id", + "destination.user.name", + "ecs.version", + "error.code", + "error.id", + "error.message", + "event.action", + "event.category", + "event.dataset", + "event.hash", + "event.id", + "event.kind", + "event.module", + "event.original", + "event.outcome", + "event.timezone", + "event.type", + "file.device", + "file.extension", + "file.gid", + "file.group", + "file.inode", + "file.mode", + "file.owner", + "file.path", + "file.target_path", + "file.type", + "file.uid", + "geo.city_name", + "geo.continent_name", + "geo.country_iso_code", + "geo.country_name", + "geo.name", + "geo.region_iso_code", + "geo.region_name", + "group.id", + "group.name", + "host.architecture", + "host.geo.city_name", + "host.geo.continent_name", + "host.geo.country_iso_code", + "host.geo.country_name", + "host.geo.name", + "host.geo.region_iso_code", + "host.geo.region_name", + "host.hostname", + "host.id", + "host.mac", + "host.name", + "host.os.family", + "host.os.full", + "host.os.kernel", + "host.os.name", + "host.os.platform", + "host.os.version", + "host.type", + "host.user.email", + "host.user.full_name", + "host.user.group.id", + "host.user.group.name", + "host.user.hash", + "host.user.id", + "host.user.name", + "http.request.body.content", + "http.request.method", + "http.request.referrer", + "http.response.body.content", + "http.version", + "log.level", + "log.original", + "network.application", + "network.community_id", + "network.direction", + "network.iana_number", + "network.name", + "network.protocol", + "network.transport", + "network.type", + "observer.geo.city_name", + "observer.geo.continent_name", + "observer.geo.country_iso_code", + "observer.geo.country_name", + "observer.geo.name", + "observer.geo.region_iso_code", + "observer.geo.region_name", + "observer.hostname", + "observer.mac", + "observer.os.family", + "observer.os.full", + "observer.os.kernel", + "observer.os.name", + "observer.os.platform", + "observer.os.version", + "observer.serial_number", + "observer.type", + "observer.vendor", + "observer.version", + "organization.id", + "organization.name", + "os.family", + "os.full", + "os.kernel", + "os.name", + "os.platform", + "os.version", + "process.args", + "process.executable", + "process.name", + "process.title", + "process.working_directory", + "server.address", + "server.domain", + "server.geo.city_name", + "server.geo.continent_name", + "server.geo.country_iso_code", + "server.geo.country_name", + "server.geo.name", + "server.geo.region_iso_code", + "server.geo.region_name", + "server.mac", + "server.user.email", + "server.user.full_name", + "server.user.group.id", + "server.user.group.name", + "server.user.hash", + "server.user.id", + "server.user.name", + "service.ephemeral_id", + "service.id", + "service.name", + "service.state", + "service.type", + "service.version", + "source.address", + "source.domain", + "source.geo.city_name", + "source.geo.continent_name", + "source.geo.country_iso_code", + "source.geo.country_name", + "source.geo.name", + "source.geo.region_iso_code", + "source.geo.region_name", + "source.mac", + "source.user.email", + "source.user.full_name", + "source.user.group.id", + "source.user.group.name", + "source.user.hash", + "source.user.id", + "source.user.name", + "url.domain", + "url.fragment", + "url.full", + "url.original", + "url.password", + "url.path", + "url.query", + "url.scheme", + "url.username", + "user.email", + "user.full_name", + "user.group.id", + "user.group.name", + "user.hash", + "user.id", + "user.name", + "user_agent.device.name", + "user_agent.name", + "user_agent.original", + "user_agent.os.family", + "user_agent.os.full", + "user_agent.os.kernel", + "user_agent.os.name", + "user_agent.os.platform", + "user_agent.os.version", + "user_agent.version", + "agent.hostname", + "error.type", + "cloud.project.id", + "host.os.build", + "kubernetes.pod.name", + "kubernetes.pod.uid", + "kubernetes.namespace", + "kubernetes.node.name", + "kubernetes.replicaset.name", + "kubernetes.deployment.name", + "kubernetes.statefulset.name", + "kubernetes.container.name", + "kubernetes.container.image", + "jolokia.agent.version", + "jolokia.agent.id", + "jolokia.server.product", + "jolokia.server.version", + "jolokia.server.vendor", + "jolokia.url", + "raw", + "file.origin", + "file.selinux.user", + "file.selinux.role", + "file.selinux.domain", + "file.selinux.level", + "user.audit.id", + "user.audit.name", + "user.effective.id", + "user.effective.name", + "user.effective.group.id", + "user.effective.group.name", + "user.filesystem.id", + "user.filesystem.name", + "user.filesystem.group.id", + "user.filesystem.group.name", + "user.saved.id", + "user.saved.name", + "user.saved.group.id", + "user.saved.group.name", + "user.selinux.user", + "user.selinux.role", + "user.selinux.domain", + "user.selinux.level", + "user.selinux.category", + "source.path", + "destination.path", + "auditd.message_type", + "auditd.session", + "auditd.result", + "auditd.summary.actor.primary", + "auditd.summary.actor.secondary", + "auditd.summary.object.type", + "auditd.summary.object.primary", + "auditd.summary.object.secondary", + "auditd.summary.how", + "auditd.paths.inode", + "auditd.paths.dev", + "auditd.paths.obj_user", + "auditd.paths.obj_role", + "auditd.paths.obj_domain", + "auditd.paths.obj_level", + "auditd.paths.objtype", + "auditd.paths.ouid", + "auditd.paths.rdev", + "auditd.paths.nametype", + "auditd.paths.ogid", + "auditd.paths.item", + "auditd.paths.mode", + "auditd.paths.name", + "auditd.data.action", + "auditd.data.minor", + "auditd.data.acct", + "auditd.data.addr", + "auditd.data.cipher", + "auditd.data.id", + "auditd.data.entries", + "auditd.data.kind", + "auditd.data.ksize", + "auditd.data.spid", + "auditd.data.arch", + "auditd.data.argc", + "auditd.data.major", + "auditd.data.unit", + "auditd.data.table", + "auditd.data.terminal", + "auditd.data.grantors", + "auditd.data.direction", + "auditd.data.op", + "auditd.data.tty", + "auditd.data.syscall", + "auditd.data.data", + "auditd.data.family", + "auditd.data.mac", + "auditd.data.pfs", + "auditd.data.items", + "auditd.data.a0", + "auditd.data.a1", + "auditd.data.a2", + "auditd.data.a3", + "auditd.data.hostname", + "auditd.data.lport", + "auditd.data.rport", + "auditd.data.exit", + "auditd.data.fp", + "auditd.data.laddr", + "auditd.data.sport", + "auditd.data.capability", + "auditd.data.nargs", + "auditd.data.new-enabled", + "auditd.data.audit_backlog_limit", + "auditd.data.dir", + "auditd.data.cap_pe", + "auditd.data.model", + "auditd.data.new_pp", + "auditd.data.old-enabled", + "auditd.data.oauid", + "auditd.data.old", + "auditd.data.banners", + "auditd.data.feature", + "auditd.data.vm-ctx", + "auditd.data.opid", + "auditd.data.seperms", + "auditd.data.seresult", + "auditd.data.new-rng", + "auditd.data.old-net", + "auditd.data.sigev_signo", + "auditd.data.ino", + "auditd.data.old_enforcing", + "auditd.data.old-vcpu", + "auditd.data.range", + "auditd.data.res", + "auditd.data.added", + "auditd.data.fam", + "auditd.data.nlnk-pid", + "auditd.data.subj", + "auditd.data.a[0-3]", + "auditd.data.cgroup", + "auditd.data.kernel", + "auditd.data.ocomm", + "auditd.data.new-net", + "auditd.data.permissive", + "auditd.data.class", + "auditd.data.compat", + "auditd.data.fi", + "auditd.data.changed", + "auditd.data.msg", + "auditd.data.dport", + "auditd.data.new-seuser", + "auditd.data.invalid_context", + "auditd.data.dmac", + "auditd.data.ipx-net", + "auditd.data.iuid", + "auditd.data.macproto", + "auditd.data.obj", + "auditd.data.ipid", + "auditd.data.new-fs", + "auditd.data.vm-pid", + "auditd.data.cap_pi", + "auditd.data.old-auid", + "auditd.data.oses", + "auditd.data.fd", + "auditd.data.igid", + "auditd.data.new-disk", + "auditd.data.parent", + "auditd.data.len", + "auditd.data.oflag", + "auditd.data.uuid", + "auditd.data.code", + "auditd.data.nlnk-grp", + "auditd.data.cap_fp", + "auditd.data.new-mem", + "auditd.data.seperm", + "auditd.data.enforcing", + "auditd.data.new-chardev", + "auditd.data.old-rng", + "auditd.data.outif", + "auditd.data.cmd", + "auditd.data.hook", + "auditd.data.new-level", + "auditd.data.sauid", + "auditd.data.sig", + "auditd.data.audit_backlog_wait_time", + "auditd.data.printer", + "auditd.data.old-mem", + "auditd.data.perm", + "auditd.data.old_pi", + "auditd.data.state", + "auditd.data.format", + "auditd.data.new_gid", + "auditd.data.tcontext", + "auditd.data.maj", + "auditd.data.watch", + "auditd.data.device", + "auditd.data.grp", + "auditd.data.bool", + "auditd.data.icmp_type", + "auditd.data.new_lock", + "auditd.data.old_prom", + "auditd.data.acl", + "auditd.data.ip", + "auditd.data.new_pi", + "auditd.data.default-context", + "auditd.data.inode_gid", + "auditd.data.new-log_passwd", + "auditd.data.new_pe", + "auditd.data.selected-context", + "auditd.data.cap_fver", + "auditd.data.file", + "auditd.data.net", + "auditd.data.virt", + "auditd.data.cap_pp", + "auditd.data.old-range", + "auditd.data.resrc", + "auditd.data.new-range", + "auditd.data.obj_gid", + "auditd.data.proto", + "auditd.data.old-disk", + "auditd.data.audit_failure", + "auditd.data.inif", + "auditd.data.vm", + "auditd.data.flags", + "auditd.data.nlnk-fam", + "auditd.data.old-fs", + "auditd.data.old-ses", + "auditd.data.seqno", + "auditd.data.fver", + "auditd.data.qbytes", + "auditd.data.seuser", + "auditd.data.cap_fe", + "auditd.data.new-vcpu", + "auditd.data.old-level", + "auditd.data.old_pp", + "auditd.data.daddr", + "auditd.data.old-role", + "auditd.data.ioctlcmd", + "auditd.data.smac", + "auditd.data.apparmor", + "auditd.data.fe", + "auditd.data.perm_mask", + "auditd.data.ses", + "auditd.data.cap_fi", + "auditd.data.obj_uid", + "auditd.data.reason", + "auditd.data.list", + "auditd.data.old_lock", + "auditd.data.bus", + "auditd.data.old_pe", + "auditd.data.new-role", + "auditd.data.prom", + "auditd.data.uri", + "auditd.data.audit_enabled", + "auditd.data.old-log_passwd", + "auditd.data.old-seuser", + "auditd.data.per", + "auditd.data.scontext", + "auditd.data.tclass", + "auditd.data.ver", + "auditd.data.new", + "auditd.data.val", + "auditd.data.img-ctx", + "auditd.data.old-chardev", + "auditd.data.old_val", + "auditd.data.success", + "auditd.data.inode_uid", + "auditd.data.removed", + "auditd.data.socket.port", + "auditd.data.socket.saddr", + "auditd.data.socket.addr", + "auditd.data.socket.family", + "auditd.data.socket.path", + "geoip.continent_name", + "geoip.city_name", + "geoip.region_name", + "geoip.country_iso_code", + "hash.blake2b_256", + "hash.blake2b_384", + "hash.blake2b_512", + "hash.md5", + "hash.sha1", + "hash.sha224", + "hash.sha256", + "hash.sha384", + "hash.sha3_224", + "hash.sha3_256", + "hash.sha3_384", + "hash.sha3_512", + "hash.sha512", + "hash.sha512_224", + "hash.sha512_256", + "hash.xxh64", + "event.origin", + "user.entity_id", + "user.terminal", + "process.entity_id", + "socket.entity_id", + "system.audit.host.timezone.name", + "system.audit.host.hostname", + "system.audit.host.id", + "system.audit.host.architecture", + "system.audit.host.mac", + "system.audit.host.os.platform", + "system.audit.host.os.name", + "system.audit.host.os.family", + "system.audit.host.os.version", + "system.audit.host.os.kernel", + "system.audit.package.entity_id", + "system.audit.package.name", + "system.audit.package.version", + "system.audit.package.release", + "system.audit.package.arch", + "system.audit.package.license", + "system.audit.package.summary", + "system.audit.package.url", + "system.audit.user.name", + "system.audit.user.uid", + "system.audit.user.gid", + "system.audit.user.dir", + "system.audit.user.shell", + "system.audit.user.user_information", + "system.audit.user.password.type", + "fields.*" + ] + }, + "refresh_interval": "5s" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json new file mode 100644 index 0000000000000..dfe0444e0bbd4 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -0,0 +1,13 @@ +{ + "type": "doc", + "value": { + "id": "_uZE6nwBOpWiDweSth_D", + "index": "threat-indicator-0001", + "source": { + "@timestamp": "2019-09-01T00:41:06.527Z", + "agent": { + "threat": "03ccb0ce-f65c-4279-a619-05f1d5bb000b" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json new file mode 100644 index 0000000000000..0c24fa429d908 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json @@ -0,0 +1,30 @@ +{ + "type": "index", + "value": { + "aliases": { + "threat-indicator": { + "is_write_index": false + } + }, + "index": "threat-indicator-0001", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } +} diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 46085b0db3063..166fc39f4aaaa 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -281,7 +281,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await actionsButton.click(); const menuPanel = await testSubjects.find('endpointActionsMenuPanel'); const actionItems = await menuPanel.findAllByTagName<'button'>('button'); - const expectedItems = ['Edit Policy', 'Edit Trusted Applications']; + const expectedItems = ['Edit Trusted Applications']; for (const action of actionItems) { const buttonText = await action.getVisibleText(); @@ -289,27 +289,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { } }); - it('should navigate to Policy Details when the edit security policy action is clicked', async () => { - await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy'); - await pageObjects.policy.ensureIsOnDetailsPage(); - }); - - it('should allow the user to navigate, edit, save Policy Details and be redirected back to ingest', async () => { - await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy'); - await pageObjects.policy.ensureIsOnDetailsPage(); - await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); - await pageObjects.policy.confirmAndSave(); - - await testSubjects.existOrFail('policyDetailsSuccessMessage'); - await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail(); - }); - - it('should navigate back to Ingest Policy Edit package page on click of cancel button', async () => { - await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy'); - await (await pageObjects.policy.findCancelButton()).click(); - await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail(); - }); - it('should navigate to Trusted Apps', async () => { await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('trustedApps'); await pageObjects.trustedApps.ensureIsOnTrustedAppsListPage(); @@ -321,6 +300,53 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await backButton.click(); await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail(); }); + + it('should show the endpoint policy form', async () => { + await testSubjects.existOrFail('endpointIntegrationPolicyForm'); + }); + + it('should allow updates to policy items', async () => { + const winDnsEventingCheckbox = await testSubjects.find('policyWindowsEvent_dns'); + await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( + winDnsEventingCheckbox + ); + expect(await winDnsEventingCheckbox.isSelected()).to.be(true); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + expect(await winDnsEventingCheckbox.isSelected()).to.be(false); + }); + + it('should preserve updates done from the Fleet form', async () => { + await pageObjects.ingestManagerCreatePackagePolicy.setPackagePolicyDescription( + 'protect everything' + ); + + const winDnsEventingCheckbox = await testSubjects.find('policyWindowsEvent_dns'); + await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( + winDnsEventingCheckbox + ); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + + expect( + await pageObjects.ingestManagerCreatePackagePolicy.getPackagePolicyDescriptionValue() + ).to.be('protect everything'); + }); + + it('should include updated endpoint data when saved', async () => { + const winDnsEventingCheckbox = await testSubjects.find('policyWindowsEvent_dns'); + await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( + winDnsEventingCheckbox + ); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + const wasSelected = await winDnsEventingCheckbox.isSelected(); + await (await pageObjects.ingestManagerCreatePackagePolicy.findSaveButton(true)).click(); + await pageObjects.ingestManagerCreatePackagePolicy.waitForSaveSuccessNotification(true); + + await pageObjects.ingestManagerCreatePackagePolicy.navigateToAgentPolicyEditPackagePolicy( + policyInfo.agentPolicy.id, + policyInfo.packagePolicy.id + ); + expect(await testSubjects.isSelected('policyWindowsEvent_dns')).to.be(wasSelected); + }); }); }); } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 70958d7ca7631..741040b12fd7b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -140,7 +140,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const newPolicyName = `endpoint policy ${Date.now()}`; await pageObjects.ingestManagerCreatePackagePolicy.selectAgentPolicy(); await pageObjects.ingestManagerCreatePackagePolicy.setPackagePolicyName(newPolicyName); - await (await pageObjects.ingestManagerCreatePackagePolicy.findDSaveButton()).click(); + await (await pageObjects.ingestManagerCreatePackagePolicy.findSaveButton()).click(); await pageObjects.ingestManagerCreatePackagePolicy.waitForSaveSuccessNotification(); await pageObjects.policy.ensureIsOnPolicyPage(); await policyTestResources.deletePolicyByName(newPolicyName); diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts index 747b62a9550c6..48e5b6a23458f 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts @@ -41,8 +41,10 @@ export function IngestManagerCreatePackagePolicy({ /** * Finds and returns the save button on the sticky bottom bar */ - async findDSaveButton() { - return await testSubjects.find('createPackagePolicySaveButton'); + async findSaveButton(forEditPage: boolean = false) { + return await testSubjects.find( + forEditPage ? 'saveIntegration' : 'createPackagePolicySaveButton' + ); }, /** @@ -80,11 +82,22 @@ export function IngestManagerCreatePackagePolicy({ await testSubjects.setValue('packagePolicyNameInput', name); }, + async getPackagePolicyDescriptionValue() { + return await testSubjects.getAttribute('packagePolicyDescriptionInput', 'value'); + }, + + async setPackagePolicyDescription(desc: string) { + await this.scrollToCenterOfWindow('packagePolicyDescriptionInput'); + await testSubjects.setValue('packagePolicyDescriptionInput', desc); + }, + /** * Waits for the save Notification toast to be visible */ - async waitForSaveSuccessNotification() { - await testSubjects.existOrFail('packagePolicyCreateSuccessToast'); + async waitForSaveSuccessNotification(forEditPage: boolean = false) { + await testSubjects.existOrFail( + forEditPage ? 'policyUpdateSuccessToast' : 'packagePolicyCreateSuccessToast' + ); }, /** @@ -115,11 +128,13 @@ export function IngestManagerCreatePackagePolicy({ /** * Center a given Element on the Window viewport - * @param element + * @param element if defined as a string, it should be the test subject to find */ - async scrollToCenterOfWindow(element: WebElementWrapper) { + async scrollToCenterOfWindow(element: WebElementWrapper | string) { + const ele = typeof element === 'string' ? await testSubjects.find(element) : element; + const [elementPosition, windowSize] = await Promise.all([ - element.getPosition(), + ele.getPosition(), browser.getWindowSize(), ]); await browser.execute( diff --git a/yarn.lock b/yarn.lock index 8644a53e6f52f..73741371d10c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1404,13 +1404,12 @@ version "0.0.0" uid "" -"@elastic/elasticsearch@7.10.0-rc.1": - version "7.10.0-rc.1" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.10.0-rc.1.tgz#c23fc5cbfdb40cf2ce6f9cd796b75940e8c9dc8a" - integrity sha512-STaBlEwYbT8yT3HJ+mbO1kx+Kb7Ft7Q0xG5GxZbqbAJ7PZvgGgJWwN7jUg4oKJHbTfxV3lPvFa+PaUK2TqGuYg== +"@elastic/elasticsearch@7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.10.0.tgz#da105a9c1f14146f9f2cab4e7026cb7949121b8d" + integrity sha512-vXtMAQf5/DwqeryQgRriMtnFppJNLc/R7/R0D8E+wG5/kGM5i7mg+Hi7TM4NZEuXgtzZ2a/Nf7aR0vLyrxOK/w== dependencies: debug "^4.1.1" - decompress-response "^4.2.0" hpagent "^0.1.1" ms "^2.1.1" pump "^3.0.0" @@ -1490,15 +1489,14 @@ async-retry "^1.2.3" strip-ansi "^5.2.0" -"@elastic/good@8.1.1-kibana2": - version "8.1.1-kibana2" - resolved "https://registry.yarnpkg.com/@elastic/good/-/good-8.1.1-kibana2.tgz#3ba7413da9fae4c67f128f3e9b1dc28f24523c7a" - integrity sha512-2AYmQMBjmh2896FePnnGr9nwoqRxZ6bTjregDRI0CB9r4sIpIzG6J7oMa0GztdDMlrk5CslX1g9SN5EihddPlw== +"@elastic/good@^9.0.1-kibana3": + version "9.0.1-kibana3" + resolved "https://registry.yarnpkg.com/@elastic/good/-/good-9.0.1-kibana3.tgz#a70c2b30cbb4f44d1cf4a464562e0680322eac9b" + integrity sha512-UtPKr0TmlkL1abJfO7eEVUTqXWzLKjMkz+65FvxU/Ub9kMAr4No8wHLRfDHFzBkWoDIbDWygwld011WzUnea1Q== dependencies: - hoek "5.x.x" - joi "13.x.x" - oppsy "2.x.x" - pumpify "1.3.x" + "@hapi/hoek" "9.x.x" + "@hapi/oppsy" "3.x.x" + "@hapi/validate" "1.x.x" "@elastic/makelogs@^6.0.0": version "6.0.0" @@ -1848,7 +1846,7 @@ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== -"@hapi/hoek@^9.0.0": +"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0": version "9.1.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6" integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw== @@ -1913,6 +1911,13 @@ "@hapi/hoek" "8.x.x" "@hapi/vise" "3.x.x" +"@hapi/oppsy@3.x.x": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@hapi/oppsy/-/oppsy-3.0.0.tgz#1ae397e200e86d0aa41055f103238ed8652947ca" + integrity sha512-0kfUEAqIi21GzFVK2snMO07znMEBiXb+/pOx1dmgOO9TuvFstcfmHU5i56aDfiFP2DM5WzQCU2UWc2gK1lMDhQ== + dependencies: + "@hapi/hoek" "9.x.x" + "@hapi/pez@^4.1.2": version "4.1.2" resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-4.1.2.tgz#14984d0c31fed348f10c962968a21d9761f55503" @@ -2003,6 +2008,14 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@hapi/validate@1.x.x": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad" + integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@hapi/vise@3.x.x": version "3.1.1" resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-3.1.1.tgz#dfc88f2ac90682f48bdc1b3f9b8f1eab4eabe0c8" @@ -5263,10 +5276,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@12.19.4", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^12.0.2": - version "12.19.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" - integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== +"@types/node@*", "@types/node@14.14.7", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^12.0.2": + version "14.14.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" + integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== "@types/nodemailer@^6.4.0": version "6.4.0" @@ -5524,13 +5537,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-sticky@^6.0.3": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@types/react-sticky/-/react-sticky-6.0.3.tgz#94d16a951467b29ad44c224081d9503e7e590434" - integrity sha512-tW0Y1hTr2Tao4yX58iKl0i7BaqrdObGXAzsyzd8VGVrWVEgbQuV6P6QKVd/kFC7FroXyelftiVNJ09pnfkcjww== - dependencies: - "@types/react" "*" - "@types/react-syntax-highlighter@11.0.4": version "11.0.4" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz#d86d17697db62f98046874f62fdb3e53a0bbc4cd" @@ -11187,13 +11193,6 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -decompress-response@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.0.tgz#805ca9d1d3cdf17a03951475ad6cdc93115cec3f" - integrity sha512-MHebOkORCgLW1ramLri5vzfR4r7HgXXrVkVr/eaPVRCtYWFUp9hNAuqsBxhpABbpqd7zY2IrjxXfTuaVrW0Z2A== - dependencies: - mimic-response "^2.0.0" - decompress-response@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" @@ -18826,14 +18825,14 @@ lmdb-store-0.9@0.7.3: node-gyp-build "^4.2.3" weak-lru-cache "^0.3.9" -lmdb-store@^0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.8.15.tgz#4efb0341c2df505dd6f3a7f26f834f0a142a80a2" - integrity sha512-4Q0WZh2FmcJC6esZRUWMfkCmNiz0WU9cOgrxt97ZMTnVfHyOdZhtrt0oOF5EQPfetxxJf/BorKY28aX92R6G6g== +lmdb-store@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.9.0.tgz#9a07735baaabcb8a46ee08c58ce1d578b69bdc12" + integrity sha512-5yxZ/s2J4w5mq3II5w2i4EiAAT+RvGZ3dtiWPYQDV/F8BpwqZOi7QmHdwawf15stvXv9P92Rm7t2WPbjOV9Xkg== dependencies: fs-extra "^9.0.1" lmdb-store-0.9 "0.7.3" - msgpackr "^0.5.4" + msgpackr "^0.6.0" nan "^2.14.1" node-gyp-build "^4.2.3" weak-lru-cache "^0.3.9" @@ -20352,21 +20351,28 @@ ms@2.1.1, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -msgpackr-extract@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.5.tgz#0f206da058bd3dad0f8605d324de001a8f4de967" - integrity sha512-zHhstybu+m/j3H6CVBMcILVIzATK6dWRGtlePJjsnSAj8kLT5joMa9i0v21Uc80BPNDcwFsnG/dz2318tfI81w== +msgpackr-extract@^0.3.5, msgpackr-extract@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.6.tgz#f20c0a278e44377471b1fa2a3a75a32c87693755" + integrity sha512-ASUrKn0MEFp2onn+xUBQhCNR6+RzzQAcs6p0RqKQ9sfqOZjzQ21a+ASyzgh+QAJrKcWBiZLN6L4+iXKPJV6pXg== dependencies: nan "^2.14.1" node-gyp-build "^4.2.3" -msgpackr@^0.5.3, msgpackr@^0.5.4: +msgpackr@^0.5.3: version "0.5.4" resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-0.5.4.tgz#c21c03d5e132d2e54d0b9ced02a75b1f48413380" integrity sha512-ILEWtIWwd5ESWHKoVjJ4GP7JWkpuAUJ20qi2j2qEC6twecBmK4E6YG3QW847OpmvdAhMJGq2LoDJRn/kNERTeQ== optionalDependencies: msgpackr-extract "^0.3.5" +msgpackr@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-0.6.0.tgz#57f75f80247ed3bcb937b7b5b0c7ef48123bee80" + integrity sha512-GF+hXvh1mn9f43ndEigmyTwomeJ/5OQWaxJTMeFrXouGTCYvzEtnF7Bd1DTCxOHXO85BeWFgUVA7Ev61R2KkVw== + optionalDependencies: + msgpackr-extract "^0.3.6" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -21319,7 +21325,7 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" -oppsy@2.x.x, oppsy@^2.0.0: +oppsy@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/oppsy/-/oppsy-2.0.0.tgz#3a194517adc24c3c61cdc56f35f4537e93a35e34" integrity sha1-OhlFF63CTDxhzcVvNfRTfpOjXjQ= @@ -22740,7 +22746,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -pumpify@1.3.x, pumpify@^1.3.3, pumpify@^1.3.5: +pumpify@^1.3.3, pumpify@^1.3.5: version "1.3.6" resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.3.6.tgz#00d40e5ded0a3bf1e0788b1c0cf426a42882ab64" integrity sha512-BurGAcvezsINL5US9T9wGHHcLNrG6MCp//ECtxron3vcR+Rfx5Anqq7HbZXNJvFQli8FGVsWCAvywEJFV5Hx/Q== @@ -22879,7 +22885,7 @@ raf-schd@^4.0.0, raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== -raf@^3.1.0, raf@^3.3.0, raf@^3.4.1: +raf@^3.1.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -23557,14 +23563,6 @@ react-sizeme@^2.6.7: shallowequal "^1.1.0" throttle-debounce "^2.1.0" -react-sticky@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/react-sticky/-/react-sticky-6.0.3.tgz#7a18b643e1863da113d7f7036118d2a75d9ecde4" - integrity sha512-LNH4UJlRatOqo29/VHxDZOf6fwbgfgcHO4mkEFvrie5FuaZCSTGtug5R8NGqJ0kSnX8gHw8qZN37FcvnFBJpTQ== - dependencies: - prop-types "^15.5.8" - raf "^3.3.0" - react-style-singleton@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.0.tgz#7396885332e9729957f9df51f08cadbfc164e1c4" @@ -25430,10 +25428,10 @@ shelljs@^0.6.0: resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" integrity sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg= -shelljs@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" - integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== +shelljs@^0.8.3, shelljs@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== dependencies: glob "^7.0.0" interpret "^1.0.0"