diff --git a/.backportrc.json b/.backportrc.json index 731f49183dba5..0894909d2aac4 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "targetBranchChoices": [ { "name": "master", "checked": true }, { "name": "7.x", "checked": true }, + "7.8", "7.7", "7.6", "7.5", diff --git a/.eslintrc.js b/.eslintrc.js index dde0ce010d4d4..56c06902e062b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -238,6 +238,7 @@ module.exports = { ], from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', + '!(src|x-pack)/plugins/**/(public|server)/mocks/index.{js,ts}', '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}', ], allowSameFolder: true, diff --git a/.i18nrc.json b/.i18nrc.json index be3c043b6e52f..3b2e628f7226f 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -43,7 +43,7 @@ "src/plugins/telemetry", "src/plugins/telemetry_management_section" ], - "tileMap": "src/legacy/core_plugins/tile_map", + "tileMap": "src/plugins/tile_map", "timelion": ["src/legacy/core_plugins/timelion", "src/plugins/vis_type_timelion"], "uiActions": "src/plugins/ui_actions", "visDefaultEditor": "src/plugins/vis_default_editor", diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md index 24b56a9b98621..a79244a24acf5 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md @@ -21,3 +21,9 @@ export interface IIndexPattern | [title](./kibana-plugin-plugins-data-server.iindexpattern.title.md) | string | | | [type](./kibana-plugin-plugins-data-server.iindexpattern.type.md) | string | | +## Methods + +| Method | Description | +| --- | --- | +| [getTimeField()](./kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md) | | + diff --git a/docs/management/alerting/images/alerts-and-actions-ui.png b/docs/management/alerting/images/alerts-and-actions-ui.png index acf3f3b1f0be9..d46df21e6f6b0 100644 Binary files a/docs/management/alerting/images/alerts-and-actions-ui.png and b/docs/management/alerting/images/alerts-and-actions-ui.png differ diff --git a/docs/management/alerting/images/alerts-details-instance-muting.png b/docs/management/alerting/images/alerts-details-instance-muting.png index 9d26fad419e4f..fd59e79d07279 100644 Binary files a/docs/management/alerting/images/alerts-details-instance-muting.png and b/docs/management/alerting/images/alerts-details-instance-muting.png differ diff --git a/docs/management/alerting/images/alerts-details-instances-active.png b/docs/management/alerting/images/alerts-details-instances-active.png index d6895bd4952b8..7506d1cb8c65e 100644 Binary files a/docs/management/alerting/images/alerts-details-instances-active.png and b/docs/management/alerting/images/alerts-details-instances-active.png differ diff --git a/docs/management/alerting/images/alerts-details-instances-inactive.png b/docs/management/alerting/images/alerts-details-instances-inactive.png index b049b4ba082f6..a757d59e12360 100644 Binary files a/docs/management/alerting/images/alerts-details-instances-inactive.png and b/docs/management/alerting/images/alerts-details-instances-inactive.png differ diff --git a/docs/management/alerting/images/alerts-details-muting.png b/docs/management/alerting/images/alerts-details-muting.png index 9b47d82a74639..29cdf707b4912 100644 Binary files a/docs/management/alerting/images/alerts-details-muting.png and b/docs/management/alerting/images/alerts-details-muting.png differ diff --git a/docs/management/alerting/images/alerts-filter-by-action-type.png b/docs/management/alerting/images/alerts-filter-by-action-type.png index 94336a20e1d6c..c0e495a87ecd3 100644 Binary files a/docs/management/alerting/images/alerts-filter-by-action-type.png and b/docs/management/alerting/images/alerts-filter-by-action-type.png differ diff --git a/docs/management/alerting/images/alerts-filter-by-type.png b/docs/management/alerting/images/alerts-filter-by-type.png index 75ffb3ff69bab..859274e9b6613 100644 Binary files a/docs/management/alerting/images/alerts-filter-by-type.png and b/docs/management/alerting/images/alerts-filter-by-type.png differ diff --git a/docs/management/alerting/images/individual-mute-disable.png b/docs/management/alerting/images/individual-mute-disable.png index ca00240a4af61..dc187c97de309 100644 Binary files a/docs/management/alerting/images/individual-mute-disable.png and b/docs/management/alerting/images/individual-mute-disable.png differ diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 8794c389d72bc..09878b3059ac8 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -43,11 +43,10 @@ see https://www.elastic.co/subscriptions[the subscription page]. [[create-connectors]] === Preconfigured connectors and action types -You can create connectors for actions in <> or via the action API. -For out-of-the-box and standardized connectors, you can <> +For out-of-the-box and standardized connectors, you can <> before {kib} starts. -Action type with only preconfigured connectors could be specified as a <>. +If you preconfigure a connector, you can also <>. include::action-types/email.asciidoc[] include::action-types/index.asciidoc[] @@ -56,4 +55,3 @@ include::action-types/server-log.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] include::pre-configured-connectors.asciidoc[] -include::pre-configured-action-types.asciidoc[] diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index 794fc14005f2f..689d870d9cadc 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -19,6 +19,37 @@ Username:: username for 'login' type authentication. Password:: password for 'login' type authentication. [float] +[[Preconfigured-email-configuration]] +==== Preconfigured action type + +[source,text] +-- + id: 'my-email' + name: preconfigured-email-action-type + actionTypeId: .email + config: + from: testsender@test.com <1.1> + host: validhostname <1.2> + port: 8080 <1.3> + secure: false <1.4> + secrets: + user: testuser <2.1> + password: passwordkeystorevalue <2.2> +-- + +`config` defines the action type specific to the configuration and contains the following properties: + +<1.1> `from:` is an email address and correspond to *Sender*. +<1.2> `host:` is a string and correspond to *Host*. +<1.3> `port:` is a number and correspond to *Port*. +<1.4> `secure:` is a boolean and correspond to *Secure*. + +`secrets` defines action type sensitive configuration: + +<2.1> `user:` is a string and correspond to *User*. +<2.2> `password:` is a string and correspond to *Password*. Should be stored in the <>. + + [[email-action-configuration]] ==== Action configuration diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc index 625b8f704b7c6..4f5254e3311d8 100644 --- a/docs/user/alerting/action-types/index.asciidoc +++ b/docs/user/alerting/action-types/index.asciidoc @@ -15,6 +15,28 @@ Index:: The {es} index to be written to. Refresh:: Setting for the {ref}/docs-refresh.html[refresh] policy for the write request. Execution time field:: This field will be automatically set to the time the alert condition was detected. +[float] +[[Preconfigured-index-configuration]] +==== Preconfigured action type + +[source,text] +-- + id: 'my-index' + name: action-type-index + actionTypeId: .index + config: + index: .kibana <1> + refresh: true <2> + executionTimeField: somedate <3> +-- + +`config` defines the action type specific to the configuration and contains the following properties: + +<1> `index:` is a string and correspond to *Index*. +<2> `refresh:` is a boolean and correspond to *Refresh*. +<3> `executionTimeField:` is a string and correspond to *Execution time field*. + + [float] [[index-action-configuration]] ==== Action configuration diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index 673b4f6263e18..957c035b028f6 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -135,6 +135,29 @@ Name:: The name of the connector. The name is used to identify a connector API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is whitelisted. Integration Key:: A 32 character PagerDuty Integration Key for an integration on a service, also referred to as the routing key. +[float] +[[Preconfigured-pagerduty-configuration]] +==== Preconfigured action type + +[source,text] +-- + id: 'my-pagerduty' + name: preconfigured-pagerduty-action-type + actionTypeId: .pagerduty + config: + apiUrl: https://test.host <1.1> + secrets: + routingKey: testroutingkey <2.1> +-- + +`config` defines the action type specific to the configuration and contains the following properties: + +<1.1> `apiUrl:` is URL string and correspond to *API URL*. + +`secrets` defines action type sensitive configuration: + +<2.1> `routingKey:` is a string and correspond to *Integration Key*. + [float] [[pagerduty-action-configuration]] ==== Action configuration diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc index 8f888785626c9..f08dbe5542f0f 100644 --- a/docs/user/alerting/action-types/server-log.asciidoc +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -12,6 +12,17 @@ Server log connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +[float] +[[Preconfigured-server-log-configuration]] +==== Preconfigured action type + +[source,text] +-- + id: 'my-server-log' + name: test + actionTypeId: .server-log +-- + [float] [[server-log-action-configuration]] ==== Action configuration diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc index c0965d65bfdbe..195093536bc04 100644 --- a/docs/user/alerting/action-types/slack.asciidoc +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -13,6 +13,24 @@ Slack connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. +[float] +[[Preconfigured-slack-configuration]] +==== Preconfigured action type + +[source,text] +-- + id: 'my-slack' + name: preconfigured-slack-action-type + actionTypeId: .slack + config: + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' <1> +-- + +`config` defines the action type specific to the configuration and contains the following properties: + +<1> `webhookUrl:` is URL string and correspond to *Webhook URL*. + + [float] [[slack-action-configuration]] ==== Action configuration diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc index 64bfa6a1d6364..f4c108426642d 100644 --- a/docs/user/alerting/action-types/webhook.asciidoc +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -17,6 +17,36 @@ Headers:: A set of key-value pairs sent as headers with the request User:: An optional username. If set, HTTP basic authentication is used. Currently only basic authentication is supported. Password:: An optional password. If set, HTTP basic authentication is used. Currently only basic authentication is supported. +[float] +[[Preconfigured-webhook-configuration]] +==== Preconfigured action type + +[source,text] +-- + id: 'my-webhook' + name: preconfigured-webhook-action-type + actionTypeId: .webhook + config: + url: https://test.host <1.1> + method: POST <1.2> + headers: <1.3> + testheader: testvalue + secrets: + user: testuser <2.1> + password: passwordkeystorevalue <2.2> +-- + +`config` defines the action type specific to the configuration and contains the following properties: + +<1.1> `url:` is URL string and correspond to *URL*. +<1.2> `method:` is a string and correspond to *Method*. +<1.3> `headers:` is Record and correspond to *Headers*. + +`secrets` defines action type sensitive configuration: + +<2.1> `user:` is a string and correspond to *User*. +<2.2> `password:` is a string and correspond to *Password*. Should be stored in the <>. + [float] [[webhook-action-configuration]] ==== Action configuration diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index f05afac34e595..d05a727016455 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -22,12 +22,12 @@ image::images/alert-flyout-sections.png[The three sections of an alert definitio All alert share the following four properties in common: [role="screenshot"] -image::images/alert-flyout-general-details.png[All alerts have name, tags, check every, and re-notify every properties in common] +image::images/alert-flyout-general-details.png[alt='All alerts have name, tags, check every, and notify every properties in common'] Name:: The name of the alert. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable alert listing in the management UI. A distinctive name can help identify and find an alert. Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. -Re-notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. +Notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. [float] [[defining-alerts-type-conditions]] diff --git a/docs/user/alerting/images/alert-flyout-action-type-selection.png b/docs/user/alerting/images/alert-flyout-action-type-selection.png index e4448ca5f3fcd..2df2a031c6661 100644 Binary files a/docs/user/alerting/images/alert-flyout-action-type-selection.png and b/docs/user/alerting/images/alert-flyout-action-type-selection.png differ diff --git a/docs/user/alerting/images/alert-flyout-alert-conditions.png b/docs/user/alerting/images/alert-flyout-alert-conditions.png index f3e8f42ff0f37..8e0eff0224363 100644 Binary files a/docs/user/alerting/images/alert-flyout-alert-conditions.png and b/docs/user/alerting/images/alert-flyout-alert-conditions.png differ diff --git a/docs/user/alerting/images/alert-flyout-alert-type-selection.png b/docs/user/alerting/images/alert-flyout-alert-type-selection.png index a0a25dc5f1bbc..ccd3f07f07c94 100644 Binary files a/docs/user/alerting/images/alert-flyout-alert-type-selection.png and b/docs/user/alerting/images/alert-flyout-alert-type-selection.png differ diff --git a/docs/user/alerting/images/alert-flyout-general-details.png b/docs/user/alerting/images/alert-flyout-general-details.png index db56c16c1c308..883c2348ecc8a 100644 Binary files a/docs/user/alerting/images/alert-flyout-general-details.png and b/docs/user/alerting/images/alert-flyout-general-details.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-conditions.png b/docs/user/alerting/images/alert-types-index-threshold-conditions.png index 356732dfb9777..5d66123ac733e 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-conditions.png and b/docs/user/alerting/images/alert-types-index-threshold-conditions.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png b/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png index fc40da7436547..055b643ec3458 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png and b/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png b/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png index ea3a3849c8927..5be81b45612bc 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png and b/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-index.png b/docs/user/alerting/images/alert-types-index-threshold-example-index.png index 8f818f7001278..b13201ce5d38a 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-index.png and b/docs/user/alerting/images/alert-types-index-threshold-example-index.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-preview.png b/docs/user/alerting/images/alert-types-index-threshold-example-preview.png index b5d9c38d99810..70e1355004c47 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-preview.png and b/docs/user/alerting/images/alert-types-index-threshold-example-preview.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png b/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png index 9c51807b8d219..7e9432d8c8678 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png and b/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png b/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png index 24e4e03f829ce..4b1eaa631dc98 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png and b/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-window.png b/docs/user/alerting/images/alert-types-index-threshold-example-window.png index 5405415958485..b4b272d2a241a 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-window.png and b/docs/user/alerting/images/alert-types-index-threshold-example-window.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-preview.png b/docs/user/alerting/images/alert-types-index-threshold-preview.png index 3709f162b612b..b3b868dbc41e8 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-preview.png and b/docs/user/alerting/images/alert-types-index-threshold-preview.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-select.png b/docs/user/alerting/images/alert-types-index-threshold-select.png index 0c2776e01b962..18c28a703e966 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-select.png and b/docs/user/alerting/images/alert-types-index-threshold-select.png differ diff --git a/docs/user/alerting/images/alerting-overview.png b/docs/user/alerting/images/alerting-overview.png index 383bc8c2ce015..b4ec6f3df6028 100644 Binary files a/docs/user/alerting/images/alerting-overview.png and b/docs/user/alerting/images/alerting-overview.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-select-type.png b/docs/user/alerting/images/pre-configured-action-type-select-type.png index 5f555f851cd81..29e5a29edc7c0 100644 Binary files a/docs/user/alerting/images/pre-configured-action-type-select-type.png and b/docs/user/alerting/images/pre-configured-action-type-select-type.png differ diff --git a/docs/user/alerting/pre-configured-action-types.asciidoc b/docs/user/alerting/pre-configured-action-types.asciidoc deleted file mode 100644 index 780a2119037b1..0000000000000 --- a/docs/user/alerting/pre-configured-action-types.asciidoc +++ /dev/null @@ -1,61 +0,0 @@ -[role="xpack"] -[[pre-configured-action-types]] - -== Preconfigured action types - -A preconfigure an action type has all the information it needs prior to startup. -A preconfigured action type offers the following capabilities: - -- Requires no setup. Configuration and credentials needed to execute an -action are predefined. -- Has only <>. -- Connectors of the preconfigured action type cannot be edited or deleted. - -[float] -[[preconfigured-action-type-example]] -=== Creating a preconfigured action - -In the `kibana.yml` file: - -. Exclude the action type from `xpack.actions.enabledActionTypes`. -. Add all its connectors. - -The following example shows a valid configuration of preconfigured action type with one out-of-the box connector. - -```js - xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1> - xpack.actions.preconfigured: <2> - - id: 'my-server-log' - actionTypeId: .server-log - name: 'Server log #xyz' -``` - -<1> `enabledActionTypes` should exclude preconfigured action type to prevent creating and deleting connectors. -<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. - -[float] -[[pre-configured-action-type-alert-form]] -=== Attaching a preconfigured action to an alert - -To attach an action to an alert, -select from a list of available action types, and -then select the *Server log* type. This action type was configured previously. - -[role="screenshot"] -image::images/pre-configured-action-type-alert-form.png[Create alert with selected Server log action type] - -[float] -[[managing-pre-configured-action-types]] -=== Managing preconfigured actions - -Connectors with preconfigured actions appear in the connector list, regardless of which space the user is in. -They are tagged as “preconfigured” and cannot be deleted. - -[role="screenshot"] -image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured] - -Clicking *Create connector* shows the list of available action types. -Preconfigured action types are not included because you can't create a connector with a preconfigured action type. - -[role="screenshot"] -image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu] diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc index 4c408da92f579..5ff4ea15df561 100644 --- a/docs/user/alerting/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/pre-configured-connectors.asciidoc @@ -1,11 +1,10 @@ [role="xpack"] -[[pre-configured-connectors]] +[[pre-configured-action-types-and-connectors]] -== Preconfigured connectors +== Preconfigured connectors and action types -You can preconfigure an action connector to have all the information it needs prior to startup +You can preconfigure an action type or a connector to have all the information it needs prior to startup by adding it to the `kibana.yml` file. -Sensitive configuration information, such as credentials, can use the {kib} keystore. Preconfigured connectors offer the following capabilities: @@ -14,11 +13,15 @@ action are predefined, including the connector name and ID. - Appear in all spaces because they are not saved objects. - Cannot be edited or deleted. +Sensitive configuration information, such as credentials, can use the <>. + +A preconfigured action types has only preconfigured connectors. Preconfigured connectors can belong to either the preconfigured action type or to the regular action type. + [float] [[preconfigured-connector-example]] -=== Example of a preconfigured connector +=== Creating a preconfigured connector -The following example shows a valid configuration 2 out-of-the box connector. +The following example shows a valid configuration of two out-of-the box connectors: <> and <>. ```js xpack.actions.preconfigured: @@ -49,26 +52,30 @@ The following example shows a valid configuration 2 out-of-the box connector. [NOTE] ============================================== -Sensitive properties, such as passwords, can also be stored in the {kib} keystore. +Sensitive properties, such as passwords, can also be stored in the <>. ============================================== [float] -[[pre-configured-connector-alert-form]] -=== Creating an alert with a preconfigured connector +[[preconfigured-action-type-example]] +=== Creating a preconfigured action type -When attaching an action to an alert, -select from a list of available action types, and -then select the Slack or Webhook type. Those action types were configured previously. -The preconfigured connector is installed and is automatically selected. +In the `kibana.yml` file: -[role="screenshot"] -image::images/alert-pre-configured-slack-connector.png[Create alert with selected Slack action type] +. Exclude the action type from `xpack.actions.enabledActionTypes`. +. Add all its preconfigured connectors. -The dropdown is populated with additional preconfigured Slack connectors. -The `preconfigured` label distinguishes them from space-aware connectors that use saved objects. +The following example shows a valid configuration of preconfigured action type with one out-of-the box connector. -[role="screenshot"] -image::images/alert-pre-configured-connectors-dropdown.png[Dropdown list with pre-cofigured connectors] +```js + xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1> + xpack.actions.preconfigured: <2> + - id: 'my-server-log' + actionTypeId: .server-log + name: 'Server log #xyz' +``` + +<1> `enabledActionTypes` should exclude preconfigured action type to prevent creating and deleting connectors. +<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. [float] [[managing-pre-configured-connectors]] @@ -85,3 +92,37 @@ A message indicates that this is a preconfigured connector. [role="screenshot"] image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] + +The connector details preview is disabled for preconfigured connectors. + +[role="screenshot"] +image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured] + + +[float] +[[managing-pre-configured-action-types]] +=== Managing preconfigured action types + +Clicking *Create connector* shows the list of available action types. +Disabled action types are not included. + +[role="screenshot"] +image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu] + +[float] +[[pre-configured-connector-alert-form]] +=== Alert with a preconfigured connector + +When attaching an action to an alert, +select from a list of available action types, and +then select the Slack or Webhook type. Those action types were configured previously. +The preconfigured connector is installed and is automatically selected. + +[role="screenshot"] +image::images/alert-pre-configured-slack-connector.png[Create alert with selected Slack action type] + +The dropdown is populated with additional preconfigured Slack connectors. +The `preconfigured` label distinguishes them from space-aware connectors that use saved objects. + +[role="screenshot"] +image::images/alert-pre-configured-connectors-dropdown.png[Dropdown list with pre-cofigured connectors] diff --git a/docs/visualize/timelion.asciidoc b/docs/visualize/timelion.asciidoc index 852c3e1ecdeca..9e41cce561454 100644 --- a/docs/visualize/timelion.asciidoc +++ b/docs/visualize/timelion.asciidoc @@ -32,7 +32,9 @@ To start tracking the real-time percentage of CPU, enter the following in the *T [source,text] ---------------------------------- -.es(index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct') +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') ---------------------------------- [role="screenshot"] @@ -70,7 +72,12 @@ To easily distinguish between the two data sets, add the label names: [source,text] ---------------------------------- -.es(offset=-1h,index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('last hour'), .es(index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('current hour') <1> +.es(offset=-1h,index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct').label('last hour'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct').label('current hour') <1> ---------------------------------- <1> `.label()` adds custom labels to the visualization. diff --git a/src/core/public/chrome/ui/_loading_indicator.scss b/src/core/public/chrome/ui/_loading_indicator.scss index 026c23b93b040..ad934717b4b76 100644 --- a/src/core/public/chrome/ui/_loading_indicator.scss +++ b/src/core/public/chrome/ui/_loading_indicator.scss @@ -11,7 +11,7 @@ $kbnLoadingIndicatorColor2: tint($euiColorAccent, 60%); top: 0; // 1 left: 0; // 1 right: 0; // 1 - z-index: $euiZLevel1; // 1 + z-index: $euiZLevel2; // 1 overflow: hidden; // 2 height: $euiSizeXS / 2; @@ -28,7 +28,7 @@ $kbnLoadingIndicatorColor2: tint($euiColorAccent, 60%); right: 0; bottom: 0; position: absolute; - z-index: $euiZLevel1 + 1; + z-index: $euiZLevel2 + 1; visibility: visible; display: block; animation: kbn-animate-loading-indicator 2s linear infinite; diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 85f15b4c18b66..5e55a34193a96 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -88,5 +88,5 @@ export interface SavedObjectMigrationContext { * @public */ export interface SavedObjectMigrationMap { - [version: string]: SavedObjectMigrationFn; + [version: string]: SavedObjectMigrationFn; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 62d11ee7cf9a7..bd6046b5ec281 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1735,7 +1735,7 @@ export type SavedObjectMigrationFn; } // @public diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index a13f61af60173..5019c8bd22341 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -50,6 +50,9 @@ export const PROJECTS = [ ...glob .sync('test/plugin_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) .map(path => new Project(resolve(REPO_ROOT, path))), + ...glob + .sync('test/interpreter_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) + .map(path => new Project(resolve(REPO_ROOT, path))), ]; export function filterProjectsByFlag(projectFlag?: string) { diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js index 9f5f4b764f9b0..691318e32245b 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -21,6 +21,9 @@ import Bluebird from 'bluebird'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import $ from 'jquery'; + +import 'leaflet/dist/leaflet.js'; +import 'leaflet-vega'; // Will be replaced with new path when tests are moved // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { createVegaVisualization } from '../../../../../../plugins/vis_type_vega/public/vega_visualization'; @@ -100,6 +103,39 @@ describe('VegaVisualizations', () => { setSavedObjects(npStart.core.savedObjects); setNotifications(npStart.core.notifications); + const mockMapConfig = { + includeElasticMapsService: true, + proxyElasticMapsServiceInMaps: false, + tilemap: { + deprecated: { + config: { + options: { + attribution: '', + }, + }, + }, + options: { + attribution: '', + minZoom: 0, + maxZoom: 10, + }, + }, + regionmap: { + includeElasticMapsService: true, + layers: [], + }, + manifestServiceUrl: '', + emsFileApiUrl: 'https://vector.maps.elastic.co', + emsTileApiUrl: 'https://tiles.maps.elastic.co', + emsLandingPageUrl: 'https://maps.elastic.co/v7.7', + emsFontLibraryUrl: 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf', + emsTileLayerId: { + bright: 'road_map', + desaturated: 'road_map_desaturated', + dark: 'dark_map', + }, + }; + beforeEach(ngMock.module('kibana')); beforeEach( ngMock.inject(() => { @@ -127,7 +163,7 @@ describe('VegaVisualizations', () => { return 'not found'; } }); - const serviceSettings = new ServiceSettings(); + const serviceSettings = new ServiceSettings(mockMapConfig, mockMapConfig.tilemap); vegaVisualizationDependencies = { serviceSettings, core: { diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index ad67a74121cc9..4e97d46ab1773 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -45,7 +45,6 @@ import 'ui/autoload/all'; import './management'; import './dev_tools'; import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/public'; -import 'leaflet'; import { localApplicationService } from './local_application_service'; npSetup.plugins.kibanaLegacy.registerLegacyAppAlias('doc', 'discover', { keepPrefix: true }); diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js index 87592cf4e750e..7271f39debb39 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -20,6 +20,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import _ from 'lodash'; + import ChoroplethLayer from '../choropleth_layer'; import { ImageComparator } from 'test_utils/image_comparator'; import worldJson from './world.json'; @@ -103,31 +104,29 @@ describe('RegionMapsVisualizationTests', function() { let getManifestStub; beforeEach( ngMock.inject(() => { + const mapConfig = { + emsFileApiUrl: '', + emsTileApiUrl: '', + emsLandingPageUrl: '', + }; + const tilemapsConfig = { + deprecated: { + config: { + options: { + attribution: '123', + }, + }, + }, + }; setInjectedVarFunc(injectedVar => { switch (injectedVar) { - case 'mapConfig': - return { - emsFileApiUrl: '', - emsTileApiUrl: '', - emsLandingPageUrl: '', - }; - case 'tilemapsConfig': - return { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, - }, - }; case 'version': return '123'; default: return 'not found'; } }); - const serviceSettings = new ServiceSettings(); + const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); const regionmapsConfig = { includeElasticMapsService: true, layers: [], diff --git a/src/legacy/core_plugins/region_map/public/choropleth_layer.js b/src/legacy/core_plugins/region_map/public/choropleth_layer.js index 4ea9cc1f7bfbf..f8c48958a1b9b 100644 --- a/src/legacy/core_plugins/region_map/public/choropleth_layer.js +++ b/src/legacy/core_plugins/region_map/public/choropleth_layer.js @@ -18,7 +18,6 @@ */ import $ from 'jquery'; -import L from 'leaflet'; import _ from 'lodash'; import d3 from 'd3'; import { i18n } from '@kbn/i18n'; @@ -86,6 +85,7 @@ export default class ChoroplethLayer extends KibanaMapLayer { this._layerName = name; this._layerConfig = layerConfig; + // eslint-disable-next-line no-undef this._leafletLayer = L.geoJson(null, { onEachFeature: (feature, layer) => { layer.on('click', () => { @@ -96,6 +96,7 @@ export default class ChoroplethLayer extends KibanaMapLayer { mouseover: () => { const tooltipContents = this._tooltipFormatter(feature); if (!location) { + // eslint-disable-next-line no-undef const leafletGeojson = L.geoJson(feature); location = leafletGeojson.getBounds().getCenter(); } @@ -428,6 +429,7 @@ CORS configuration of the server permits requests from the Kibana application on const { min, max } = getMinMax(this._metrics); + // eslint-disable-next-line no-undef const boundsOfAllFeatures = new L.LatLngBounds(); return { leafletStyleFunction: geojsonFeature => { @@ -435,6 +437,7 @@ CORS configuration of the server permits requests from the Kibana application on if (!match) { return emptyStyle(); } + // eslint-disable-next-line no-undef const boundsOfFeature = L.geoJson(geojsonFeature).getBounds(); boundsOfAllFeatures.extend(boundsOfFeature); diff --git a/src/legacy/core_plugins/tile_map/index.ts b/src/legacy/core_plugins/tile_map/index.ts deleted file mode 100644 index 27f019318a82b..0000000000000 --- a/src/legacy/core_plugins/tile_map/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const tileMapPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'tile_map', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => { - const serverConfig = server.config(); - const mapConfig: Record = serverConfig.get('map'); - - return { - emsTileLayerId: mapConfig.emsTileLayerId, - }; - }, - }, - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default tileMapPluginInitializer; diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 1bc85fa110ca0..698c124d2d805 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -301,7 +301,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` >
@@ -995,7 +995,7 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] >
diff --git a/src/plugins/dashboard/public/application/dashboard_empty_screen.tsx b/src/plugins/dashboard/public/application/dashboard_empty_screen.tsx index 8bf205b8cb507..955d5244ce190 100644 --- a/src/plugins/dashboard/public/application/dashboard_empty_screen.tsx +++ b/src/plugins/dashboard/public/application/dashboard_empty_screen.tsx @@ -50,8 +50,8 @@ export function DashboardEmptyScreen({ }: DashboardEmptyScreenProps) { const IS_DARK_THEME = uiSettings.get('theme:darkMode'); const emptyStateGraphicURL = IS_DARK_THEME - ? '/plugins/kibana/home/assets/welcome_graphic_dark_2x.png' - : '/plugins/kibana/home/assets/welcome_graphic_light_2x.png'; + ? '/plugins/home/assets/welcome_graphic_dark_2x.png' + : '/plugins/home/assets/welcome_graphic_light_2x.png'; const linkToVisualizeParagraph = (

} description={ diff --git a/src/legacy/core_plugins/kibana/public/home/assets/illustration_elastic_heart.png b/src/plugins/home/public/assets/illustration_elastic_heart.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/assets/illustration_elastic_heart.png rename to src/plugins/home/public/assets/illustration_elastic_heart.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/ecommerce/dashboard.png b/src/plugins/home/public/assets/sample_data_resources/ecommerce/dashboard.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/ecommerce/dashboard.png rename to src/plugins/home/public/assets/sample_data_resources/ecommerce/dashboard.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/ecommerce/dashboard_dark.png b/src/plugins/home/public/assets/sample_data_resources/ecommerce/dashboard_dark.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/ecommerce/dashboard_dark.png rename to src/plugins/home/public/assets/sample_data_resources/ecommerce/dashboard_dark.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/flights/dashboard.png b/src/plugins/home/public/assets/sample_data_resources/flights/dashboard.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/flights/dashboard.png rename to src/plugins/home/public/assets/sample_data_resources/flights/dashboard.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/flights/dashboard_dark.png b/src/plugins/home/public/assets/sample_data_resources/flights/dashboard_dark.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/flights/dashboard_dark.png rename to src/plugins/home/public/assets/sample_data_resources/flights/dashboard_dark.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/logs/dashboard.png b/src/plugins/home/public/assets/sample_data_resources/logs/dashboard.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/logs/dashboard.png rename to src/plugins/home/public/assets/sample_data_resources/logs/dashboard.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/logs/dashboard_dark.png b/src/plugins/home/public/assets/sample_data_resources/logs/dashboard_dark.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/logs/dashboard_dark.png rename to src/plugins/home/public/assets/sample_data_resources/logs/dashboard_dark.png diff --git a/src/legacy/core_plugins/kibana/public/home/assets/welcome_graphic_dark_2x.png b/src/plugins/home/public/assets/welcome_graphic_dark_2x.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/assets/welcome_graphic_dark_2x.png rename to src/plugins/home/public/assets/welcome_graphic_dark_2x.png diff --git a/src/legacy/core_plugins/kibana/public/home/assets/welcome_graphic_light_2x.png b/src/plugins/home/public/assets/welcome_graphic_light_2x.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/assets/welcome_graphic_light_2x.png rename to src/plugins/home/public/assets/welcome_graphic_light_2x.png diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts index 3e16187c44343..b0cc2e2db3cc9 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts @@ -36,8 +36,8 @@ export const ecommerceSpecProvider = function(): SampleDatasetSchema { id: 'ecommerce', name: ecommerceName, description: ecommerceDescription, - previewImagePath: '/plugins/kibana/home/sample_data_resources/ecommerce/dashboard.png', - darkPreviewImagePath: '/plugins/kibana/home/sample_data_resources/ecommerce/dashboard_dark.png', + previewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard.png', + darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard_dark.png', overviewDashboard: '722b74f0-b882-11e8-a6d9-e546fe2bba5f', appLinks: initialAppLinks, defaultIndex: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts index d63ea8f7fb493..fc3cb6094b5ea 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts @@ -36,8 +36,8 @@ export const flightsSpecProvider = function(): SampleDatasetSchema { id: 'flights', name: flightsName, description: flightsDescription, - previewImagePath: '/plugins/kibana/home/sample_data_resources/flights/dashboard.png', - darkPreviewImagePath: '/plugins/kibana/home/sample_data_resources/flights/dashboard_dark.png', + previewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard.png', + darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard_dark.png', overviewDashboard: '7adfa750-4c81-11e8-b3d7-01146121b73d', appLinks: initialAppLinks, defaultIndex: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts index bb6e2982f59a0..d8f205dff24e8 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts @@ -36,8 +36,8 @@ export const logsSpecProvider = function(): SampleDatasetSchema { id: 'logs', name: logsName, description: logsDescription, - previewImagePath: '/plugins/kibana/home/sample_data_resources/logs/dashboard.png', - darkPreviewImagePath: '/plugins/kibana/home/sample_data_resources/logs/dashboard_dark.png', + previewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard.png', + darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard_dark.png', overviewDashboard: 'edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b', appLinks: initialAppLinks, defaultIndex: '90943e30-9a47-11e8-b64d-95841ca0b247', diff --git a/src/plugins/maps_legacy/config.ts b/src/plugins/maps_legacy/config.ts new file mode 100644 index 0000000000000..13a0ad6b393a3 --- /dev/null +++ b/src/plugins/maps_legacy/config.ts @@ -0,0 +1,67 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { configSchema as tilemapSchema } from '../tile_map/config'; + +// TODO: Pull this portion from region_map +export const regionmapSchema = schema.object({ + includeElasticMapsService: schema.boolean({ defaultValue: true }), + layers: schema.arrayOf( + schema.object({ + url: schema.string(), + format: schema.object({ + type: schema.string({ defaultValue: 'geojson' }), + }), + meta: schema.object({ + feature_collection_path: schema.string({ defaultValue: 'data' }), + }), + attribution: schema.string(), + name: schema.string(), + fields: schema.arrayOf( + schema.object({ + name: schema.string(), + description: schema.string(), + }) + ), + }), + { defaultValue: [] } + ), +}); + +export const configSchema = schema.object({ + includeElasticMapsService: schema.boolean({ defaultValue: true }), + proxyElasticMapsServiceInMaps: schema.boolean({ defaultValue: false }), + tilemap: tilemapSchema, + regionmap: regionmapSchema, + manifestServiceUrl: schema.string({ defaultValue: '' }), + emsFileApiUrl: schema.string({ defaultValue: 'https://vector.maps.elastic.co' }), + emsTileApiUrl: schema.string({ defaultValue: 'https://tiles.maps.elastic.co' }), + emsLandingPageUrl: schema.string({ defaultValue: 'https://maps.elastic.co/v7.7' }), + emsFontLibraryUrl: schema.string({ + defaultValue: 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf', + }), + emsTileLayerId: schema.object({ + bright: schema.string({ defaultValue: 'road_map' }), + desaturated: schema.string({ defaultValue: 'road_map_desaturated' }), + dark: schema.string({ defaultValue: 'dark_map' }), + }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index d66be2b156bb9..cd503883164ac 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -2,5 +2,7 @@ "id": "mapsLegacy", "version": "8.0.0", "kibanaVersion": "kibana", - "ui": true + "configPath": ["map"], + "ui": true, + "server": true } diff --git a/src/plugins/maps_legacy/public/__tests__/map/kibana_map.js b/src/plugins/maps_legacy/public/__tests__/map/kibana_map.js index 83b5359362e4c..1002a8e9eedc8 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/__tests__/map/kibana_map.js @@ -20,7 +20,6 @@ import expect from '@kbn/expect'; import { KibanaMap } from '../../map/kibana_map'; import { KibanaMapLayer } from '../../map/kibana_map_layer'; -import L from 'leaflet'; describe('kibana_map tests', function() { let domNode; @@ -218,6 +217,7 @@ describe('kibana_map tests', function() { function makeMockLayer(attribution) { const layer = new KibanaMapLayer(); layer._attribution = attribution; + // eslint-disable-next-line no-undef layer._leafletLayer = L.geoJson(null); return layer; } diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index 17cecab9f7459..3fe377fbdc41f 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -17,12 +17,15 @@ * under the License. */ -import { CoreSetup } from 'kibana/public'; -import { bindSetupCoreAndPlugins, MapsLegacyPlugin } from './plugin'; // @ts-ignore -import * as colorUtil from './map/color_util'; +import { CoreSetup, PluginInitializerContext } from 'kibana/public'; +// @ts-ignore +import { L } from './leaflet'; // @ts-ignore import { KibanaMap } from './map/kibana_map'; +import { bindSetupCoreAndPlugins, MapsLegacyPlugin } from './plugin'; +// @ts-ignore +import * as colorUtil from './map/color_util'; // @ts-ignore import { KibanaMapLayer } from './map/kibana_map_layer'; // @ts-ignore @@ -41,8 +44,15 @@ import { // @ts-ignore import { mapTooltipProvider } from './tooltip_provider'; -export function plugin() { - return new MapsLegacyPlugin(); +export interface MapsLegacyConfigType { + emsTileLayerId: string; + includeElasticMapsService: boolean; + proxyElasticMapsServiceInMaps: boolean; + tilemap: any; +} + +export function plugin(initializerContext: PluginInitializerContext) { + return new MapsLegacyPlugin(initializerContext); } /** @public */ @@ -59,6 +69,7 @@ export { FileLayer, TmsLayer, mapTooltipProvider, + L, }; // Due to a leaflet/leaflet-draw bug, it's not possible to consume leaflet maps w/ draw control diff --git a/src/plugins/maps_legacy/public/leaflet.js b/src/plugins/maps_legacy/public/leaflet.js new file mode 100644 index 0000000000000..e36da2c52b8c5 --- /dev/null +++ b/src/plugins/maps_legacy/public/leaflet.js @@ -0,0 +1,36 @@ +/* + * 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. + */ + +export let L; + +if (!window.hasOwnProperty('L')) { + require('leaflet/dist/leaflet.css'); + window.L = require('leaflet/dist/leaflet.js'); + window.L.Browser.touch = false; + window.L.Browser.pointer = false; + + require('leaflet-vega'); + require('leaflet.heat/dist/leaflet-heat.js'); + require('leaflet-draw/dist/leaflet.draw.css'); + require('leaflet-draw/dist/leaflet.draw.js'); + require('leaflet-responsive-popup/leaflet.responsive.popup.css'); + require('leaflet-responsive-popup/leaflet.responsive.popup.js'); +} else { + L = window.L; +} diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index c7cec1b14159a..85dafc318db8d 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -19,15 +19,16 @@ import { EventEmitter } from 'events'; import { createZoomWarningMsg } from './map_messages'; -import L from 'leaflet'; import $ from 'jquery'; import _ from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../common/constants/origin'; import { getToasts } from '../kibana_services'; +import { L } from '../leaflet'; function makeFitControl(fitContainer, kibanaMap) { + // eslint-disable-next-line no-undef const FitControl = L.Control.extend({ options: { position: 'topleft', @@ -63,6 +64,7 @@ function makeFitControl(fitContainer, kibanaMap) { } function makeLegendControl(container, kibanaMap, position) { + // eslint-disable-next-line no-undef const LegendControl = L.Control.extend({ options: { position: 'topright', @@ -123,11 +125,13 @@ export class KibanaMap extends EventEmitter { maxZoom: options.maxZoom, center: options.center ? options.center : [0, 0], zoom: options.zoom ? options.zoom : 2, + // eslint-disable-next-line no-undef renderer: L.canvas(), zoomAnimation: false, // Desaturate map tiles causes animation rendering artifacts zoomControl: options.zoomControl === undefined ? true : options.zoomControl, }; + // eslint-disable-next-line no-undef this._leafletMap = L.map(containerNode, leafletOptions); this._leafletMap.attributionControl.setPrefix(''); @@ -228,10 +232,11 @@ export class KibanaMap extends EventEmitter { } if (!this._popup) { - this._popup = L.responsivePopup({ autoPan: false }); + // eslint-disable-next-line no-undef + this._popup = new L.ResponsivePopup({ autoPan: false }); this._popup.setLatLng(event.position); this._popup.setContent(event.content); - this._popup.openOn(this._leafletMap); + this._leafletMap.openPopup(this._popup); } else { if (!this._popup.getLatLng().equals(event.position)) { this._popup.setLatLng(event.position); @@ -335,6 +340,7 @@ export class KibanaMap extends EventEmitter { } setCenter(latitude, longitude) { + // eslint-disable-next-line no-undef const latLong = L.latLng(latitude, longitude); if (latLong.equals && !latLong.equals(this._leafletMap.getCenter())) { this._leafletMap.setView(latLong); @@ -461,6 +467,7 @@ export class KibanaMap extends EventEmitter { circlemarker: false, }, }; + // eslint-disable-next-line no-undef this._leafletDrawControl = new L.Control.Draw(drawOptions); this._leafletMap.addControl(this._leafletDrawControl); } @@ -470,6 +477,7 @@ export class KibanaMap extends EventEmitter { return; } + // eslint-disable-next-line no-undef const fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit'); this._leafletFitControl = makeFitControl(fitContainer, this); this._leafletMap.addControl(this._leafletFitControl); @@ -621,6 +629,7 @@ export class KibanaMap extends EventEmitter { } _getTMSBaseLayer(options) { + // eslint-disable-next-line no-undef return L.tileLayer(options.url, { minZoom: options.minZoom, maxZoom: options.maxZoom, @@ -640,7 +649,8 @@ export class KibanaMap extends EventEmitter { }; return typeof options.url === 'string' && options.url.length - ? L.tileLayer.wms(options.url, wmsOptions) + ? // eslint-disable-next-line no-undef + L.tileLayer.wms(options.url, wmsOptions) : null; } diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index 8e3a0648e99d4..437b78a3c3472 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -27,10 +27,10 @@ import { ORIGIN } from '../common/constants/origin'; const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; export class ServiceSettings { - constructor() { + constructor(mapConfig, tilemapsConfig) { const getInjectedVar = getInjectedVarFunc(); - this.mapConfig = getInjectedVar('mapConfig'); - this.tilemapsConfig = getInjectedVar('tilemapsConfig'); + this._mapConfig = mapConfig; + this._tilemapsConfig = tilemapsConfig; const kbnVersion = getInjectedVar('version'); this._showZoomMessage = true; @@ -38,9 +38,9 @@ export class ServiceSettings { language: i18n.getLocale(), appVersion: kbnVersion, appName: 'kibana', - fileApiUrl: this.mapConfig.emsFileApiUrl, - tileApiUrl: this.mapConfig.emsTileApiUrl, - landingPageUrl: this.mapConfig.emsLandingPageUrl, + fileApiUrl: this._mapConfig.emsFileApiUrl, + tileApiUrl: this._mapConfig.emsTileApiUrl, + landingPageUrl: this._mapConfig.emsLandingPageUrl, // Wrap to avoid errors passing window fetch fetchFunction: function(...args) { return fetch(...args); @@ -57,10 +57,10 @@ export class ServiceSettings { // TMS attribution const attributionFromConfig = _.escape( - markdownIt.render(this.tilemapsConfig.deprecated.config.options.attribution || '') + markdownIt.render(this._tilemapsConfig.deprecated.config.options.attribution || '') ); // TMS Options - this.tmsOptionsFromConfig = _.assign({}, this.tilemapsConfig.deprecated.config.options, { + this.tmsOptionsFromConfig = _.assign({}, this._tilemapsConfig.deprecated.config.options, { attribution: attributionFromConfig, }); } @@ -92,7 +92,7 @@ export class ServiceSettings { } async getFileLayers() { - if (!this.mapConfig.includeElasticMapsService) { + if (!this._mapConfig.includeElasticMapsService) { return []; } @@ -121,7 +121,7 @@ export class ServiceSettings { */ async getTMSServices() { let allServices = []; - if (this.tilemapsConfig.deprecated.isOverridden) { + if (this._tilemapsConfig.deprecated.isOverridden) { //use tilemap.* settings from yml const tmsService = _.cloneDeep(this.tmsOptionsFromConfig); tmsService.id = TMS_IN_YML_ID; @@ -129,11 +129,11 @@ export class ServiceSettings { allServices.push(tmsService); } - if (this.mapConfig.includeElasticMapsService) { + if (this._mapConfig.includeElasticMapsService) { const servicesFromManifest = await this._emsClient.getTMSServices(); const strippedServiceFromManifest = await Promise.all( servicesFromManifest - .filter(tmsService => tmsService.getId() === this.mapConfig.emsTileLayerId.bright) + .filter(tmsService => tmsService.getId() === this._mapConfig.emsTileLayerId.bright) .map(async tmsService => { //shim for compatibility return { @@ -173,7 +173,7 @@ export class ServiceSettings { async _getAttributesForEMSTMSLayer(isDesaturated, isDarkMode) { const tmsServices = await this._emsClient.getTMSServices(); - const emsTileLayerId = this.mapConfig.emsTileLayerId; + const emsTileLayerId = this._mapConfig.emsTileLayerId; let serviceId; if (isDarkMode) { serviceId = emsTileLayerId.dark; @@ -200,13 +200,13 @@ export class ServiceSettings { if (tmsServiceConfig.origin === ORIGIN.EMS) { return this._getAttributesForEMSTMSLayer(isDesaturated, isDarkMode); } else if (tmsServiceConfig.origin === ORIGIN.KIBANA_YML) { - const config = this.tilemapsConfig.deprecated.config; + const config = this._tilemapsConfig.deprecated.config; const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; } else { //this is an older config. need to resolve this dynamically. if (tmsServiceConfig.id === TMS_IN_YML_ID) { - const config = this.tilemapsConfig.deprecated.config; + const config = this._tilemapsConfig.deprecated.config; const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; } else { diff --git a/src/plugins/maps_legacy/public/plugin.ts b/src/plugins/maps_legacy/public/plugin.ts index acc7655a5e263..78c2498b9ee90 100644 --- a/src/plugins/maps_legacy/public/plugin.ts +++ b/src/plugins/maps_legacy/public/plugin.ts @@ -18,14 +18,15 @@ */ // @ts-ignore -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; // @ts-ignore import { setToasts, setUiSettings, setInjectedVarFunc } from './kibana_services'; // @ts-ignore import { ServiceSettings } from './map/service_settings'; // @ts-ignore import { getPrecision, getZoomPrecision } from './map/precision'; -import { MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; +import { MapsLegacyConfigType, MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; +import { ConfigSchema } from '../config'; /** * These are the interfaces with your public contracts. You should export these @@ -45,13 +46,22 @@ export interface MapsLegacySetupDependencies {} export interface MapsLegacyStartDependencies {} export class MapsLegacyPlugin implements Plugin { + readonly _initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this._initializerContext = initializerContext; + } + public setup(core: CoreSetup, plugins: MapsLegacySetupDependencies) { bindSetupCoreAndPlugins(core); + const config = this._initializerContext.config.get(); + return { - serviceSettings: new ServiceSettings(), + serviceSettings: new ServiceSettings(config, config.tilemap), getZoomPrecision, getPrecision, + config, }; } diff --git a/src/legacy/core_plugins/tile_map/public/legacy.ts b/src/plugins/maps_legacy/server/index.ts similarity index 54% rename from src/legacy/core_plugins/tile_map/public/legacy.ts rename to src/plugins/maps_legacy/server/index.ts index dd8d4c6e9311e..18f58189fc607 100644 --- a/src/legacy/core_plugins/tile_map/public/legacy.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -17,19 +17,33 @@ * under the License. */ +import { PluginConfigDescriptor } from 'kibana/server'; import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; +import { configSchema, ConfigSchema } from '../config'; -import { TileMapPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - mapsLegacy: npSetup.plugins.mapsLegacy, +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + includeElasticMapsService: true, + proxyElasticMapsServiceInMaps: true, + tilemap: true, + regionmap: true, + manifestServiceUrl: true, + emsFileApiUrl: true, + emsTileApiUrl: true, + emsLandingPageUrl: true, + emsFontLibraryUrl: true, + emsTileLayerId: true, + }, + schema: configSchema, }; -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core); +export const plugin = (initializerContext: PluginInitializerContext) => ({ + setup() { + // @ts-ignore + const config$ = initializerContext.config.create(); + return { + config: config$, + }; + }, + start() {}, +}); diff --git a/src/plugins/tile_map/config.ts b/src/plugins/tile_map/config.ts new file mode 100644 index 0000000000000..435e52103d156 --- /dev/null +++ b/src/plugins/tile_map/config.ts @@ -0,0 +1,47 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + url: schema.maybe(schema.string()), + deprecated: schema.any({ + defaultValue: { + config: { + options: { + attribution: '', + }, + }, + }, + }), + options: schema.object({ + attribution: schema.string({ defaultValue: '' }), + minZoom: schema.number({ defaultValue: 0, min: 0 }), + maxZoom: schema.number({ defaultValue: 10 }), + tileSize: schema.maybe(schema.number()), + subdomains: schema.maybe(schema.arrayOf(schema.string())), + errorTileUrl: schema.maybe(schema.string()), + tms: schema.maybe(schema.boolean()), + reuseTiles: schema.maybe(schema.boolean()), + bounds: schema.maybe(schema.arrayOf(schema.number({ min: 2 }))), + default: schema.maybe(schema.boolean()), + }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/tile_map/kibana.json b/src/plugins/tile_map/kibana.json new file mode 100644 index 0000000000000..71ae0bb29d17f --- /dev/null +++ b/src/plugins/tile_map/kibana.json @@ -0,0 +1,14 @@ +{ + "id": "tileMap", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["map", "tilemap"], + "ui": true, + "server": true, + "requiredPlugins": [ + "visualizations", + "expressions", + "mapsLegacy", + "data" + ] +} diff --git a/src/legacy/core_plugins/tile_map/package.json b/src/plugins/tile_map/package.json similarity index 100% rename from src/legacy/core_plugins/tile_map/package.json rename to src/plugins/tile_map/package.json diff --git a/src/legacy/core_plugins/tile_map/public/__snapshots__/tilemap_fn.test.js.snap b/src/plugins/tile_map/public/__snapshots__/tilemap_fn.test.js.snap similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__snapshots__/tilemap_fn.test.js.snap rename to src/plugins/tile_map/public/__snapshots__/tilemap_fn.test.js.snap diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/blues.png b/src/plugins/tile_map/public/__tests__/blues.png similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__tests__/blues.png rename to src/plugins/tile_map/public/__tests__/blues.png diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js similarity index 85% rename from src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js rename to src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index bce2e157ebbc8..303ce67be7102 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -23,37 +23,37 @@ import { ImageComparator } from 'test_utils/image_comparator'; import dummyESResponse from './dummy_es_response.json'; import initial from './initial.png'; import blues from './blues.png'; -import shadedGeohashGrid from './shadedGeohashGrid.png'; +import shadedGeohashGrid from './shaded_geohash_grid.png'; import heatmapRaw from './heatmap_raw.png'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import EMS_CATALOGUE from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_manifest.json'; +import EMS_CATALOGUE from '../../../maps_legacy/public/__tests__/map/ems_mocks/sample_manifest.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import EMS_FILES from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json'; +import EMS_FILES from '../../../maps_legacy/public/__tests__/map/ems_mocks/sample_files.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import EMS_TILES from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json'; +import EMS_TILES from '../../../maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_bright'; +import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../maps_legacy/public/__tests__/map/ems_mocks/sample_style_bright'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_desaturated'; +import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../maps_legacy/public/__tests__/map/ems_mocks/sample_style_desaturated'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import EMS_STYLE_DARK_MAP from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_dark'; +import EMS_STYLE_DARK_MAP from '../../../maps_legacy/public/__tests__/map/ems_mocks/sample_style_dark'; import { createTileMapVisualization } from '../tile_map_visualization'; import { createTileMapTypeDefinition } from '../tile_map_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExprVis } from '../../../../../plugins/visualizations/public/expressions/vis'; +import { ExprVis } from '../../../visualizations/public/expressions/vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_types/base_vis_type'; +import { BaseVisType } from '../../../visualizations/public/vis_types/base_vis_type'; import { getPrecision, getZoomPrecision, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps_legacy/public/map/precision'; +} from '../../../maps_legacy/public/map/precision'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; +import { ServiceSettings } from '../../../maps_legacy/public/map/service_settings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; -import { getBaseMapsVis } from '../../../../../plugins/maps_legacy/public'; +import { setInjectedVarFunc } from '../../../maps_legacy/public/kibana_services'; +import { getBaseMapsVis } from '../../../maps_legacy/public'; function mockRawData() { const stack = [dummyESResponse]; @@ -91,24 +91,22 @@ describe('CoordinateMapsVisualizationTest', function() { beforeEach(ngMock.module('kibana')); beforeEach( ngMock.inject((Private, $injector) => { + const mapConfig = { + emsFileApiUrl: '', + emsTileApiUrl: '', + emsLandingPageUrl: '', + }; + const tilemapsConfig = { + deprecated: { + config: { + options: { + attribution: '123', + }, + }, + }, + }; setInjectedVarFunc(injectedVar => { switch (injectedVar) { - case 'mapConfig': - return { - emsFileApiUrl: '', - emsTileApiUrl: '', - emsLandingPageUrl: '', - }; - case 'tilemapsConfig': - return { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, - }, - }; case 'version': return '123'; default: @@ -125,7 +123,7 @@ describe('CoordinateMapsVisualizationTest', function() { getInjectedVar: () => {}, }, }; - const serviceSettings = new ServiceSettings(); + const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); const uiSettings = $injector.get('config'); diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/dummy_es_response.json b/src/plugins/tile_map/public/__tests__/dummy_es_response.json similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__tests__/dummy_es_response.json rename to src/plugins/tile_map/public/__tests__/dummy_es_response.json diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js b/src/plugins/tile_map/public/__tests__/geohash_layer.js similarity index 96% rename from src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js rename to src/plugins/tile_map/public/__tests__/geohash_layer.js index bdf9cd806eb8b..a288e78ef00c1 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js +++ b/src/plugins/tile_map/public/__tests__/geohash_layer.js @@ -20,12 +20,12 @@ import expect from '@kbn/expect'; import { GeohashLayer } from '../geohash_layer'; // import heatmapPng from './heatmap.png'; -import scaledCircleMarkersPng from './scaledCircleMarkers.png'; +import scaledCircleMarkersPng from './scaled_circle_markers.png'; // import shadedCircleMarkersPng from './shadedCircleMarkers.png'; import { ImageComparator } from 'test_utils/image_comparator'; import GeoHashSampleData from './dummy_es_response.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { KibanaMap } from '../../../../../plugins/maps_legacy/public/map/kibana_map'; +import { KibanaMap } from '../../../maps_legacy/public/map/kibana_map'; describe('geohash_layer', function() { let domNode; diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/heatmap.png b/src/plugins/tile_map/public/__tests__/heatmap.png similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__tests__/heatmap.png rename to src/plugins/tile_map/public/__tests__/heatmap.png diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/heatmap_raw.png b/src/plugins/tile_map/public/__tests__/heatmap_raw.png similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__tests__/heatmap_raw.png rename to src/plugins/tile_map/public/__tests__/heatmap_raw.png diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/initial.png b/src/plugins/tile_map/public/__tests__/initial.png similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__tests__/initial.png rename to src/plugins/tile_map/public/__tests__/initial.png diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png b/src/plugins/tile_map/public/__tests__/scaled_circle_markers.png similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png rename to src/plugins/tile_map/public/__tests__/scaled_circle_markers.png diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png b/src/plugins/tile_map/public/__tests__/shaded_circle_markers.png similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png rename to src/plugins/tile_map/public/__tests__/shaded_circle_markers.png diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png b/src/plugins/tile_map/public/__tests__/shaded_geohash_grid.png similarity index 100% rename from src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png rename to src/plugins/tile_map/public/__tests__/shaded_geohash_grid.png diff --git a/src/legacy/core_plugins/tile_map/public/_tile_map.scss b/src/plugins/tile_map/public/_tile_map.scss similarity index 100% rename from src/legacy/core_plugins/tile_map/public/_tile_map.scss rename to src/plugins/tile_map/public/_tile_map.scss diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/plugins/tile_map/public/components/tile_map_options.tsx similarity index 95% rename from src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx rename to src/plugins/tile_map/public/components/tile_map_options.tsx index 1efb0b2f884f8..f7fb4daff63f0 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/plugins/tile_map/public/components/tile_map_options.tsx @@ -22,13 +22,8 @@ import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { - BasicOptions, - RangeOption, - SelectOption, - SwitchOption, -} from '../../../../../plugins/charts/public'; -import { WmsOptions, TileMapVisParams, MapTypes } from '../../../../../plugins/maps_legacy/public'; +import { BasicOptions, RangeOption, SelectOption, SwitchOption } from '../../../charts/public'; +import { WmsOptions, TileMapVisParams, MapTypes } from '../../../maps_legacy/public'; export type TileMapOptionsProps = VisOptionsProps; diff --git a/src/legacy/core_plugins/tile_map/public/css_filters.js b/src/plugins/tile_map/public/css_filters.js similarity index 100% rename from src/legacy/core_plugins/tile_map/public/css_filters.js rename to src/plugins/tile_map/public/css_filters.js diff --git a/src/legacy/core_plugins/tile_map/public/geohash_layer.js b/src/plugins/tile_map/public/geohash_layer.js similarity index 98% rename from src/legacy/core_plugins/tile_map/public/geohash_layer.js rename to src/plugins/tile_map/public/geohash_layer.js index f0261483d302d..dbe64871265b1 100644 --- a/src/legacy/core_plugins/tile_map/public/geohash_layer.js +++ b/src/plugins/tile_map/public/geohash_layer.js @@ -17,10 +17,9 @@ * under the License. */ -import L from 'leaflet'; import { min, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaMapLayer, MapTypes } from '../../../../plugins/maps_legacy/public'; +import { L, KibanaMapLayer, MapTypes } from '../../maps_legacy/public'; import { HeatmapMarkers } from './markers/heatmap'; import { ScaledCirclesMarkers } from './markers/scaled_circles'; import { ShadedCirclesMarkers } from './markers/shaded_circles'; diff --git a/src/legacy/core_plugins/tile_map/public/index.scss b/src/plugins/tile_map/public/index.scss similarity index 90% rename from src/legacy/core_plugins/tile_map/public/index.scss rename to src/plugins/tile_map/public/index.scss index 767a71225a7d8..4ce500b2da4d2 100644 --- a/src/legacy/core_plugins/tile_map/public/index.scss +++ b/src/plugins/tile_map/public/index.scss @@ -7,4 +7,4 @@ // tlmChart__legend--small // tlmChart__legend-isLoading -@import './tile_map'; +@import 'tile_map'; diff --git a/src/legacy/core_plugins/tile_map/public/index.ts b/src/plugins/tile_map/public/index.ts similarity index 93% rename from src/legacy/core_plugins/tile_map/public/index.ts rename to src/plugins/tile_map/public/index.ts index 3d0d970e4dc20..d2b9a15a6ad3c 100644 --- a/src/legacy/core_plugins/tile_map/public/index.ts +++ b/src/plugins/tile_map/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializerContext } from 'kibana/public'; import { TileMapPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/legacy/core_plugins/tile_map/public/markers/geohash_grid.js b/src/plugins/tile_map/public/markers/geohash_grid.js similarity index 96% rename from src/legacy/core_plugins/tile_map/public/markers/geohash_grid.js rename to src/plugins/tile_map/public/markers/geohash_grid.js index 406a50ccde966..0150f6d2c54c9 100644 --- a/src/legacy/core_plugins/tile_map/public/markers/geohash_grid.js +++ b/src/plugins/tile_map/public/markers/geohash_grid.js @@ -17,8 +17,8 @@ * under the License. */ -import L from 'leaflet'; import { ScaledCirclesMarkers } from './scaled_circles'; +import { L } from '../../../maps_legacy/public'; export class GeohashGridMarkers extends ScaledCirclesMarkers { getMarkerFunction() { diff --git a/src/legacy/core_plugins/tile_map/public/markers/heatmap.js b/src/plugins/tile_map/public/markers/heatmap.js similarity index 98% rename from src/legacy/core_plugins/tile_map/public/markers/heatmap.js rename to src/plugins/tile_map/public/markers/heatmap.js index 0ae26bfcf032b..ed9dbccbfbcde 100644 --- a/src/legacy/core_plugins/tile_map/public/markers/heatmap.js +++ b/src/plugins/tile_map/public/markers/heatmap.js @@ -17,10 +17,10 @@ * under the License. */ -import L from 'leaflet'; import _ from 'lodash'; import d3 from 'd3'; import { EventEmitter } from 'events'; +import { L } from '../../../maps_legacy/public'; /** * Map overlay: canvas layer with leaflet.heat plugin @@ -34,7 +34,7 @@ export class HeatmapMarkers extends EventEmitter { super(); this._geojsonFeatureCollection = featureCollection; const points = dataToHeatArray(featureCollection, max); - this._leafletLayer = L.heatLayer(points, options); + this._leafletLayer = new L.HeatLayer(points, options); this._tooltipFormatter = options.tooltipFormatter; this._zoom = zoom; this._disableTooltips = false; diff --git a/src/legacy/core_plugins/tile_map/public/markers/scaled_circles.js b/src/plugins/tile_map/public/markers/scaled_circles.js similarity index 97% rename from src/legacy/core_plugins/tile_map/public/markers/scaled_circles.js rename to src/plugins/tile_map/public/markers/scaled_circles.js index f39de6ca7d179..028d3de515ae7 100644 --- a/src/legacy/core_plugins/tile_map/public/markers/scaled_circles.js +++ b/src/plugins/tile_map/public/markers/scaled_circles.js @@ -17,13 +17,12 @@ * under the License. */ -import L from 'leaflet'; import _ from 'lodash'; import d3 from 'd3'; import $ from 'jquery'; import { EventEmitter } from 'events'; -import { colorUtil } from '../../../../../plugins/maps_legacy/public'; -import { truncatedColorMaps } from '../../../../../plugins/charts/public'; +import { L, colorUtil } from '../../../maps_legacy/public'; +import { truncatedColorMaps } from '../../../charts/public'; export class ScaledCirclesMarkers extends EventEmitter { constructor( diff --git a/src/legacy/core_plugins/tile_map/public/markers/shaded_circles.js b/src/plugins/tile_map/public/markers/shaded_circles.js similarity index 97% rename from src/legacy/core_plugins/tile_map/public/markers/shaded_circles.js rename to src/plugins/tile_map/public/markers/shaded_circles.js index e21d753f7001a..745d0422856c6 100644 --- a/src/legacy/core_plugins/tile_map/public/markers/shaded_circles.js +++ b/src/plugins/tile_map/public/markers/shaded_circles.js @@ -17,9 +17,9 @@ * under the License. */ -import L from 'leaflet'; import _ from 'lodash'; import { ScaledCirclesMarkers } from './scaled_circles'; +import { L } from '../../../maps_legacy/public'; export class ShadedCirclesMarkers extends ScaledCirclesMarkers { getMarkerFunction() { diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts similarity index 68% rename from src/legacy/core_plugins/tile_map/public/plugin.ts rename to src/plugins/tile_map/public/plugin.ts index aa1460a7e2890..e55f7189929df 100644 --- a/src/legacy/core_plugins/tile_map/public/plugin.ts +++ b/src/plugins/tile_map/public/plugin.ts @@ -22,9 +22,9 @@ import { Plugin, PluginInitializerContext, IUiSettingsClient, -} from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; +} from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; // TODO: Determine why visualizations don't populate without this import 'angular-sanitize'; @@ -32,7 +32,13 @@ import 'angular-sanitize'; import { createTileMapFn } from './tile_map_fn'; // @ts-ignore import { createTileMapTypeDefinition } from './tile_map_type'; -import { getBaseMapsVis, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; +import { getBaseMapsVis, MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { setFormatService, setQueryService } from './services'; + +export interface TileMapConfigType { + tilemap: any; +} /** @private */ interface TileMapVisualizationDependencies { @@ -50,7 +56,18 @@ export interface TileMapPluginSetupDependencies { } /** @internal */ -export class TileMapPlugin implements Plugin, void> { +export interface TileMapPluginStartDependencies { + data: DataPublicPluginStart; +} + +export interface TileMapPluginSetup { + config: any; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TileMapPluginStart {} + +/** @internal */ +export class TileMapPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { @@ -72,9 +89,16 @@ export class TileMapPlugin implements Plugin, void> { expressions.registerFunction(() => createTileMapFn(visualizationDependencies)); visualizations.createBaseVisualization(createTileMapTypeDefinition(visualizationDependencies)); + + const config = this.initializerContext.config.get(); + return { + config, + }; } - public start(core: CoreStart) { - // nothing to do here yet + public start(core: CoreStart, { data }: TileMapPluginStartDependencies) { + setFormatService(data.fieldFormats); + setQueryService(data.query); + return {}; } } diff --git a/test/plugin_functional/plugins/core_provider_plugin/index.ts b/src/plugins/tile_map/public/services.ts similarity index 62% rename from test/plugin_functional/plugins/core_provider_plugin/index.ts rename to src/plugins/tile_map/public/services.ts index 01f3a67c6b554..fd075a041ac9b 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/index.ts +++ b/src/plugins/tile_map/public/services.ts @@ -17,20 +17,13 @@ * under the License. */ -import { resolve } from 'path'; -import { Legacy } from '../../../../kibana'; +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; -// eslint-disable-next-line import/no-default-export -export default function CoreProviderPlugin(kibana: any) { - const config: Legacy.PluginSpecOptions = { - id: 'core-provider', - require: [], - publicDir: resolve(__dirname, 'public'), - init: (server: Legacy.Server) => ({}), - uiExports: { - hacks: [resolve(__dirname, 'public/index')], - }, - }; +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('vislib data.fieldFormats'); - return new kibana.Plugin(config); -} +export const [getQueryService, setQueryService] = createGetterSetter< + DataPublicPluginStart['query'] +>('Query'); diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_fn.js b/src/plugins/tile_map/public/tile_map_fn.js similarity index 95% rename from src/legacy/core_plugins/tile_map/public/tile_map_fn.js rename to src/plugins/tile_map/public/tile_map_fn.js index 5ad4a2c33db25..5f43077bcb24b 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_fn.js +++ b/src/plugins/tile_map/public/tile_map_fn.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { convertToGeoJson } from '../../../../plugins/maps_legacy/public'; +import { convertToGeoJson } from '../../maps_legacy/public'; import { i18n } from '@kbn/i18n'; export const createTileMapFn = () => ({ diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/plugins/tile_map/public/tile_map_type.js similarity index 95% rename from src/legacy/core_plugins/tile_map/public/tile_map_type.js rename to src/plugins/tile_map/public/tile_map_type.js index ca6a586d22008..aa0160f3f5a9d 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/plugins/tile_map/public/tile_map_type.js @@ -19,12 +19,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { convertToGeoJson, MapTypes } from '../../../../plugins/maps_legacy/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { convertToGeoJson, MapTypes } from '../../maps_legacy/public'; +import { Schemas } from '../../vis_default_editor/public'; import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; import { supportsCssFilters } from './css_filters'; -import { truncatedColorSchemas } from '../../../../plugins/charts/public'; +import { truncatedColorSchemas } from '../../charts/public'; export function createTileMapTypeDefinition(dependencies) { const CoordinateMapsVisualization = createTileMapVisualization(dependencies); diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js b/src/plugins/tile_map/public/tile_map_visualization.js similarity index 95% rename from src/legacy/core_plugins/tile_map/public/tile_map_visualization.js rename to src/plugins/tile_map/public/tile_map_visualization.js index 6a7bda5e18883..f96c7291b34cf 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js +++ b/src/plugins/tile_map/public/tile_map_visualization.js @@ -19,13 +19,8 @@ import { get } from 'lodash'; import { GeohashLayer } from './geohash_layer'; -import { npStart } from 'ui/new_platform'; -import { getFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; -import { - scaleBounds, - geoContains, - mapTooltipProvider, -} from '../../../../plugins/maps_legacy/public'; +import { getFormatService, getQueryService } from './services'; +import { scaleBounds, geoContains, mapTooltipProvider } from '../../maps_legacy/public'; import { tooltipFormatter } from './tooltip_formatter'; export const createTileMapVisualization = dependencies => { @@ -183,7 +178,9 @@ export const createTileMapVisualization = dependencies => { const newParams = this._getMapsParams(); const metricDimension = this._params.dimensions.metric; const metricLabel = metricDimension ? metricDimension.label : ''; - const metricFormat = getFormat(metricDimension && metricDimension.format); + const metricFormat = getFormatService().deserialize( + metricDimension && metricDimension.format + ); return { label: metricLabel, @@ -213,7 +210,7 @@ export const createTileMapVisualization = dependencies => { filter[filterName] = { ignore_unmapped: true }; filter[filterName][field] = filterData; - const { filterManager } = npStart.plugins.data.query; + const { filterManager } = getQueryService(); filterManager.addFilters([filter]); this.vis.updateState(); diff --git a/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js b/src/plugins/tile_map/public/tilemap_fn.test.js similarity index 90% rename from src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js rename to src/plugins/tile_map/public/tilemap_fn.test.js index 6da37f4c5ef86..8fa12c9f9dbbe 100644 --- a/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js +++ b/src/plugins/tile_map/public/tilemap_fn.test.js @@ -18,11 +18,10 @@ */ // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; import { createTileMapFn } from './tile_map_fn'; -jest.mock('ui/new_platform'); -jest.mock('../../../../plugins/maps_legacy/public', () => ({ +jest.mock('../../maps_legacy/public', () => ({ convertToGeoJson: jest.fn().mockReturnValue({ featureCollection: { type: 'FeatureCollection', @@ -37,7 +36,7 @@ jest.mock('../../../../plugins/maps_legacy/public', () => ({ }), })); -import { convertToGeoJson } from '../../../../plugins/maps_legacy/public'; +import { convertToGeoJson } from '../../maps_legacy/public'; describe('interpreter/functions#tilemap', () => { const fn = functionWrapper(createTileMapFn()); diff --git a/src/legacy/core_plugins/tile_map/public/tooltip_formatter.js b/src/plugins/tile_map/public/tooltip_formatter.js similarity index 100% rename from src/legacy/core_plugins/tile_map/public/tooltip_formatter.js rename to src/plugins/tile_map/public/tooltip_formatter.js diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts b/src/plugins/tile_map/server/index.ts similarity index 70% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts rename to src/plugins/tile_map/server/index.ts index d7a764b581c01..3381553fe9364 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts +++ b/src/plugins/tile_map/server/index.ts @@ -17,12 +17,19 @@ * under the License. */ -import { PluginInitializer, PluginInitializerContext } from 'src/core/public'; -import { Plugin, StartDeps } from './plugin'; -export { StartDeps }; +import { PluginConfigDescriptor } from 'kibana/server'; +import { configSchema, ConfigSchema } from '../config'; -export const plugin: PluginInitializer = ( - initializerContext: PluginInitializerContext -) => { - return new Plugin(initializerContext); +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + url: true, + deprecated: true, + options: true, + }, + schema: configSchema, }; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index b52dcfbd914f9..1bce7ac92e564 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -27,6 +27,7 @@ import { setInjectedVars, setUISettings, setKibanaMapFactory, + setMapsLegacyConfig, } from './services'; import { createVegaFn } from './vega_fn'; @@ -76,6 +77,7 @@ export class VegaPlugin implements Plugin, void> { }); setUISettings(core.uiSettings); setKibanaMapFactory(getKibanaMapFactoryProvider(core)); + setMapsLegacyConfig(mapsLegacy.config); const visualizationDependencies: Readonly = { core, diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index f81f87d7ad2e1..f2fddb41cf72b 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -21,6 +21,7 @@ import { SavedObjectsStart } from 'kibana/public'; import { NotificationsStart, IUiSettingsClient } from 'src/core/public'; import { DataPublicPluginStart } from '../../data/public'; import { createGetterSetter } from '../../kibana_utils/public'; +import { MapsLegacyConfigType } from '../../maps_legacy/public'; export const [getData, setData] = createGetterSetter('Data'); @@ -43,6 +44,10 @@ export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ emsTileLayerId: unknown; }>('InjectedVars'); +export const [getMapsLegacyConfig, setMapsLegacyConfig] = createGetterSetter( + 'MapsLegacyConfig' +); + export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; -export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; +export const getEmsTileLayerId = () => getMapsLegacyConfig().emsTileLayerId; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js index 8e4009eab8488..bc1cb4e4734c7 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js @@ -17,9 +17,7 @@ * under the License. */ -import L from 'leaflet'; -import 'leaflet-vega'; -import { KibanaMapLayer } from '../../../maps_legacy/public'; +import { KibanaMapLayer, L } from '../../../maps_legacy/public'; export class VegaMapLayer extends KibanaMapLayer { constructor(spec, options) { @@ -28,7 +26,6 @@ export class VegaMapLayer extends KibanaMapLayer { // Used by super.getAttributions() this._attribution = options.attribution; delete options.attribution; - this._leafletLayer = L.vega(spec, options); } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 895d496a896aa..4cd3eea503cb0 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -102,6 +102,7 @@ export class VegaMapView extends VegaBaseView { // let maxBounds = null; // if (mapConfig.maxBounds) { // const b = mapConfig.maxBounds; + // eslint-disable-next-line no-undef // maxBounds = L.latLngBounds(L.latLng(b[1], b[0]), L.latLng(b[3], b[2])); // } diff --git a/test/functional/apps/timelion/index.js b/test/functional/apps/timelion/index.js index 3b5167addf4e6..021fa24397850 100644 --- a/test/functional/apps/timelion/index.js +++ b/test/functional/apps/timelion/index.js @@ -28,7 +28,7 @@ export default function({ getService, loadTestFile }) { before(async function() { log.debug('Starting timelion before method'); - browser.setWindowSize(1280, 800); + await browser.setWindowSize(1280, 800); await esArchiver.loadIfNeeded('logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); }); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 81d22838d1e8b..b7a6e10efd7dc 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -33,7 +33,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider class SettingsPage { async clickNavigation() { - find.clickDisplayedByCssSelector('.app-link:nth-child(5) a'); + await find.clickDisplayedByCssSelector('.app-link:nth-child(5) a'); } async clickLinkText(text: string) { @@ -110,7 +110,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } async toggleAdvancedSettingCheckbox(propertyName: string) { - testSubjects.click(`advancedSetting-editField-${propertyName}`); + await testSubjects.click(`advancedSetting-editField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click(`advancedSetting-saveButton`); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/services/find.ts b/test/functional/services/find.ts index 312668b718dc0..bdcc5ba95e9fb 100644 --- a/test/functional/services/find.ts +++ b/test/functional/services/find.ts @@ -476,7 +476,7 @@ export async function FindProvider({ getService }: FtrProviderContext) { value: string ): Promise { log.debug(`Find.waitForAttributeToChange('${selector}', '${attribute}', '${value}')`); - retry.waitFor(`${attribute} to equal "${value}"`, async () => { + await retry.waitFor(`${attribute} to equal "${value}"`, async () => { const el = await this.byCssSelector(selector); return value === (await el.getAttribute(attribute)); }); diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 1b7ef2c1855d0..df79db50b8683 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -17,7 +17,8 @@ * under the License. */ -import { delimiter } from 'path'; +import { delimiter, resolve } from 'path'; +import Fs from 'fs'; import * as Rx from 'rxjs'; import { mergeMap, map, takeUntil } from 'rxjs/operators'; @@ -37,6 +38,7 @@ import { Executor } from 'selenium-webdriver/lib/http'; import { getLogger } from 'selenium-webdriver/lib/logging'; import { installDriver } from 'ms-chromium-edge-driver'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { pollForLogEntry$ } from './poll_for_log_entry'; import { createStdoutSocket } from './create_stdout_stream'; import { preventParallelCalls } from './prevent_parallel_calls'; @@ -50,6 +52,13 @@ const certValidation: string = process.env.NODE_TLS_REJECT_UNAUTHORIZED as strin const SECOND = 1000; const MINUTE = 60 * SECOND; const NO_QUEUE_COMMANDS = ['getLog', 'getStatus', 'newSession', 'quit']; +const downloadDir = resolve(REPO_ROOT, 'target/functional-tests/downloads'); +const chromiumDownloadPrefs = { + prefs: { + 'download.default_directory': downloadDir, + 'download.prompt_for_download': false, + }, +}; /** * Best we can tell WebDriver locks up sometimes when we send too many @@ -112,6 +121,7 @@ async function attemptToCreateCommand( chromeCapabilities.set('goog:chromeOptions', { w3c: true, args: chromeOptions, + ...chromiumDownloadPrefs, }); chromeCapabilities.set('unexpectedAlertBehaviour', 'accept'); chromeCapabilities.set('goog:loggingPrefs', { browser: 'ALL' }); @@ -150,6 +160,10 @@ async function attemptToCreateCommand( edgeOptions.setEdgeChromium(true); // @ts-ignore internal modules are not typed edgeOptions.setBinaryPath(edgePaths.browserPath); + const options = edgeOptions.get('ms:edgeOptions'); + // overriding options to include preferences + Object.assign(options, chromiumDownloadPrefs); + edgeOptions.set('ms:edgeOptions', options); const session = await new Builder() .forBrowser('MicrosoftEdge') .setEdgeOptions(edgeOptions) @@ -185,6 +199,14 @@ async function attemptToCreateCommand( firefoxOptions.set('moz:firefoxOptions', { prefs: { 'devtools.console.stdout.content': true }, }); + firefoxOptions.setPreference('browser.download.folderList', 2); + firefoxOptions.setPreference('browser.download.manager.showWhenStarting', false); + firefoxOptions.setPreference('browser.download.dir', downloadDir); + firefoxOptions.setPreference( + 'browser.helperApps.neverAsk.saveToDisk', + 'application/comma-separated-values, text/csv, text/plain' + ); + if (headlessBrowser === '1') { // See: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode firefoxOptions.headless(); @@ -308,6 +330,9 @@ export async function initWebDriver( log.verbose(entry.message); }); + // create browser download folder + Fs.mkdirSync(downloadDir, { recursive: true }); + // download Edge driver only in case of usage if (browserType === Browsers.ChromiumEdge) { edgePaths = await installDriver(); diff --git a/test/interpreter_functional/config.ts b/test/interpreter_functional/config.ts index 0fe7df4d50715..d3cfcea9823e9 100644 --- a/test/interpreter_functional/config.ts +++ b/test/interpreter_functional/config.ts @@ -50,6 +50,9 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), + + // Required to load new platform plugins via `--plugin-path` flag. + '--env.name=development', ...plugins.map( pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts deleted file mode 100644 index 1d5564ec06e4e..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 { Legacy } from 'kibana'; -import { - ArrayOrItem, - LegacyPluginApi, - LegacyPluginSpec, - LegacyPluginOptions, -} from 'src/legacy/plugin_discovery/types'; - -// eslint-disable-next-line import/no-default-export -export default function(kibana: LegacyPluginApi): ArrayOrItem { - const pluginSpec: Partial = { - id: 'kbn_tp_run_pipeline', - uiExports: { - app: { - title: 'Run Pipeline', - description: 'This is a sample plugin to test running pipeline expressions', - main: 'plugins/kbn_tp_run_pipeline/legacy', - }, - }, - - init(server: Legacy.Server) { - // The following lines copy over some configuration variables from Kibana - // to this plugin. This will be needed when embedding visualizations, so that e.g. - // region map is able to get its configuration. - server.injectUiAppVars('kbn_tp_run_pipeline', async () => { - return server.getInjectedUiAppVars('kibana'); - }); - }, - }; - return new kibana.Plugin(pluginSpec); -} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json new file mode 100644 index 0000000000000..f0c1c3a34fbc0 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json @@ -0,0 +1,13 @@ +{ + "id": "kbn_tp_run_pipeline", + "version": "0.0.1", + "kibanaVersion": "kibana", + "requiredPlugins": [ + "data", + "savedObjects", + "kibanaUtils", + "expressions" + ], + "server": false, + "ui": true +} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 338e85038922d..ebc74be937ef0 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -1,6 +1,7 @@ { "name": "kbn_tp_run_pipeline", "version": "1.0.0", + "main": "target/test/interpreter_functional/plugins/kbn_tp_run_pipeline", "kibana": { "version": "kibana", "templateVersion": "1.0.0" @@ -10,5 +11,13 @@ "@elastic/eui": "22.3.1", "react": "^16.12.0", "react-dom": "^16.12.0" + }, + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "@kbn/plugin-helpers": "9.0.2", + "typescript": "3.7.2" } } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/app.tsx similarity index 100% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/app.tsx diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx similarity index 99% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx index a50248a5b6fa3..ace2af2b4f0cf 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui'; import { first } from 'rxjs/operators'; import { IInterpreterRenderHandlers, ExpressionValue } from 'src/plugins/expressions'; -import { RequestAdapter, DataAdapter } from '../../../../../../../../src/plugins/inspector'; +import { RequestAdapter, DataAdapter } from '../../../../../../../src/plugins/inspector'; import { Adapters, ExpressionRenderHandler } from '../../types'; import { getExpressions } from '../../services'; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts index c4cc7175d6157..d7a764b581c01 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts @@ -17,4 +17,12 @@ * under the License. */ -export * from './np_ready'; +import { PluginInitializer, PluginInitializerContext } from 'src/core/public'; +import { Plugin, StartDeps } from './plugin'; +export { StartDeps }; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => { + return new Plugin(initializerContext); +}; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts deleted file mode 100644 index a7cd313038d69..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { PluginInitializerContext } from 'src/core/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { plugin } from './np_ready'; - -// This is required so some default styles and required scripts/Angular modules are loaded, -// or the timezone setting is correctly applied. -import 'ui/autoload/all'; -// Used to run esaggs queries -import 'uiExports/fieldFormats'; -import 'uiExports/search'; -// Used for kibana_context function - -import 'uiExports/savedObjectTypes'; -import 'uiExports/interpreter'; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins); -export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/plugin.ts similarity index 100% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/plugin.ts diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/services.ts similarity index 91% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/services.ts index a700727d87299..4972911d5894f 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public'; +import { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public'; import { ExpressionsStart } from './types'; export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/types.ts similarity index 100% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/types.ts diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json new file mode 100644 index 0000000000000..5fcaeafbb0d85 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.ts b/test/interpreter_functional/test_suites/run_pipeline/basic.ts index a2172dd2da1ba..51ad789143c54 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.ts @@ -113,10 +113,11 @@ export default function({ await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot() ).toMatchScreenshot(); - const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; - await ( - await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot() - ).toMatchScreenshot(); + // TODO: should be uncommented when the region map is migrated to the new platform + // const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; + // await ( + // await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot() + // ).toMatchScreenshot(); }); }); }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts index 00693845bb266..2486fb0e1fbd0 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -21,6 +21,17 @@ import expect from '@kbn/expect'; import { ExpressionValue } from 'src/plugins/expressions'; import { FtrProviderContext } from '../../../functional/ftr_provider_context'; +declare global { + interface Window { + runPipeline: ( + expressions: string, + context?: ExpressionValue, + initialContext?: ExpressionValue + ) => any; + renderPipelineResponse: (context?: ExpressionValue) => Promise; + } +} + export type ExpressionResult = any; export type ExpectExpression = ( @@ -165,7 +176,7 @@ export function expectExpressionProvider({ log.debug('starting to render'); const result = await browser.executeAsync( (_context: ExpressionResult, done: (renderResult: any) => void) => - window.renderPipelineResponse(_context).then(renderResult => { + window.renderPipelineResponse(_context).then((renderResult: any) => { done(renderResult); return renderResult; }), diff --git a/test/plugin_functional/plugins/core_provider_plugin/kibana.json b/test/plugin_functional/plugins/core_provider_plugin/kibana.json new file mode 100644 index 0000000000000..1d5c5824d6b97 --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "core_provider_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "optionalPlugins": ["core_plugin_a", "core_plugin_b", "licensing"], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/core_provider_plugin/public/index.ts b/test/plugin_functional/plugins/core_provider_plugin/public/index.ts index c74928203db56..2f271fe5ef65b 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/public/index.ts +++ b/test/plugin_functional/plugins/core_provider_plugin/public/index.ts @@ -16,13 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -import { npSetup, npStart } from 'ui/new_platform'; +import { Plugin, CoreSetup, CoreStart } from 'kibana/public'; import '../types'; -window.__coreProvider = { - setup: npSetup, - start: npStart, - testUtils: { - delay: (ms: number) => new Promise(res => setTimeout(res, ms)), - }, -}; +export const plugin = () => new CoreProviderPlugin(); + +class CoreProviderPlugin implements Plugin { + private setupDeps?: { core: CoreSetup; plugins: Record }; + public setup(core: CoreSetup, plugins: Record) { + this.setupDeps = { + core, + plugins, + }; + } + + public start(core: CoreStart, plugins: Record) { + window.__coreProvider = { + setup: this.setupDeps!, + start: { + core, + plugins, + }, + testUtils: { + delay: (ms: number) => new Promise(res => setTimeout(res, ms)), + }, + }; + } + public stop() {} +} diff --git a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json index c29959197958d..baedb5f2f621b 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json @@ -8,7 +8,7 @@ "index.ts", "types.ts", "public/**/*.ts", - "../../../../typings/**/*", + "../../../../typings/**/*" ], "exclude": [] } diff --git a/test/plugin_functional/plugins/core_provider_plugin/types.ts b/test/plugin_functional/plugins/core_provider_plugin/types.ts index bf19578c37baa..cae3b604ecd95 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/types.ts +++ b/test/plugin_functional/plugins/core_provider_plugin/types.ts @@ -16,17 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import { LegacyCoreSetup, LegacyCoreStart } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; declare global { interface Window { __coreProvider: { setup: { - core: LegacyCoreSetup; + core: CoreSetup; plugins: Record; }; start: { - core: LegacyCoreStart; + core: CoreStart; plugins: Record; }; testUtils: { diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts index 0781cf8a4f5bd..b2c0413c5024b 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_status.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -57,7 +57,8 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }, i)) as any; }; - describe('application status management', () => { + // FLAKY: https://github.com/elastic/kibana/issues/65423 + describe.skip('application status management', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('app_status_start'); }); diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 097833750bc80..b8e26b8e6ffcb 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -40,8 +40,8 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider const find = getService('find'); const testSubjects = getService('testSubjects'); - const navigateTo = (path: string) => - browser.navigateTo(`${PageObjects.common.getHostPort()}${path}`); + const navigateTo = async (path: string) => + await browser.navigateTo(`${PageObjects.common.getHostPort()}${path}`); const navigateToApp = async (title: string) => { await appsMenu.clickLink(title); return browser.execute(() => { diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts index 8ddd0ff96ba8f..b2393443989f9 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts @@ -47,14 +47,6 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider await PageObjects.common.navigateToApp('settings'); }); - it('to injectedMetadata service', async () => { - expect( - await browser.execute(() => { - return window.__coreProvider.setup.core.injectedMetadata.getKibanaBuildNumber(); - }) - ).to.be.a('number'); - }); - it('to start services via coreSetup.getStartServices', async () => { expect( await browser.executeAsync(async cb => { diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 1f6e09fad19e9..e3f46e7a6ada4 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -6,6 +6,7 @@ echo " -> building kibana platform plugins" node scripts/build_kibana_platform_plugins \ --oss \ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ --verbose; # doesn't persist, also set in kibanaPipeline.groovy diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index 8dc41639fa946..c962b962b1e5e 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -5,6 +5,7 @@ source src/dev/ci_setup/setup_env.sh echo " -> building kibana platform plugins" node scripts/build_kibana_platform_plugins \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ diff --git a/test/tsconfig.json b/test/tsconfig.json index 5a3716e620fed..a270144bd49fe 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -19,6 +19,7 @@ "typings/**/*" ], "exclude": [ - "plugin_functional/plugins/**/*" + "plugin_functional/plugins/**/*", + "interpreter_functional/plugins/**/*" ] } diff --git a/webpackShims/leaflet.js b/webpackShims/leaflet.js deleted file mode 100644 index c35076e129533..0000000000000 --- a/webpackShims/leaflet.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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. - */ - -require('../node_modules/leaflet/dist/leaflet.css'); -window.L = module.exports = require('../node_modules/leaflet/dist/leaflet'); -window.L.Browser.touch = false; -window.L.Browser.pointer = false; - -require('../node_modules/leaflet.heat/dist/leaflet-heat.js'); - -require('../node_modules/leaflet-draw/dist/leaflet.draw.css'); -require('../node_modules/leaflet-draw/dist/leaflet.draw.js'); - -require('../node_modules/leaflet-responsive-popup/leaflet.responsive.popup.css'); -require('../node_modules/leaflet-responsive-popup/leaflet.responsive.popup.js'); diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 532c49803e7b0..746fa693e435e 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -27,7 +27,6 @@ import 'uiExports/search'; import 'uiExports/shareContextMenuExtensions'; import _ from 'lodash'; import 'ui/autoload/all'; -import 'leaflet'; import { npStart } from 'ui/new_platform'; import { localApplicationService } from 'plugins/kibana/local_application_service'; diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 40123040764b7..a46cdfe35e32d 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import mappings from './mappings.json'; import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; @@ -39,23 +38,13 @@ export function maps(kibana) { }, injectDefaultVars(server) { const serverConfig = server.config(); - const mapConfig = serverConfig.get('map'); return { showMapVisualizationTypes: serverConfig.get('xpack.maps.showMapVisualizationTypes'), showMapsInspectorAdapter: serverConfig.get('xpack.maps.showMapsInspectorAdapter'), enableVectorTiles: serverConfig.get('xpack.maps.enableVectorTiles'), preserveDrawingBuffer: serverConfig.get('xpack.maps.preserveDrawingBuffer'), - isEmsEnabled: mapConfig.includeElasticMapsService, - emsFontLibraryUrl: mapConfig.emsFontLibraryUrl, - emsTileLayerId: mapConfig.emsTileLayerId, - proxyElasticMapsServiceInMaps: mapConfig.proxyElasticMapsServiceInMaps, - emsFileApiUrl: mapConfig.emsFileApiUrl, - emsTileApiUrl: mapConfig.emsTileApiUrl, - emsLandingPageUrl: mapConfig.emsLandingPageUrl, kbnPkgVersion: serverConfig.get('pkg.version'), - regionmapLayers: _.get(mapConfig, 'regionmap.layers', []), - tilemap: _.get(mapConfig, 'tilemap', {}), }; }, styleSheetPaths: `${__dirname}/public/index.scss`, @@ -112,14 +101,12 @@ export function maps(kibana) { licensing: newPlatformPlugins.licensing, home: newPlatformPlugins.home, usageCollection: newPlatformPlugins.usageCollection, + mapsLegacy: newPlatformPlugins.mapsLegacy, }; // legacy dependencies const __LEGACY = { config: server.config, - mapConfig() { - return server.config().get('map'); - }, route: server.route.bind(server), plugins: { elasticsearch: server.plugins.elasticsearch, @@ -132,8 +119,8 @@ export function maps(kibana) { getInjectedUiAppVars: server.getInjectedUiAppVars, }; - const mapPluginSetup = new MapPlugin().setup(coreSetup, pluginsSetup, __LEGACY); - server.expose('getMapConfig', mapPluginSetup.getMapConfig); + const mapPlugin = new MapPlugin(); + mapPlugin.setup(coreSetup, pluginsSetup, __LEGACY); }, }); } diff --git a/x-pack/legacy/plugins/maps/server/plugin.js b/x-pack/legacy/plugins/maps/server/plugin.js index 79f3dcf76b82e..d2d5309606cde 100644 --- a/x-pack/legacy/plugins/maps/server/plugin.js +++ b/x-pack/legacy/plugins/maps/server/plugin.js @@ -19,8 +19,9 @@ import { emsBoundariesSpecProvider } from './tutorials/ems'; export class MapPlugin { setup(core, plugins, __LEGACY) { - const { featuresPlugin, home, licensing, usageCollection } = plugins; + const { featuresPlugin, home, licensing, usageCollection, mapsLegacy } = plugins; let routesInitialized = false; + const mapConfig = mapsLegacy.config; featuresPlugin.registerFeature({ id: APP_ID, @@ -58,7 +59,7 @@ export class MapPlugin { const { state } = license.check('maps', 'basic'); if (state === 'valid' && !routesInitialized) { routesInitialized = true; - initRoutes(__LEGACY, license.uid); + initRoutes(__LEGACY, license.uid, mapConfig); } }); @@ -134,7 +135,7 @@ export class MapPlugin { home.tutorials.registerTutorial( emsBoundariesSpecProvider({ prependBasePath: core.http.basePath.prepend, - emsLandingPageUrl: __LEGACY.mapConfig().emsLandingPageUrl, + emsLandingPageUrl: mapConfig.emsLandingPageUrl, }) ); } @@ -142,11 +143,5 @@ export class MapPlugin { __LEGACY.injectUiAppVars(APP_ID, async () => { return await __LEGACY.getInjectedUiAppVars('kibana'); }); - - return { - getMapConfig() { - return __LEGACY.mapConfig(); - }, - }; } } diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index d49f9827e3ea0..6b83f4026f1db 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -31,9 +31,8 @@ import Boom from 'boom'; const ROOT = `/${GIS_API_PATH}`; -export function initRoutes(server, licenseUid) { +export function initRoutes(server, licenseUid, mapConfig) { const serverConfig = server.config(); - const mapConfig = serverConfig.get('map'); let emsClient; if (mapConfig.includeElasticMapsService) { diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 3be2f26557079..a8f50ec3535e2 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -71,6 +71,19 @@ describe('register()', () => { `); }); + test('shallow clones the given action type', () => { + const myType: ActionType = { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }; + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register(myType); + myType.name = 'Changed'; + expect(actionTypeRegistry.get('my-action-type').name).toEqual('My action type'); + }); + test('throws error if action type already registered', () => { const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionTypeRegistry.register({ diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 723982b11e1cc..73ae49a7e69c2 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -91,7 +91,7 @@ export class ActionTypeRegistry { ) ); } - this.actionTypes.set(actionType.id, actionType); + this.actionTypes.set(actionType.id, { ...actionType }); this.taskManager.registerTaskDefinitions({ [`actions:${actionType.id}`]: { title: actionType.name, diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerting/server/alert_type_registry.test.ts index f9df390242cd4..f556287703347 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.test.ts @@ -72,6 +72,25 @@ describe('register()', () => { `); }); + test('shallow clones the given alert type', () => { + const alertType: AlertType = { + id: 'test', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + executor: jest.fn(), + }; + const registry = new AlertTypeRegistry(alertTypeRegistryParams); + registry.register(alertType); + alertType.name = 'Changed'; + expect(registry.get('test').name).toEqual('Test'); + }); + test('should throw an error if type is already registered', () => { const registry = new AlertTypeRegistry(alertTypeRegistryParams); registry.register({ diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerting/server/alert_type_registry.ts index 55e39b6a817db..8bcb4d838ca1b 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.ts @@ -41,7 +41,7 @@ export class AlertTypeRegistry { ); } alertType.actionVariables = normalizedActionVariables(alertType.actionVariables); - this.alertTypes.set(alertType.id, alertType); + this.alertTypes.set(alertType.id, { ...alertType }); this.taskManager.registerTaskDefinitions({ [`alerting:${alertType.id}`]: { title: alertType.name, diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 2ff30a61499b6..dc457c38a52af 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -34,7 +34,6 @@ export interface Connection { } export interface ServiceNodeMetrics { - numInstances: number; avgMemoryUsage: number | null; avgCpuUsage: number | null; avgTransactionDuration: number | null; diff --git a/x-pack/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx rename to x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx index cb983cdffa028..1e3a73acfab57 100644 --- a/x-pack/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx @@ -5,8 +5,8 @@ */ import { Location } from 'history'; -import { BreadcrumbRoute, getBreadcrumbs } from '../ProvideBreadcrumbs'; -import { RouteName } from '../route_config/route_names'; +import { BreadcrumbRoute, getBreadcrumbs } from './ProvideBreadcrumbs'; +import { RouteName } from './route_config/route_names'; describe('getBreadcrumbs', () => { const getTestRoutes = (): BreadcrumbRoute[] => [ diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx index 8960af0f21fd2..b4a556c497c1b 100644 --- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx @@ -30,10 +30,18 @@ function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) { class UpdateBreadcrumbsComponent extends React.Component { public updateHeaderBreadcrumbs() { - const breadcrumbs = this.props.breadcrumbs.map(({ value, match }) => ({ - text: value, - href: getAPMHref(match.url, this.props.location.search) - })); + const breadcrumbs = this.props.breadcrumbs.map( + ({ value, match }, index) => { + const isLastBreadcrumbItem = + index === this.props.breadcrumbs.length - 1; + return { + text: value, + href: isLastBreadcrumbItem + ? undefined // makes the breadcrumb item not clickable + : getAPMHref(match.url, this.props.location.search) + }; + } + ); document.title = getTitleFromBreadCrumbs(this.props.breadcrumbs); this.props.core.chrome.setBreadcrumbs(breadcrumbs); diff --git a/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap index 51bdb63874e63..e7f6cba59318a 100644 --- a/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap @@ -15,7 +15,7 @@ Array [ "text": "opbeans-node", }, Object { - "href": "#/services/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": undefined, "text": "Errors", }, ] @@ -40,7 +40,7 @@ Array [ "text": "Errors", }, Object { - "href": "#/services/opbeans-node/errors/myGroupId?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": undefined, "text": "myGroupId", }, ] @@ -61,7 +61,7 @@ Array [ "text": "opbeans-node", }, Object { - "href": "#/services/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": undefined, "text": "Transactions", }, ] @@ -86,7 +86,7 @@ Array [ "text": "Transactions", }, Object { - "href": "#/services/opbeans-node/transactions/view?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": undefined, "text": "my-transaction-name", }, ] @@ -95,7 +95,7 @@ Array [ exports[`UpdateBreadcrumbs Homepage 1`] = ` Array [ Object { - "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": undefined, "text": "APM", }, ] diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 7e15d0116b84d..b5bfa63c1bdde 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -90,11 +90,11 @@ const ANOMALY_DETECTION_TITLE = i18n.translate( { defaultMessage: 'Anomaly Detection' } ); -const ANOMALY_DETECTION_INFO = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverInfo', +const ANOMALY_DETECTION_TOOLTIP = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip', { defaultMessage: - 'Display the health of your service by enabling the anomaly detection feature in Machine Learning.' + 'Service health indicators are powered by the anomaly detection feature in machine learning' } ); @@ -108,11 +108,11 @@ const ANOMALY_DETECTION_LINK = i18n.translate( { defaultMessage: 'View anomalies' } ); -const ANOMALY_DETECTION_ENABLE_TEXT = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverEnable', +const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled', { defaultMessage: - 'Enable anomaly detection from the Integrations menu in the Service details view.' + 'Display service health indicators by enabling anomaly detection from the Integrations menu in the Service details view.' } ); @@ -154,15 +154,18 @@ export function Contents({ {isService && ( -

- -

{ANOMALY_DETECTION_TITLE}

-
-   - -
{hasAnomalyDetection ? ( <> +
+ +

{ANOMALY_DETECTION_TITLE}

+
+   + +
@@ -188,7 +191,12 @@ export function Contents({ ) : ( - {ANOMALY_DETECTION_ENABLE_TEXT} + <> + +

{ANOMALY_DETECTION_TITLE}

+
+ {ANOMALY_DETECTION_DISABLED_TEXT} + )} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index e5962afd76eb8..2edd36f0d1380 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -16,7 +16,6 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) avgRequestsPerMinute={164.47222031860858} avgCpuUsage={0.32809666568309237} avgMemoryUsage={0.5504868173242986} - numInstances={2} isLoading={false} /> )) @@ -27,7 +26,6 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) avgRequestsPerMinute={null} avgCpuUsage={null} avgMemoryUsage={null} - numInstances={1} isLoading={true} /> )) @@ -38,7 +36,6 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) avgRequestsPerMinute={8.439583235652972} avgCpuUsage={null} avgMemoryUsage={null} - numInstances={1} isLoading={false} /> )) @@ -49,7 +46,6 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) avgRequestsPerMinute={null} avgCpuUsage={null} avgMemoryUsage={null} - numInstances={1} isLoading={false} /> )); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 5c28fc0a5a7d0..39d54dc5801d2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner -} from '@elastic/eui'; +import { EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; @@ -30,10 +25,6 @@ function LoadingSpinner() { ); } -const BadgeRow = styled(EuiFlexItem)` - padding-bottom: ${lightTheme.gutterTypes.gutterSmall}; -`; - export const ItemRow = styled('tr')` line-height: 2; `; @@ -57,7 +48,6 @@ export function ServiceMetricList({ avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, - numInstances, isLoading }: ServiceMetricListProps) { const listItems = [ @@ -110,39 +100,22 @@ export function ServiceMetricList({ : null } ]; - const showBadgeRow = numInstances > 1; return isLoading ? ( ) : ( - <> - {showBadgeRow && ( - - - {numInstances > 1 && ( - - {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { - values: { numInstances }, - defaultMessage: '{numInstances} instances' - })} - - )} - - - )} - - - {listItems.map( - ({ title, description }) => - description && ( - - {title} - {description} - - ) - )} - -
- + + + {listItems.map( + ({ title, description }) => + description && ( + + {title} + {description} + + ) + )} + +
); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx index 3a6f94b975800..79a6370b4be46 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -132,7 +132,10 @@ export function AgentConfigurationCreateEdit({ setPage('choose-settings-step')} + onClickNext={() => { + resetSettings(); + setPage('choose-settings-step'); + }} /> )} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index d7e28828572d5..7e8dccb8aff06 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -14,8 +14,7 @@ import { TRANSACTION_DURATION, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, - METRIC_SYSTEM_TOTAL_MEMORY, - SERVICE_NODE_NAME + METRIC_SYSTEM_TOTAL_MEMORY } from '../../../common/elasticsearch_fieldnames'; import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; @@ -56,22 +55,19 @@ export async function getServiceMapServiceNodeInfo({ errorMetrics, transactionMetrics, cpuMetrics, - memoryMetrics, - instanceMetrics + memoryMetrics ] = await Promise.all([ getErrorMetrics(taskParams), getTransactionMetrics(taskParams), getCpuMetrics(taskParams), - getMemoryMetrics(taskParams), - getNumInstances(taskParams) + getMemoryMetrics(taskParams) ]); return { ...errorMetrics, ...transactionMetrics, ...cpuMetrics, - ...memoryMetrics, - ...instanceMetrics + ...memoryMetrics }; } @@ -226,47 +222,3 @@ async function getMemoryMetrics({ avgMemoryUsage: response.aggregations?.avgMemoryUsage.value ?? null }; } - -async function getNumInstances({ - setup, - filter -}: TaskParameters): Promise<{ numInstances: number }> { - const { client, indices } = setup; - const response = await client.search({ - index: indices['apm_oss.transactionIndices'], - body: { - query: { - bool: { - filter: filter.concat([ - { - term: { - [PROCESSOR_EVENT]: 'transaction' - } - }, - { - exists: { - field: SERVICE_NODE_NAME - } - }, - { - exists: { - field: METRIC_SYSTEM_TOTAL_MEMORY - } - } - ]) - } - }, - aggs: { - instances: { - cardinality: { - field: SERVICE_NODE_NAME - } - } - } - } - }); - - return { - numInstances: response.aggregations?.instances.value || 1 - }; -} diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index bd39dcfb39fe2..ba57d1475bc4f 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -73,7 +73,7 @@ export class CanvasPlugin id: 'canvas', title: 'Canvas', euiIconType: 'canvasApp', - order: 0, // need to figure out if this is the proper order for us + order: 3000, mount: async (params: AppMountParameters) => { // Load application bundle const { renderApp, initializeCanvas, teardownCanvas } = await import('./application'); diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 58393b88e37a3..9e7aedcc90bb5 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -560,7 +560,7 @@ export class EndpointDocGenerator { applied: { actions: { configure_elasticsearch_connection: { - message: 'elasticsearch comms configured successfully', + message: 'elasticsearch comes configured successfully', status: HostPolicyResponseActionStatus.success, }, configure_kernel: { @@ -648,7 +648,7 @@ export class EndpointDocGenerator { response: { configurations: { events: { - concerned_actions: this.randomHostPolicyResponseActions(), + concerned_actions: ['download_model'], status: this.randomHostPolicyResponseActionStatus(), }, logging: { diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index a1ddc97a90d29..181b0e7ab3884 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -25,7 +25,7 @@ export type Immutable = T extends undefined | null | boolean | string | numbe ? ImmutableSet : ImmutableObject; -type ImmutableArray = ReadonlyArray>; +export type ImmutableArray = ReadonlyArray>; type ImmutableMap = ReadonlyMap, Immutable>; type ImmutableSet = ReadonlySet>; type ImmutableObject = { readonly [K in keyof T]: Immutable }; @@ -644,6 +644,8 @@ export interface HostPolicyResponseActions { read_malware_config: HostPolicyResponseActionDetails; } +export type HostPolicyResponseConfiguration = HostPolicyResponse['endpoint']['policy']['applied']['response']['configurations']; + interface HostPolicyResponseConfigurationStatus { status: HostPolicyResponseActionStatus; concerned_actions: Array; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts index bcfd6b96c9eb8..a5378a02ed6fb 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList } from '../../../../../common/types'; +import { HostResultList, HostPolicyResponseActionStatus } from '../../../../../common/types'; import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors'; import { HostState } from '../../types'; import { ImmutableMiddlewareFactory } from '../../types'; @@ -77,7 +77,31 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = core endpoint: { policy: { applied: { - status: 'success', + version: '1.0.0', + status: HostPolicyResponseActionStatus.success, + id: '17d4b81d-9940-4b64-9de5-3e03ef1fb5cf', + actions: { + download_model: { + status: 'success', + message: 'Model downloaded', + }, + ingest_events_config: { + status: 'failure', + message: 'No action taken', + }, + }, + response: { + configurations: { + malware: { + status: 'success', + concerned_actions: ['download_model'], + }, + events: { + status: 'failure', + concerned_actions: ['ingest_events_config'], + }, + }, + }, }, }, }, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts index b0711baf9cdff..e16d4ff5d18c2 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts @@ -5,7 +5,12 @@ */ import querystring from 'querystring'; import { createSelector } from 'reselect'; -import { Immutable } from '../../../../../common/types'; +import { + Immutable, + HostPolicyResponseActions, + HostPolicyResponseConfiguration, + HostPolicyResponseActionStatus, +} from '../../../../../common/types'; import { HostState, HostIndexUIQueryParams } from '../../types'; const PAGE_SIZES = Object.freeze([10, 20, 50]); @@ -28,6 +33,61 @@ export const detailsLoading = (state: Immutable): boolean => state.de export const detailsError = (state: Immutable) => state.detailsError; +/** + * Returns the full policy response from the endpoint after a user modifies a policy. + */ +const detailsPolicyAppliedResponse = (state: Immutable) => + state.policyResponse && state.policyResponse.endpoint.policy.applied; + +/** + * Returns the response configurations from the endpoint after a user modifies a policy. + */ +export const policyResponseConfigurations: ( + state: Immutable +) => undefined | Immutable = createSelector( + detailsPolicyAppliedResponse, + applied => { + return applied?.response?.configurations; + } +); + +/** + * Returns a map of the number of failed and warning policy response actions per configuration. + */ +export const policyResponseFailedOrWarningActionCount: ( + state: Immutable +) => Map = createSelector(detailsPolicyAppliedResponse, applied => { + const failureOrWarningByConfigType = new Map(); + if (applied?.response?.configurations !== undefined && applied?.actions !== undefined) { + Object.entries(applied.response.configurations).map(([key, val]) => { + let count = 0; + for (const action of val.concerned_actions) { + const actionStatus = applied.actions[action]?.status; + if ( + actionStatus === HostPolicyResponseActionStatus.failure || + actionStatus === HostPolicyResponseActionStatus.warning + ) { + count += 1; + } + } + return failureOrWarningByConfigType.set(key, count); + }); + } + return failureOrWarningByConfigType; +}); + +/** + * Returns the actions taken by the endpoint for each response configuration after a user modifies a policy. + */ +export const policyResponseActions: ( + state: Immutable +) => undefined | Partial = createSelector( + detailsPolicyAppliedResponse, + applied => { + return applied?.actions; + } +); + export const isOnHostPage = (state: Immutable) => state.location ? state.location.pathname === '/hosts' : false; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx index 02f91307c988e..9abb54e8b1807 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx @@ -9,7 +9,7 @@ import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui'; import styled from 'styled-components'; export type FlyoutSubHeaderProps = CommonProps & { - children: React.ReactNode; + children?: React.ReactNode; backButton?: { title: string; onClick: MouseEventHandler; @@ -25,6 +25,9 @@ const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` padding-bottom: ${props => props.theme.eui.paddingSizes.s}; } + .flyoutSubHeaderBackButton { + font-size: ${props => props.theme.eui.euiFontSizeXS}; + } .back-button-content { padding-left: 0; &-text { @@ -48,7 +51,7 @@ const BUTTON_TEXT_PROPS = Object.freeze({ className: 'back-button-content-text' export const FlyoutSubHeader = memo( ({ children, backButton, ...otherProps }) => { return ( - + {backButton && (
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */} @@ -60,6 +63,7 @@ export const FlyoutSubHeader = memo( size="xs" href={backButton?.href ?? ''} onClick={backButton?.onClick} + className="flyoutSubHeaderBackButton" > {backButton?.title} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_constants.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_constants.ts new file mode 100644 index 0000000000000..5250eeaf028d5 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HostPolicyResponseActionStatus } from '../../../../../../common/types'; + +export const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze< + { [key in keyof typeof HostPolicyResponseActionStatus]: string } +>({ + success: 'success', + warning: 'warning', + failure: 'danger', +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx index 7d948f54bd0bc..ee1c7543d7e0a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx @@ -16,13 +16,14 @@ import { import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { HostMetadata, HostPolicyResponseActionStatus } from '../../../../../../common/types'; +import { HostMetadata } from '../../../../../../common/types'; import { FormattedDateAndTime } from '../../formatted_date_time'; import { LinkToApp } from '../../components/link_to_app'; import { useHostSelector, useHostLogsUrl } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; import { policyResponseStatus, uiQueryParams } from '../../../store/hosts/selectors'; import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler'; +import { POLICY_STATUS_TO_HEALTH_COLOR } from './host_constants'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -31,14 +32,6 @@ const HostIds = styled(EuiListGroupItem)` } `; -const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze< - { [key in keyof typeof HostPolicyResponseActionStatus]: string } ->({ - success: 'success', - warning: 'warning', - failure: 'danger', -}); - export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const { appId, appPath, url } = useHostLogsUrl(details.host.id); const queryParams = useHostSelector(uiQueryParams); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx index e44a45f300daa..017ce9a66f8c5 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx @@ -9,8 +9,9 @@ import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, - EuiTitle, EuiLoadingContent, + EuiTitle, + EuiText, EuiSpacer, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; @@ -25,6 +26,9 @@ import { detailsError, showView, detailsLoading, + policyResponseConfigurations, + policyResponseActions, + policyResponseFailedOrWarningActionCount, } from '../../../store/hosts/selectors'; import { HostDetails } from './host_details'; import { PolicyResponse } from './policy_response'; @@ -101,6 +105,9 @@ const PolicyResponseFlyoutPanel = memo<{ hostMeta: HostMetadata; }>(({ hostMeta }) => { const { show, ...queryParams } = useHostSelector(uiQueryParams); + const responseConfig = useHostSelector(policyResponseConfigurations); + const responseActionStatus = useHostSelector(policyResponseActions); + const responseAttentionCount = useHostSelector(policyResponseFailedOrWarningActionCount); const detailsUri = useMemo( () => urlFromQueryParams({ @@ -125,18 +132,28 @@ const PolicyResponseFlyoutPanel = memo<{ - -

+ /> + + +

-

- - - - +

+ + {responseConfig !== undefined && responseActionStatus !== undefined ? ( + + ) : ( + + )} ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx index eacb6a52d3184..aa04f2fdff57f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx @@ -3,8 +3,145 @@ * 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, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiAccordion, EuiNotificationBadge, EuiHealth } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { htmlIdGenerator } from '@elastic/eui'; +import { + HostPolicyResponseActions, + HostPolicyResponseConfiguration, + Immutable, + ImmutableArray, +} from '../../../../../../common/types'; +import { formatResponse } from './policy_response_friendly_names'; +import { POLICY_STATUS_TO_HEALTH_COLOR } from './host_constants'; -export const PolicyResponse = memo(() => { - return
Policy Status to be displayed here soon.
; -}); +/** + * Nested accordion in the policy response detailing any concerned + * actions the endpoint took to apply the policy configuration. + */ +const PolicyResponseConfigAccordion = styled(EuiAccordion)` + > .euiAccordion__triggerWrapper { + padding: ${props => props.theme.eui.paddingSizes.s}; + } + &.euiAccordion-isOpen { + background-color: ${props => props.theme.eui.euiFocusBackgroundColor}; + } + .euiAccordion__childWrapper { + background-color: ${props => props.theme.eui.euiColorLightestShade}; + } + .policyResponseAttentionBadge { + background-color: ${props => props.theme.eui.euiColorDanger}; + color: ${props => props.theme.eui.euiColorEmptyShade}; + } + .euiAccordion__button { + :hover, + :focus { + text-decoration: none; + } + } + :hover:not(.euiAccordion-isOpen) { + background-color: ${props => props.theme.eui.euiColorLightestShade}; + } +`; + +const ResponseActions = memo( + ({ + actions, + actionStatus, + }: { + actions: ImmutableArray; + actionStatus: Partial; + }) => { + return ( + <> + {actions.map((action, index) => { + const statuses = actionStatus[action]; + if (statuses === undefined) { + return undefined; + } + return ( + +

{formatResponse(action)}

+ + } + paddingSize="s" + extraAction={ + + +

{formatResponse(statuses.status)}

+
+
+ } + > + +

{statuses.message}

+
+
+ ); + })} + + ); + } +); + +/** + * A policy response is returned by the endpoint and shown in the host details after a user modifies a policy + */ +export const PolicyResponse = memo( + ({ + responseConfig, + responseActionStatus, + responseAttentionCount, + }: { + responseConfig: Immutable; + responseActionStatus: Partial; + responseAttentionCount: Map; + }) => { + return ( + <> + {Object.entries(responseConfig).map(([key, val]) => { + const attentionCount = responseAttentionCount.get(key); + return ( + htmlIdGenerator()(), [])} + key={useMemo(() => htmlIdGenerator()(), [])} + data-test-subj="hostDetailsPolicyResponseConfigAccordion" + buttonContent={ + +

{formatResponse(key)}

+
+ } + paddingSize="m" + extraAction={ + attentionCount && + attentionCount > 0 && ( + + {attentionCount} + + ) + } + > + +
+ ); + })} + + ); + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response_friendly_names.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response_friendly_names.ts new file mode 100644 index 0000000000000..251b3e86bc3f9 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response_friendly_names.ts @@ -0,0 +1,170 @@ +/* + * 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'; + +const responseMap = new Map(); +responseMap.set( + 'success', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.success', { + defaultMessage: 'Success', + }) +); +responseMap.set( + 'warning', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.warning', { + defaultMessage: 'Warning', + }) +); +responseMap.set( + 'failure', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.failed', { + defaultMessage: 'Failed', + }) +); +responseMap.set( + 'malware', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.malware', { + defaultMessage: 'Malware', + }) +); +responseMap.set( + 'events', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.events', { + defaultMessage: 'Events', + }) +); +responseMap.set( + 'configure_elasticsearch_connection', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.configureElasticSearchConnection', { + defaultMessage: 'Configure Elastic Search Connection', + }) +); +responseMap.set( + 'configure_logging', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.configureLogging', { + defaultMessage: 'Configure Logging', + }) +); +responseMap.set( + 'configure_kernel', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.configureKernel', { + defaultMessage: 'Configure Kernel', + }) +); +responseMap.set( + 'configure_malware', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.configureMalware', { + defaultMessage: 'Configure Malware', + }) +); +responseMap.set( + 'connect_kernel', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.connectKernel', { + defaultMessage: 'Connect Kernel', + }) +); +responseMap.set( + 'detect_file_open_events', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.detectFileOpenEvents', { + defaultMessage: 'Detect File Open Events', + }) +); +responseMap.set( + 'detect_file_write_events', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.detectFileWriteEvents', { + defaultMessage: 'Detect File Write Events', + }) +); +responseMap.set( + 'detect_image_load_events', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.detectImageLoadEvents', { + defaultMessage: 'Detect Image Load Events', + }) +); +responseMap.set( + 'detect_process_events', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.detectProcessEvents', { + defaultMessage: 'Detect Process Events', + }) +); +responseMap.set( + 'download_global_artifacts', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.downloadGlobalArtifacts', { + defaultMessage: 'Download Global Artifacts', + }) +); +responseMap.set( + 'load_config', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.loadConfig', { + defaultMessage: 'Load Config', + }) +); +responseMap.set( + 'load_malware_model', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.loadMalwareModel', { + defaultMessage: 'Load Malware Model', + }) +); +responseMap.set( + 'read_elasticsearch_config', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.readElasticSearchConfig', { + defaultMessage: 'Read ElasticSearch Config', + }) +); +responseMap.set( + 'read_events_config', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.readEventsConfig', { + defaultMessage: 'Read Events Config', + }) +); +responseMap.set( + 'read_kernel_config', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.readKernelConfig', { + defaultMessage: 'Read Kernel Config', + }) +); +responseMap.set( + 'read_logging_config', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.readLoggingConfig', { + defaultMessage: 'Read Logging Config', + }) +); +responseMap.set( + 'read_malware_config', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.readMalwareConfig', { + defaultMessage: 'Read Malware Config', + }) +); +responseMap.set( + 'workflow', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.workflow', { + defaultMessage: 'Workflow', + }) +); +responseMap.set( + 'download_model', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.downloadModel', { + defaultMessage: 'Download Model', + }) +); +responseMap.set( + 'ingest_events_config', + i18n.translate('xpack.endpoint.hostDetails.policyResponse.injestEventsConfig', { + defaultMessage: 'Injest Events Config', + }) +); + +/** + * Takes in the snake-cased response from the API and + * removes the underscores and capitalizes the string. + */ +export function formatResponse(responseString: string) { + if (responseMap.has(responseString)) { + return responseMap.get(responseString); + } + return responseString; +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index 5a8765110c909..aaeff935b32b4 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -25,7 +25,7 @@ describe('when on the hosts page', () => { let coreStart: AppContextTestRender['coreStart']; let middlewareSpy: AppContextTestRender['middlewareSpy']; - beforeEach(async () => { + beforeEach(() => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); @@ -127,6 +127,14 @@ describe('when on the hosts page', () => { ) => { const policyResponse = docGenerator.generatePolicyResponse(); policyResponse.endpoint.policy.applied.status = overallStatus; + policyResponse.endpoint.policy.applied.response.configurations.malware.status = overallStatus; + policyResponse.endpoint.policy.applied.actions.download_model!.status = overallStatus; + if ( + overallStatus === HostPolicyResponseActionStatus.failure || + overallStatus === HostPolicyResponseActionStatus.warning + ) { + policyResponse.endpoint.policy.applied.actions.download_model!.message = 'no action taken'; + } store.dispatch({ type: 'serverReturnedHostPolicyResponse', payload: { @@ -281,6 +289,9 @@ describe('when on the hosts page', () => { fireEvent.click(policyStatusLink); }); await userChangedUrlChecker; + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(); + }); }); it('should hide the host details panel', async () => { const hostDetailsFlyout = await renderResult.queryByTestId('hostDetailsFlyoutBody'); @@ -299,6 +310,43 @@ describe('when on the hosts page', () => { (await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutTitle')).textContent ).toBe('Policy Response'); }); + it('should show a configuration section for each protection', async () => { + const configAccordions = await renderResult.findAllByTestId( + 'hostDetailsPolicyResponseConfigAccordion' + ); + expect(configAccordions).not.toBeNull(); + }); + it('should show an actions section for each configuration', async () => { + const actionAccordions = await renderResult.findAllByTestId( + 'hostDetailsPolicyResponseActionsAccordion' + ); + const action = await renderResult.findAllByTestId('policyResponseAction'); + const statusHealth = await renderResult.findAllByTestId('policyResponseStatusHealth'); + const message = await renderResult.findAllByTestId('policyResponseMessage'); + expect(actionAccordions).not.toBeNull(); + expect(action).not.toBeNull(); + expect(statusHealth).not.toBeNull(); + expect(message).not.toBeNull(); + }); + it('should not show any numbered badges if all actions are succesful', () => { + return renderResult.findByTestId('hostDetailsPolicyResponseAttentionBadge').catch(e => { + expect(e).not.toBeNull(); + }); + }); + it('should show a numbered badge if at least one action failed', () => { + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure); + }); + const attentionBadge = renderResult.findByTestId('hostDetailsPolicyResponseAttentionBadge'); + expect(attentionBadge).not.toBeNull(); + }); + it('should show a numbered badge if at least one action has a warning', () => { + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning); + }); + const attentionBadge = renderResult.findByTestId('hostDetailsPolicyResponseAttentionBadge'); + expect(attentionBadge).not.toBeNull(); + }); it('should include the back to details link', async () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx index f7eafff137f51..39529e7c11ab1 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx @@ -38,7 +38,7 @@ const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ const clickHandler = useNavigateByRouterEventHandler(route); return ( // eslint-disable-next-line @elastic/eui/href-or-on-click - + {name} ); @@ -134,6 +134,7 @@ export const PolicyList = React.memo(() => { render(version: string) { return ( ({ - left: `${left}px`, - top: `${top}px`, + left: `${left + processNodeViewXOffset}px`, + top: `${top + processNodeViewYOffset}px`, // Width of symbol viewport scaled to fit - width: `${360 * magFactorX}px`, + width: `${logicalProcessNodeViewWidth * magFactorX}px`, // Height according to symbol viewbox AR - height: `${120 * magFactorX}px`, - // Adjusted to position/scale with camera - transform: `translateX(-${0.172413 * 360 * magFactorX + 10}px) translateY(-${0.73684 * - 120 * - magFactorX}px)`, + height: `${logicalProcessNodeViewHeight * magFactorX}px`, }), - [left, magFactorX, top] + [left, magFactorX, processNodeViewXOffset, processNodeViewYOffset, top] ); /** @@ -202,32 +209,26 @@ export const ProcessEventDot = styled( const dispatch = useResolverDispatch(); - const handleFocus = useCallback( - (focusEvent: React.FocusEvent) => { - dispatch({ - type: 'userFocusedOnResolverNode', - payload: { - nodeId, - }, - }); - }, - [dispatch, nodeId] - ); + const handleFocus = useCallback(() => { + dispatch({ + type: 'userFocusedOnResolverNode', + payload: { + nodeId, + }, + }); + }, [dispatch, nodeId]); - const handleClick = useCallback( - (clickEvent: React.MouseEvent) => { - if (animationTarget.current !== null) { - (animationTarget.current as any).beginElement(); - } - dispatch({ - type: 'userSelectedResolverNode', - payload: { - nodeId, - }, - }); - }, - [animationTarget, dispatch, nodeId] - ); + const handleClick = useCallback(() => { + if (animationTarget.current !== null) { + (animationTarget.current as any).beginElement(); + } + dispatch({ + type: 'userSelectedResolverNode', + payload: { + nodeId, + }, + }); + }, [animationTarget, dispatch, nodeId]); /* eslint-disable jsx-a11y/click-events-have-key-events */ /** @@ -375,13 +376,11 @@ export const ProcessEventDot = styled( ) )` position: absolute; - display: block; text-align: left; font-size: 10px; user-select: none; box-sizing: border-box; border-radius: 10%; - padding: 4px; white-space: nowrap; will-change: left, top, width, height; contain: strict; diff --git a/x-pack/plugins/graph/public/angular/templates/index.html b/x-pack/plugins/graph/public/angular/templates/index.html index 8555658596179..939d92518e271 100644 --- a/x-pack/plugins/graph/public/angular/templates/index.html +++ b/x-pack/plugins/graph/public/angular/templates/index.html @@ -122,8 +122,8 @@ diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 15c3ef0b84562..84fbc04aa5a31 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -8,6 +8,15 @@ import React from 'react'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { MemoryRouter } from 'react-router-dom'; + +/** + * The below import is required to avoid a console error warn from brace package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import * as stubWebWorker from '../../../../test_utils/stub_web_worker'; // eslint-disable-line no-unused-vars + import { AppWithoutRouter } from '../../public/application/app'; import { AppContextProvider } from '../../public/application/app_context'; import { Provider } from 'react-redux'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/index.ts new file mode 100644 index 0000000000000..eac68770d3de2 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { defaultShapeParameters } from './shape_datatype.test'; +export { defaultTextParameters } from './text_datatype.test'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx new file mode 100644 index 0000000000000..19bf6973472ff --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); +const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + +// Parameters automatically added to the shape datatype when saved (with the default values) +export const defaultShapeParameters = { + type: 'shape', + coerce: false, + ignore_malformed: false, + ignore_z_value: true, +}; + +describe('Mappings editor: shape datatype', () => { + let testBed: MappingsEditorTestBed; + + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + + test('initial view and default parameters values', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'shape', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { + exists, + waitFor, + waitForFn, + actions: { startEditField, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout to edit the field + await act(async () => { + startEditField('myField'); + }); + + await waitFor('mappingsEditorFieldEdit'); + + // Save the field and close the flyout + await act(async () => { + await updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + // It should have the default parameters values added + updatedMappings.properties.myField = { + type: 'shape', + ...defaultShapeParameters, + }; + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx new file mode 100644 index 0000000000000..2bfaa884a0132 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx @@ -0,0 +1,459 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; +import { getFieldConfig } from '../../../lib'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); +const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + +// Parameters automatically added to the text datatype when saved (with the default values) +export const defaultTextParameters = { + type: 'text', + eager_global_ordinals: false, + fielddata: false, + index: true, + index_options: 'positions', + index_phrases: false, + norms: true, + store: false, +}; + +describe('Mappings editor: text datatype', () => { + let testBed: MappingsEditorTestBed; + + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + + afterEach(() => { + onChangeHandler.mockReset(); + }); + + test('initial view and default parameters values', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'text', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { + exists, + waitFor, + waitForFn, + actions: { startEditField, getToggleValue, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout to edit the field + await act(async () => { + startEditField('myField'); + }); + + await waitFor('mappingsEditorFieldEdit'); + + // It should have searchable ("index" param) active by default + const indexFieldConfig = getFieldConfig('index'); + expect(getToggleValue('indexParameter.formRowToggle')).toBe(indexFieldConfig.defaultValue); + + // Save the field and close the flyout + await act(async () => { + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + // It should have the default parameters values added + updatedMappings.properties.myField = { + type: 'text', + ...defaultTextParameters, + }; + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + }, 30000); + + test('analyzer parameter: default values', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'text', + // Should have 2 dropdown selects: + // The first one set to 'language' and the second one set to 'french + search_quote_analyzer: 'french', + }, + }, + }; + + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + + const { + find, + exists, + waitFor, + waitForFn, + form: { selectCheckBox, setSelectValue }, + actions: { + startEditField, + getCheckboxValue, + showAdvancedSettings, + updateFieldAndCloseFlyout, + }, + } = testBed; + const fieldToEdit = 'myField'; + + // Start edit and immediately save to have all the default values + await act(async () => { + startEditField(fieldToEdit); + }); + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + await act(async () => { + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + ({ data } = await getMappingsEditorData()); + + let updatedMappings: any = { + ...defaultMappings, + properties: { + myField: { + ...defaultMappings.properties.myField, + ...defaultTextParameters, + }, + }, + }; + expect(data).toEqual(updatedMappings); + + // Re-open the edit panel + await act(async () => { + startEditField('myField'); + }); + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + // When no analyzer is defined, defaults to "Index default" + let indexAnalyzerValue = find('indexAnalyzer.select').props().value; + expect(indexAnalyzerValue).toEqual('index_default'); + + const searchQuoteAnalyzerSelects = find('searchQuoteAnalyzer.select'); + + expect(searchQuoteAnalyzerSelects.length).toBe(2); + expect(searchQuoteAnalyzerSelects.at(0).props().value).toBe('language'); + expect(searchQuoteAnalyzerSelects.at(1).props().value).toBe( + defaultMappings.properties.myField.search_quote_analyzer + ); + + // When no "search_analyzer" is defined, the checkBox should be checked + let isUseSameAnalyzerForSearchChecked = getCheckboxValue( + 'useSameAnalyzerForSearchCheckBox.input' + ); + expect(isUseSameAnalyzerForSearchChecked).toBe(true); + + // And the search analyzer select should not exist + expect(exists('searchAnalyzer')).toBe(false); + + // Uncheck the "Use same analyzer for search" checkbox and wait for the search analyzer select + await act(async () => { + selectCheckBox('useSameAnalyzerForSearchCheckBox.input', false); + }); + + await waitFor('searchAnalyzer'); + + let searchAnalyzerValue = find('searchAnalyzer.select').props().value; + expect(searchAnalyzerValue).toEqual('index_default'); + + await act(async () => { + // Change the value of the 3 analyzers + setSelectValue('indexAnalyzer.select', 'standard'); + setSelectValue('searchAnalyzer.select', 'simple'); + setSelectValue(find('searchQuoteAnalyzer.select').at(0), 'whitespace'); + }); + + // Make sure the second dropdown select has been removed + await waitForFn( + async () => find('searchQuoteAnalyzer.select').length === 1, + 'Error waiting for the second dropdown select of search quote analyzer to be removed' + ); + + await act(async () => { + // Save & close + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + updatedMappings = { + ...updatedMappings, + properties: { + myField: { + ...updatedMappings.properties.myField, + analyzer: 'standard', + search_analyzer: 'simple', + search_quote_analyzer: 'whitespace', + }, + }, + }; + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + + // Re-open the flyout and make sure the select have the correct updated value + await act(async () => { + startEditField('myField'); + }); + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + isUseSameAnalyzerForSearchChecked = getCheckboxValue('useSameAnalyzerForSearchCheckBox.input'); + expect(isUseSameAnalyzerForSearchChecked).toBe(false); + + indexAnalyzerValue = find('indexAnalyzer.select').props().value; + searchAnalyzerValue = find('searchAnalyzer.select').props().value; + const searchQuoteAnalyzerValue = find('searchQuoteAnalyzer.select').props().value; + + expect(indexAnalyzerValue).toBe('standard'); + expect(searchAnalyzerValue).toBe('simple'); + expect(searchQuoteAnalyzerValue).toBe('whitespace'); + }, 30000); + + test('analyzer parameter: custom analyzer (external plugin)', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'text', + analyzer: 'myCustomIndexAnalyzer', + search_analyzer: 'myCustomSearchAnalyzer', + search_quote_analyzer: 'myCustomSearchQuoteAnalyzer', + }, + }, + }; + + let updatedMappings: any = { + ...defaultMappings, + properties: { + myField: { + ...defaultMappings.properties.myField, + ...defaultTextParameters, + }, + }, + }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { + find, + exists, + waitFor, + waitForFn, + component, + form: { setInputValue, setSelectValue }, + actions: { startEditField, showAdvancedSettings, updateFieldAndCloseFlyout }, + } = testBed; + const fieldToEdit = 'myField'; + + await act(async () => { + startEditField(fieldToEdit); + }); + + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + expect(exists('indexAnalyzer-custom')).toBe(true); + expect(exists('searchAnalyzer-custom')).toBe(true); + expect(exists('searchQuoteAnalyzer-custom')).toBe(true); + + const indexAnalyzerValue = find('indexAnalyzer-custom.input').props().value; + const searchAnalyzerValue = find('searchAnalyzer-custom.input').props().value; + const searchQuoteAnalyzerValue = find('searchQuoteAnalyzer-custom.input').props().value; + + expect(indexAnalyzerValue).toBe(defaultMappings.properties.myField.analyzer); + expect(searchAnalyzerValue).toBe(defaultMappings.properties.myField.search_analyzer); + expect(searchQuoteAnalyzerValue).toBe(defaultMappings.properties.myField.search_quote_analyzer); + + const updatedIndexAnalyzer = 'newCustomIndexAnalyzer'; + const updatedSearchAnalyzer = 'whitespace'; + + await act(async () => { + // Change the index analyzer to another custom one + setInputValue('indexAnalyzer-custom.input', updatedIndexAnalyzer); + + // Change the search analyzer to a built-in analyzer + find('searchAnalyzer-toggleCustomButton').simulate('click'); + component.update(); + }); + + await waitFor('searchAnalyzer'); + + await act(async () => { + setSelectValue('searchAnalyzer.select', updatedSearchAnalyzer); + + // Change the searchQuote to use built-in analyzer + // By default it means using the "index default" + find('searchQuoteAnalyzer-toggleCustomButton').simulate('click'); + component.update(); + }); + + await waitFor('searchQuoteAnalyzer'); + + await act(async () => { + // Save & close + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + ({ data } = await getMappingsEditorData()); + + updatedMappings = { + ...updatedMappings, + properties: { + myField: { + ...updatedMappings.properties.myField, + analyzer: updatedIndexAnalyzer, + search_analyzer: updatedSearchAnalyzer, + search_quote_analyzer: undefined, // Index default means not declaring the analyzer + }, + }, + }; + + expect(data).toEqual(updatedMappings); + }, 30000); + + test('analyzer parameter: custom analyzer (from index settings)', async () => { + const indexSettings = { + analysis: { + analyzer: { + customAnalyzer_1: {}, + customAnalyzer_2: {}, + customAnalyzer_3: {}, + }, + }, + }; + + const customAnalyzers = Object.keys(indexSettings.analysis.analyzer); + + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'text', + analyzer: customAnalyzers[0], + }, + }, + }; + + let updatedMappings: any = { + ...defaultMappings, + properties: { + myField: { + ...defaultMappings.properties.myField, + ...defaultTextParameters, + }, + }, + }; + + testBed = await setup({ + value: defaultMappings, + onChange: onChangeHandler, + indexSettings, + }); + + const { + find, + exists, + waitFor, + waitForFn, + form: { setSelectValue }, + actions: { startEditField, showAdvancedSettings, updateFieldAndCloseFlyout }, + } = testBed; + const fieldToEdit = 'myField'; + + await act(async () => { + startEditField(fieldToEdit); + }); + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + // It should have 2 selects + const indexAnalyzerSelects = find('indexAnalyzer.select'); + + expect(indexAnalyzerSelects.length).toBe(2); + expect(indexAnalyzerSelects.at(0).props().value).toBe('custom'); + expect(indexAnalyzerSelects.at(1).props().value).toBe( + defaultMappings.properties.myField.analyzer + ); + + // Access the list of option of the second dropdown select + const subSelectOptions = indexAnalyzerSelects + .at(1) + .find('option') + .map(wrapper => wrapper.text()); + + expect(subSelectOptions).toEqual(customAnalyzers); + + await act(async () => { + // Change the custom analyzer dropdown to another one from the index settings + setSelectValue(find('indexAnalyzer.select').at(1), customAnalyzers[2]); + + // Save & close + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + ({ data } = await getMappingsEditorData()); + + updatedMappings = { + ...updatedMappings, + properties: { + myField: { + ...updatedMappings.properties.myField, + analyzer: customAnalyzers[2], + }, + }, + }; + + expect(data).toEqual(updatedMappings); + }, 30000); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx new file mode 100644 index 0000000000000..4af5f82d851e3 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx @@ -0,0 +1,127 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from './helpers'; +import { defaultTextParameters, defaultShapeParameters } from './datatypes'; +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); +const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + +describe('Mappings editor: edit field', () => { + let testBed: MappingsEditorTestBed; + + afterEach(() => { + onChangeHandler.mockReset(); + }); + + test('should open a flyout with the correct field to edit', async () => { + const defaultMappings = { + properties: { + user: { + type: 'object', + properties: { + address: { + type: 'object', + properties: { + street: { type: 'text' }, + }, + }, + }, + }, + }, + }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + // Make sure all the fields are expanded and present in the DOM + await testBed.actions.expandAllFieldsAndReturnMetadata(); + }); + + const { + find, + waitFor, + actions: { startEditField }, + } = testBed; + // Open the flyout to edit the field + await act(async () => { + startEditField('user.address.street'); + }); + + await waitFor('mappingsEditorFieldEdit'); + + // It should have the correct title + expect(find('mappingsEditorFieldEdit.flyoutTitle').text()).toEqual(`Edit field 'street'`); + + // It should have the correct field path + expect(find('mappingsEditorFieldEdit.fieldPath').text()).toEqual('user > address > street'); + + // The advanced settings should be hidden initially + expect(find('mappingsEditorFieldEdit.advancedSettings').props().style.display).toEqual('none'); + }); + + test('should update form parameters when changing the field datatype', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + ...defaultTextParameters, + }, + }, + }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { + find, + exists, + waitFor, + waitForFn, + component, + actions: { startEditField, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout, change the field type and save it + await act(async () => { + startEditField('myField'); + }); + + await waitFor('mappingsEditorFieldEdit'); + + await act(async () => { + // Change the field type + find('mappingsEditorFieldEdit.fieldType').simulate('change', [ + { label: 'Shape', value: defaultShapeParameters.type }, + ]); + component.update(); + }); + + await act(async () => { + await updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + const { data } = await getMappingsEditorData(); + + const updatedMappings = { + ...defaultMappings, + properties: { + myField: { + ...defaultShapeParameters, + }, + }, + }; + + expect(data).toEqual(updatedMappings); + }, 15000); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts index fa6bee56349e9..afdc039ae77d2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts @@ -3,7 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { setup as mappingsEditorSetup, MappingsEditorTestBed } from './mappings_editor.helpers'; +import { + setup as mappingsEditorSetup, + MappingsEditorTestBed, + DomFields, + getMappingsEditorDataFactory, +} from './mappings_editor.helpers'; export { nextTick, @@ -13,7 +18,7 @@ export { } from '../../../../../../../../../test_utils'; export const componentHelpers = { - mappingsEditor: { setup: mappingsEditorSetup }, + mappingsEditor: { setup: mappingsEditorSetup, getMappingsEditorDataFactory }, }; -export { MappingsEditorTestBed }; +export { MappingsEditorTestBed, DomFields }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index c8c8ef8bfe9b3..58242ec35018c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; + import { registerTestBed, TestBed, nextTick } from '../../../../../../../../../test_utils'; +import { getChildFieldsName } from '../../../lib'; import { MappingsEditor } from '../../../mappings_editor'; jest.mock('@elastic/eui', () => ({ @@ -14,6 +18,7 @@ jest.mock('@elastic/eui', () => ({ EuiComboBox: (props: any) => ( { props.onChange([syntheticEvent['0']]); }} @@ -29,14 +34,121 @@ jest.mock('@elastic/eui', () => ({ }} /> ), + // Mocking EuiSuperSelect to be able to easily change its value + // with a `myWrapper.simulate('change', { target: { value: 'someValue' } })` + EuiSuperSelect: (props: any) => ( + { + props.onChange(e.target.value); + }} + /> + ), })); +export interface DomFields { + [key: string]: { + type: string; + properties?: DomFields; + fields?: DomFields; + }; +} + const createActions = (testBed: TestBed) => { - const { find, waitFor, form, component } = testBed; + const { find, exists, waitFor, waitForFn, form, component } = testBed; + + const getFieldInfo = (testSubjectField: string): { name: string; type: string } => { + const name = find(`${testSubjectField}-fieldName` as TestSubjects).text(); + const type = find(`${testSubjectField}-datatype` as TestSubjects).props()['data-type-value']; + return { name, type }; + }; + + const expandField = async ( + field: ReactWrapper + ): Promise<{ hasChildren: boolean; testSubjectField: string }> => { + /** + * Field list item have 2 test subject assigned to them: + * data-test-subj="fieldsListItem " + * + * We read the second one as it is unique. + */ + const testSubjectField = (field.props() as any)['data-test-subj'] + .split(' ') + .filter((subj: string) => subj !== 'fieldsListItem')[0] as string; + + const expandButton = find(`${testSubjectField}.toggleExpandButton` as TestSubjects); + + // No expand button, so this field is not expanded + if (expandButton.length === 0) { + return { hasChildren: false, testSubjectField }; + } + + const isExpanded = (expandButton.props()['aria-label'] as string).includes('Collapse'); + + if (!isExpanded) { + expandButton.simulate('click'); + } + + // Wait for the children FieldList to be in the DOM + await waitFor(`${testSubjectField}.fieldsList` as TestSubjects); + + return { hasChildren: true, testSubjectField }; + }; + + /** + * Expand all the children of a field and return a metadata object of the fields found in the DOM. + * + * @param fieldName The field under wich we want to expand all the children. + * If no fieldName is provided, we expand all the **root** level fields. + */ + const expandAllFieldsAndReturnMetadata = async ( + fieldName?: string, + domTreeMetadata: DomFields = {} + ): Promise => { + const fields = find( + fieldName ? (`${fieldName}.fieldsList.fieldsListItem` as TestSubjects) : 'fieldsListItem' + ).map(wrapper => wrapper); // convert to Array for our for of loop below + + for (const field of fields) { + const { hasChildren, testSubjectField } = await expandField(field); + + // Read the info from the DOM about that field and add it to our domFieldMeta + const { name, type } = getFieldInfo(testSubjectField); + domTreeMetadata[name] = { + type, + }; + + if (hasChildren) { + // Update our metadata object + const childFieldName = getChildFieldsName(type as any)!; + domTreeMetadata[name][childFieldName] = {}; + + // Expand its children + await expandAllFieldsAndReturnMetadata( + testSubjectField, + domTreeMetadata[name][childFieldName] + ); + } + } + + return domTreeMetadata; + }; + + // Get a nested field in the rendered DOM tree + const getFieldAt = (path: string) => { + const testSubjectField = `${path.split('.').join('')}Field`; + return find(testSubjectField as TestSubjects); + }; const addField = async (name: string, type: string) => { const currentCount = find('fieldsListItem').length; + if (!exists('createFieldForm')) { + find('addFieldButton').simulate('click'); + await waitFor('createFieldForm'); + } + form.setInputValue('nameParameterInput', name); find('createFieldForm.fieldType').simulate('change', [ { @@ -54,6 +166,36 @@ const createActions = (testBed: TestBed) => { await waitFor('fieldsListItem', currentCount + 1); }; + const startEditField = (path: string) => { + const field = getFieldAt(path); + find('editFieldButton', field).simulate('click'); + component.update(); + }; + + const updateFieldAndCloseFlyout = () => { + find('mappingsEditorFieldEdit.editFieldUpdateButton').simulate('click'); + component.update(); + }; + + const showAdvancedSettings = async () => { + const checkIsVisible = async () => + find('mappingsEditorFieldEdit.advancedSettings').props().style.display === 'block'; + + if (await checkIsVisible()) { + // Already opened, nothing else to do + return; + } + + await act(async () => { + find('mappingsEditorFieldEdit.toggleAdvancedSetting').simulate('click'); + }); + + await waitForFn( + checkIsVisible, + 'Error waiting for the advanced settings CSS style.display to be "block"' + ); + }; + const selectTab = async (tab: 'fields' | 'templates' | 'advanced') => { const index = ['fields', 'templates', 'advanced'].indexOf(tab); const tabIdToContentMap: { [key: string]: TestSubjects } = { @@ -87,11 +229,33 @@ const createActions = (testBed: TestBed) => { return value; }; + const getComboBoxValue = (testSubject: TestSubjects) => { + const value = find(testSubject).props()['data-currentvalue']; + if (value === undefined) { + return []; + } + return value.map(({ label }: any) => label); + }; + + const getToggleValue = (testSubject: TestSubjects): boolean => + find(testSubject).props()['aria-checked']; + + const getCheckboxValue = (testSubject: TestSubjects): boolean => + find(testSubject).props().checked; + return { selectTab, + getFieldAt, addField, + expandAllFieldsAndReturnMetadata, + startEditField, + updateFieldAndCloseFlyout, + showAdvancedSettings, updateJsonEditor, getJsonEditorValue, + getComboBoxValue, + getToggleValue, + getCheckboxValue, }; }; @@ -109,6 +273,33 @@ export const setup = async (props: any = { onUpdate() {} }): Promise) => { + /** + * Helper to access the latest data sent to the onChange handler back to the consumer of the . + * Read the latest call with its argument passed and build the mappings object from it. + */ + return async () => { + const mockCalls = onChangeHandler.mock.calls; + + if (mockCalls.length === 0) { + throw new Error( + `Can't access data forwarded as the onChange() prop handler hasn't been called.` + ); + } + + const [arg] = mockCalls[mockCalls.length - 1]; + const { isValid, validate, getData } = arg; + + const isMappingsValid = isValid === undefined ? await act(validate) : isValid; + const data = getData(isMappingsValid); + + return { + isValid: isMappingsValid, + data, + }; + }; +}; + export type MappingsEditorTestBed = TestBed & { actions: ReturnType; }; @@ -116,7 +307,9 @@ export type MappingsEditorTestBed = TestBed & { export type TestSubjects = | 'formTab' | 'mappingsEditor' + | 'fieldsList' | 'fieldsListItem' + | 'fieldsListItem.fieldName' | 'fieldName' | 'mappingTypesDetectedCallout' | 'documentFields' @@ -126,7 +319,38 @@ export type TestSubjects = | 'advancedConfiguration.numericDetection.input' | 'advancedConfiguration.dynamicMappingsToggle' | 'advancedConfiguration.dynamicMappingsToggle.input' + | 'advancedConfiguration.metaField' + | 'advancedConfiguration.routingRequiredToggle.input' + | 'sourceField.includesField' + | 'sourceField.excludesField' | 'dynamicTemplatesEditor' | 'nameParameterInput' + | 'addFieldButton' + | 'editFieldButton' + | 'toggleExpandButton' + | 'createFieldForm' | 'createFieldForm.fieldType' - | 'createFieldForm.addButton'; + | 'createFieldForm.addButton' + | 'mappingsEditorFieldEdit' + | 'mappingsEditorFieldEdit.fieldType' + | 'mappingsEditorFieldEdit.editFieldUpdateButton' + | 'mappingsEditorFieldEdit.flyoutTitle' + | 'mappingsEditorFieldEdit.documentationLink' + | 'mappingsEditorFieldEdit.fieldPath' + | 'mappingsEditorFieldEdit.advancedSettings' + | 'mappingsEditorFieldEdit.toggleAdvancedSetting' + | 'indexParameter.formRowToggle' + | 'indexAnalyzer.select' + | 'searchAnalyzer' + | 'searchAnalyzer.select' + | 'searchQuoteAnalyzer' + | 'searchQuoteAnalyzer.select' + | 'indexAnalyzer-custom' + | 'indexAnalyzer-custom.input' + | 'searchAnalyzer-toggleCustomButton' + | 'searchAnalyzer-custom' + | 'searchAnalyzer-custom.input' + | 'searchQuoteAnalyzer-custom' + | 'searchQuoteAnalyzer-toggleCustomButton' + | 'searchQuoteAnalyzer-custom.input' + | 'useSameAnalyzerForSearchCheckBox.input'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx new file mode 100644 index 0000000000000..8989e85d9f188 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx @@ -0,0 +1,104 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed, DomFields, nextTick } from './helpers'; + +const { setup } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); + +describe('Mappings editor: mapped fields', () => { + afterEach(() => { + onChangeHandler.mockReset(); + }); + + describe('', () => { + let testBed: MappingsEditorTestBed; + const defaultMappings = { + properties: { + myField: { + type: 'text', + fields: { + raw: { + type: 'keyword', + }, + simpleAnalyzer: { + type: 'text', + }, + }, + }, + myObject: { + type: 'object', + properties: { + deeplyNested: { + type: 'object', + properties: { + title: { + type: 'text', + fields: { + raw: { type: 'keyword' }, + }, + }, + }, + }, + }, + }, + }, + }; + + test('should correctly represent the fields in the DOM tree', async () => { + await act(async () => { + testBed = await setup({ + value: defaultMappings, + onChange: onChangeHandler, + }); + }); + + const { + actions: { expandAllFieldsAndReturnMetadata }, + } = testBed; + + let domTreeMetadata: DomFields = {}; + await act(async () => { + domTreeMetadata = await expandAllFieldsAndReturnMetadata(); + }); + + expect(domTreeMetadata).toEqual(defaultMappings.properties); + }); + + test('should allow to be controlled by parent component and update on prop change', async () => { + await act(async () => { + testBed = await setup({ + value: defaultMappings, + onChange: onChangeHandler, + }); + }); + + const { + component, + setProps, + actions: { expandAllFieldsAndReturnMetadata }, + } = testBed; + + const newMappings = { properties: { hello: { type: 'text' } } }; + let domTreeMetadata: DomFields = {}; + + await act(async () => { + // Change the `value` prop of our + setProps({ value: newMappings }); + + // Don't ask me why but the 3 following lines are all required + component.update(); + await nextTick(); + component.update(); + + domTreeMetadata = await expandAllFieldsAndReturnMetadata(); + }); + + expect(domTreeMetadata).toEqual(newMappings.properties); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index 0cf5bf3f4453f..f516dfdb372ce 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -5,15 +5,55 @@ */ import { act } from 'react-dom/test-utils'; -import { componentHelpers, MappingsEditorTestBed, nextTick, getRandomString } from './helpers'; +import { componentHelpers, MappingsEditorTestBed, nextTick } from './helpers'; -const { setup } = componentHelpers.mappingsEditor; -const mockOnUpdate = () => undefined; +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); +const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + +describe('Mappings editor: core', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + + afterEach(() => { + onChangeHandler.mockReset(); + }); + + test('default behaviour', async () => { + const defaultMappings = { + properties: { + user: { + // No type defined for user + properties: { + name: { type: 'text' }, + }, + }, + }, + }; + + await setup({ value: defaultMappings, onChange: onChangeHandler }); + + const expectedMappings = { + _meta: {}, // Was not defined so an empty object is returned + _source: {}, // Was not defined so an empty object is returned + ...defaultMappings, + properties: { + user: { + type: 'object', // Was not defined so it defaults to "object" type + ...defaultMappings.properties.user, + }, + }, + }; + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(expectedMappings); + }); -describe('', () => { describe('multiple mappings detection', () => { test('should show a warning when multiple mappings are detected', async () => { - const defaultValue = { + const value = { type1: { properties: { name1: { @@ -29,7 +69,7 @@ describe('', () => { }, }, }; - const testBed = await setup({ onUpdate: mockOnUpdate, defaultValue }); + const testBed = await setup({ onChange: onChangeHandler, value }); const { exists } = testBed; expect(exists('mappingsEditor')).toBe(true); @@ -38,14 +78,14 @@ describe('', () => { }); test('should not show a warning when mappings a single-type', async () => { - const defaultValue = { + const value = { properties: { name1: { type: 'keyword', }, }, }; - const testBed = await setup({ onUpdate: mockOnUpdate, defaultValue }); + const testBed = await setup({ onChange: onChangeHandler, value }); const { exists } = testBed; expect(exists('mappingsEditor')).toBe(true); @@ -62,12 +102,12 @@ describe('', () => { let testBed: MappingsEditorTestBed; beforeEach(async () => { - testBed = await setup({ defaultValue: defaultMappings, onUpdate() {} }); + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); }); test('should keep the changes when switching tabs', async () => { const { - actions: { addField, selectTab, updateJsonEditor, getJsonEditorValue }, + actions: { addField, selectTab, updateJsonEditor, getJsonEditorValue, getToggleValue }, component, find, exists, @@ -79,7 +119,7 @@ describe('', () => { // ------------------------------------- expect(find('fieldsListItem').length).toEqual(0); // Check that we start with an empty list - const newField = { name: getRandomString(), type: 'text' }; + const newField = { name: 'John', type: 'text' }; await act(async () => { await addField(newField.name, newField.type); }); @@ -101,7 +141,6 @@ describe('', () => { // Update the dynamic templates editor value const updatedValueTemplates = [{ after: 'bar' }]; - await act(async () => { await updateJsonEditor('dynamicTemplatesEditor', updatedValueTemplates); await nextTick(); @@ -118,9 +157,9 @@ describe('', () => { await selectTab('advanced'); }); - let isDynamicMappingsEnabled = find( + let isDynamicMappingsEnabled = getToggleValue( 'advancedConfiguration.dynamicMappingsToggle.input' - ).props()['aria-checked']; + ); expect(isDynamicMappingsEnabled).toBe(true); let isNumericDetectionVisible = exists('advancedConfiguration.numericDetection'); @@ -134,9 +173,9 @@ describe('', () => { await nextTick(); }); - isDynamicMappingsEnabled = find('advancedConfiguration.dynamicMappingsToggle.input').props()[ - 'aria-checked' - ]; + isDynamicMappingsEnabled = getToggleValue( + 'advancedConfiguration.dynamicMappingsToggle.input' + ); expect(isDynamicMappingsEnabled).toBe(false); isNumericDetectionVisible = exists('advancedConfiguration.numericDetection'); @@ -166,12 +205,185 @@ describe('', () => { await selectTab('advanced'); }); - isDynamicMappingsEnabled = find('advancedConfiguration.dynamicMappingsToggle.input').props()[ - 'aria-checked' - ]; + isDynamicMappingsEnabled = getToggleValue( + 'advancedConfiguration.dynamicMappingsToggle.input' + ); expect(isDynamicMappingsEnabled).toBe(false); isNumericDetectionVisible = exists('advancedConfiguration.numericDetection'); expect(isNumericDetectionVisible).toBe(false); }); }); + + describe('component props', () => { + /** + * Note: the "indexSettings" prop will be tested along with the "analyzer" parameter on a text datatype field, + * as it is the only place where it is consumed by the mappings editor. + * + * The test that covers it is text_datatype.test.tsx: "analyzer parameter: custom analyzer (from index settings)" + */ + const defaultMappings: any = { + dynamic: true, + numeric_detection: false, + date_detection: true, + properties: { + title: { type: 'text' }, + address: { + type: 'object', + properties: { + street: { type: 'text' }, + city: { type: 'text' }, + }, + }, + }, + dynamic_templates: [{ initial: 'value' }], + _source: { + enabled: true, + includes: ['field1', 'field2'], + excludes: ['field3'], + }, + _meta: { + some: 'metaData', + }, + _routing: { + required: false, + }, + }; + + let testBed: MappingsEditorTestBed; + + beforeEach(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + test('props.value => should prepopulate the editor data', async () => { + const { + actions: { selectTab, getJsonEditorValue, getComboBoxValue, getToggleValue }, + find, + } = testBed; + + /** + * Mapped fields + */ + // Test that root-level mappings "properties" are rendered as root-level "DOM tree items" + const fields = find('fieldsListItem.fieldName').map(item => item.text()); + expect(fields).toEqual(Object.keys(defaultMappings.properties).sort()); + + /** + * Dynamic templates + */ + await act(async () => { + await selectTab('templates'); + }); + + // Test that dynamic templates JSON is rendered in the templates editor + const templatesValue = getJsonEditorValue('dynamicTemplatesEditor'); + expect(templatesValue).toEqual(defaultMappings.dynamic_templates); + + /** + * Advanced settings + */ + await act(async () => { + await selectTab('advanced'); + }); + + const isDynamicMappingsEnabled = getToggleValue( + 'advancedConfiguration.dynamicMappingsToggle.input' + ); + expect(isDynamicMappingsEnabled).toBe(defaultMappings.dynamic); + + const isNumericDetectionEnabled = getToggleValue( + 'advancedConfiguration.numericDetection.input' + ); + expect(isNumericDetectionEnabled).toBe(defaultMappings.numeric_detection); + + expect(getComboBoxValue('sourceField.includesField')).toEqual( + defaultMappings._source.includes + ); + expect(getComboBoxValue('sourceField.excludesField')).toEqual( + defaultMappings._source.excludes + ); + + const metaFieldValue = getJsonEditorValue('advancedConfiguration.metaField'); + expect(metaFieldValue).toEqual(defaultMappings._meta); + + const isRoutingRequired = getToggleValue('advancedConfiguration.routingRequiredToggle.input'); + expect(isRoutingRequired).toBe(defaultMappings._routing.required); + }); + + test('props.onChange() => should forward the changes to the consumer component', async () => { + let updatedMappings = { ...defaultMappings }; + + const { + actions: { addField, selectTab, updateJsonEditor }, + component, + form, + } = testBed; + + /** + * Mapped fields + */ + const newField = { name: 'someNewField', type: 'text' }; + updatedMappings = { + ...updatedMappings, + properties: { + ...updatedMappings.properties, + [newField.name]: { type: 'text' }, + }, + }; + + await act(async () => { + await addField(newField.name, newField.type); + }); + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + + /** + * Dynamic templates + */ + await act(async () => { + await selectTab('templates'); + }); + + const updatedTemplatesValue = [{ someTemplateProp: 'updated' }]; + updatedMappings = { + ...updatedMappings, + dynamic_templates: updatedTemplatesValue, + }; + + await act(async () => { + await updateJsonEditor('dynamicTemplatesEditor', updatedTemplatesValue); + await nextTick(); + component.update(); + }); + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + + /** + * Advanced settings + */ + await act(async () => { + await selectTab('advanced'); + }); + + // Disbable dynamic mappings + await act(async () => { + form.toggleEuiSwitch('advancedConfiguration.dynamicMappingsToggle.input'); + }); + + ({ data } = await getMappingsEditorData()); + + // When we disable dynamic mappings, we set it to "false" and remove date and numeric detections + updatedMappings = { + ...updatedMappings, + dynamic: false, + date_detection: undefined, + dynamic_date_formats: undefined, + numeric_detection: undefined, + }; + + expect(data).toEqual(updatedMappings); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index 6b33d4450c3ae..c84756cab8e88 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useRef } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { useForm, Form, SerializerFunc } from '../../shared_imports'; +import { GenericObject } from '../../types'; import { Types, useDispatch } from '../../mappings_state'; import { DynamicMappingSection } from './dynamic_mapping_section'; import { SourceFieldSection } from './source_field_section'; @@ -17,10 +18,10 @@ import { configurationFormSchema } from './configuration_form_schema'; type MappingsConfiguration = Types['MappingsConfiguration']; interface Props { - defaultValue?: MappingsConfiguration; + value?: MappingsConfiguration; } -const stringifyJson = (json: { [key: string]: any }) => +const stringifyJson = (json: GenericObject) => Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}'; const formSerializer: SerializerFunc = formData => { @@ -57,7 +58,7 @@ const formSerializer: SerializerFunc = formData => { }; }; -const formDeserializer = (formData: { [key: string]: any }) => { +const formDeserializer = (formData: GenericObject) => { const { dynamic, numeric_detection, @@ -86,14 +87,14 @@ const formDeserializer = (formData: { [key: string]: any }) => { }; }; -export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { +export const ConfigurationForm = React.memo(({ value }: Props) => { const didMountRef = useRef(false); const { form } = useForm({ schema: configurationFormSchema, serializer: formSerializer, deserializer: formDeserializer, - defaultValue, + defaultValue: value, }); const dispatch = useDispatch(); @@ -114,14 +115,14 @@ export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { useEffect(() => { if (didMountRef.current) { - // If the defaultValue has changed (it probably means that we have loaded a new JSON) + // If the value has changed (it probably means that we have loaded a new JSON) // we need to reset the form to update the fields values. form.reset({ resetValues: true }); } else { // Avoid reseting the form on component mount. didMountRef.current = true; } - }, [defaultValue]); // eslint-disable-line react-hooks/exhaustive-deps + }, [value]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { return () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx index cb9b464d270ce..c1a2b195a3f57 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx @@ -67,6 +67,7 @@ export const DynamicMappingSection = () => ( return ( <> @@ -87,6 +88,7 @@ export const DynamicMappingSection = () => ( } else { return ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx index 68b76a1203ad5..7185016029e00 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx @@ -46,6 +46,7 @@ export const MetaFieldSection = () => ( 'aria-label': i18n.translate('xpack.idxMgmt.mappingsEditor.metaFieldEditorAriaLabel', { defaultMessage: '_meta field data editor', }), + 'data-test-subj': 'metaField', }, }} /> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx index 7f434d6f834b2..f06b292bc33c8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx @@ -35,7 +35,11 @@ export const RoutingSection = () => { /> } > - + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx index f79741d9a1a9f..4278598dfc7c1 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx @@ -65,7 +65,7 @@ export const SourceFieldSection = () => { ); const renderFormFields = () => ( - <> +
{({ label, helpText, value, setValue }) => ( @@ -89,6 +89,7 @@ export const SourceFieldSection = () => { setValue([...(value as ComboBoxOption[]), newOption]); }} fullWidth + data-test-subj="includesField" /> )} @@ -119,11 +120,12 @@ export const SourceFieldSection = () => { setValue([...(value as ComboBoxOption[]), newOption]); }} fullWidth + data-test-subj="excludesField" /> )} - +
); return ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx index a97e3b227311c..569af5d21cdb0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx @@ -25,6 +25,7 @@ interface Props { label?: string; config?: FieldConfig; allowsIndexDefaultOption?: boolean; + 'data-test-subj'?: string; } const ANALYZER_OPTIONS = PARAMETERS_OPTIONS.analyzer!; @@ -68,6 +69,7 @@ export const AnalyzerParameter = ({ label, config, allowsIndexDefaultOption = true, + 'data-test-subj': dataTestSubj, }: Props) => { const indexSettings = useIndexSettings(); const customAnalyzers = getCustomAnalyzers(indexSettings); @@ -131,6 +133,11 @@ export const AnalyzerParameter = ({ !isDefaultValueInOptions && !isDefaultValueInSubOptions ); + const [selectsDefaultValue, setSelectsDefaultValue] = useState({ + main: mainValue, + sub: subValue, + }); + const fieldConfig = config ? config : getFieldConfig('analyzer'); const fieldConfigWithLabel = label !== undefined ? { ...fieldConfig, label } : fieldConfig; @@ -142,6 +149,7 @@ export const AnalyzerParameter = ({ } field.reset({ resetValue: false }); + setSelectsDefaultValue({ main: undefined, sub: undefined }); setIsCustom(!isCustom); }; @@ -154,6 +162,7 @@ export const AnalyzerParameter = ({ size="xs" onClick={toggleCustom(field)} className="mappingsEditor__selectWithCustom__button" + data-test-subj={`${dataTestSubj}-toggleCustomButton`} > {isCustom ? i18n.translate('xpack.idxMgmt.mappingsEditor.predefinedButtonLabel', { @@ -169,17 +178,18 @@ export const AnalyzerParameter = ({ // around the field. - + ) : ( )}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx index a91231352c168..a44fd2257f52b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx @@ -36,6 +36,7 @@ interface Props { config: FieldConfig; options: Options; mapOptionsToSubOptions: MapOptionsToSubOptions; + 'data-test-subj'?: string; } export const AnalyzerParameterSelects = ({ @@ -45,6 +46,7 @@ export const AnalyzerParameterSelects = ({ config, options, mapOptionsToSubOptions, + 'data-test-subj': dataTestSubj, }: Props) => { const { form } = useForm({ defaultValue: { main: mainDefaultValue, sub: subDefaultValue } }); @@ -76,11 +78,16 @@ export const AnalyzerParameterSelects = ({ const isSuperSelect = areOptionsSuperSelect(opts); return isSuperSelect ? ( - + ) : ( ); }; @@ -102,9 +109,9 @@ export const AnalyzerParameterSelects = ({ diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx index 0cf22946bf60a..f99aa4d1eca9a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx @@ -34,6 +34,7 @@ export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: P href: documentationService.getAnalyzerLink(), }} withToggle={false} + data-test-subj="analyzerParameters" > {({ useSameAnalyzerForSearch }) => { @@ -50,6 +51,7 @@ export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: P path="analyzer" label={label} defaultValue={field.source.analyzer as string} + data-test-subj="indexAnalyzer" /> ); }} @@ -60,6 +62,9 @@ export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: P @@ -94,6 +100,7 @@ export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: P path="search_quote_analyzer" defaultValue={field.source.search_quote_analyzer as string} config={getFieldConfig('search_quote_analyzer')} + data-test-subj="searchQuoteAnalyzer" /> )} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx index fec8e49a1991c..3e91e97eef618 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx @@ -39,6 +39,7 @@ export const IndexParameter = ({ href: documentationService.getIndexLink(), }} formFieldPath="index" + data-test-subj="indexParameter" > {/* index_options */} {hasIndexOptions ? ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx index 03c774227924e..2046675881c29 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx @@ -23,7 +23,7 @@ export const AdvancedParametersSection = ({ children }: Props) => {
- + {isVisible ? i18n.translate('xpack.idxMgmt.mappingsEditor.advancedSettings.hideButtonLabel', { defaultMessage: 'Hide advanced settings', @@ -33,7 +33,7 @@ export const AdvancedParametersSection = ({ children }: Props) => { })} -
+
{/* We ned to wrap the children inside a "div" to have our css :first-child rule */}
{children}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 489424a07e04d..854270f313e59 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -96,7 +96,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props
{/* Title */} -

+

{isMultiField ? i18n.translate( 'xpack.idxMgmt.mappingsEditor.editMultiFieldTitle', @@ -127,6 +127,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props href={linkDocumentation} target="_blank" iconType="help" + data-test-subj="documentationLink" > {i18n.translate( 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', @@ -146,7 +147,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props {/* Field path */} - + {field.path.join(' > ')} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx index 97a7d205c1355..1c079c8d5cf87 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx @@ -42,6 +42,7 @@ interface Props { children?: React.ReactNode | ChildrenFunc; withToggle?: boolean; configPath?: ParameterName; + 'data-test-subj'?: string; } export const EditFieldFormRow = React.memo( @@ -54,6 +55,7 @@ export const EditFieldFormRow = React.memo( children, withToggle = true, configPath, + 'data-test-subj': dataTestSubj, }: Props) => { const form = useFormContext(); @@ -87,7 +89,7 @@ export const EditFieldFormRow = React.memo( label={title} checked={isContentVisible} onChange={onToggle} - data-test-subj="input" + data-test-subj="formRowToggle" showLabel={false} /> ) : ( @@ -99,7 +101,17 @@ export const EditFieldFormRow = React.memo( }} > {field => { - return ; + return ( + + ); }} ); @@ -165,7 +177,7 @@ export const EditFieldFormRow = React.memo( ); return ( - + {toggle} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx index 6df86d561a532..c0d922e0d1d37 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -18,7 +18,7 @@ export const FieldsList = React.memo(function FieldsListComponent({ fields, tree return null; } return ( -
    +
      {fields.map((field, index) => (
      {source.name} - + {isMultiField ? i18n.translate('xpack.idxMgmt.mappingsEditor.multiFieldBadgeLabel', { defaultMessage: '{dataType} multi-field', values: { - dataType: TYPE_DEFINITION[source.type].label, + dataType: getTypeLabelFromType(source.type), }, }) : getTypeLabelFromType(source.type)} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx index 3c4d6b08ebe44..f4aa17bf6fed9 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx @@ -16,7 +16,7 @@ import { documentationService } from '../../../../services/documentation'; type MappingsTemplates = Types['MappingsTemplates']; interface Props { - defaultValue?: MappingsTemplates; + value?: MappingsTemplates; } const stringifyJson = (json: { [key: string]: any }) => @@ -50,14 +50,14 @@ const formDeserializer = (formData: { [key: string]: any }) => { }; }; -export const TemplatesForm = React.memo(({ defaultValue }: Props) => { +export const TemplatesForm = React.memo(({ value }: Props) => { const didMountRef = useRef(false); const { form } = useForm({ schema: templatesFormSchema, serializer: formSerializer, deserializer: formDeserializer, - defaultValue, + defaultValue: value, }); const dispatch = useDispatch(); @@ -73,14 +73,14 @@ export const TemplatesForm = React.memo(({ defaultValue }: Props) => { useEffect(() => { if (didMountRef.current) { - // If the defaultValue has changed (it probably means that we have loaded a new JSON) + // If the value has changed (it probably means that we have loaded a new JSON) // we need to reset the form to update the fields values. form.reset({ resetValues: true }); } else { // Avoid reseting the form on component mount. didMountRef.current = true; } - }, [defaultValue]); // eslint-disable-line react-hooks/exhaustive-deps + }, [value]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { return () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index 0431ea472643b..4b610ff0b401d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -6,7 +6,7 @@ jest.mock('../constants', () => ({ MAIN_DATA_TYPE_DEFINITION: {} })); -import { isStateValid } from './utils'; +import { isStateValid, stripUndefinedValues } from './utils'; describe('utils', () => { describe('isStateValid()', () => { @@ -62,4 +62,49 @@ describe('utils', () => { expect(isStateValid(components)).toBe(false); }); }); + + describe('stripUndefinedValues()', () => { + test('should remove all undefined value recursively', () => { + const myDate = new Date(); + + const dataIN = { + someString: 'world', + someNumber: 123, + someBoolean: true, + someArray: [1, 2, 3], + someEmptyObject: {}, + someDate: myDate, + falsey1: 0, + falsey2: '', + stripThis: undefined, + nested: { + value: 'bar', + stripThis: undefined, + deepNested: { + value: 'baz', + stripThis: undefined, + }, + }, + }; + + const dataOUT = { + someString: 'world', + someNumber: 123, + someBoolean: true, + someArray: [1, 2, 3], + someEmptyObject: {}, + someDate: myDate, + falsey1: 0, + falsey2: '', + nested: { + value: 'bar', + deepNested: { + value: 'baz', + }, + }, + }; + + expect(stripUndefinedValues(dataIN)).toEqual(dataOUT); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index cece26618ced8..306e0448df379 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -17,6 +17,7 @@ import { ChildFieldName, ParameterName, ComboBoxOption, + GenericObject, } from '../types'; import { @@ -32,11 +33,9 @@ import { State } from '../reducer'; import { FieldConfig } from '../shared_imports'; import { TreeItem } from '../components/tree'; -export const getUniqueId = () => { - return uuid.v4(); -}; +export const getUniqueId = () => uuid.v4(); -const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { +export const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { if (dataType === 'text' || dataType === 'keyword') { return 'fields'; } else if (dataType === 'object' || dataType === 'nested') { @@ -508,3 +507,39 @@ export const isStateValid = (state: State): boolean | undefined => return isValid && value.isValid; }, true as undefined | boolean); + +/** + * This helper removes all the keys on an object with an "undefined" value. + * To avoid sending updates from the mappings editor with this type of object: + * + *``` + * { + * "dyamic": undefined, + * "date_detection": undefined, + * "dynamic": undefined, + * "dynamic_date_formats": undefined, + * "dynamic_templates": undefined, + * "numeric_detection": undefined, + * "properties": { + * "title": { "type": "text" } + * } + * } + *``` + * + * @param obj The object to retrieve the undefined values from + * @param recursive A flag to strip recursively into children objects + */ +export const stripUndefinedValues = (obj: GenericObject, recursive = true): T => + Object.entries(obj).reduce((acc, [key, value]) => { + if (value === undefined) { + return acc; + } + + if (Array.isArray(value) || value instanceof Date || value === null) { + return { ...acc, [key]: value }; + } + + return recursive && typeof value === 'object' + ? { ...acc, [key]: stripUndefinedValues(value, recursive) } + : { ...acc, [key]: value }; + }, {} as T); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index 316fee55526a3..46dc1176f62b4 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -21,18 +21,18 @@ import { MappingsState, Props as MappingsStateProps, Types } from './mappings_st import { IndexSettingsProvider } from './index_settings_context'; interface Props { - onUpdate: MappingsStateProps['onUpdate']; - defaultValue?: { [key: string]: any }; + onChange: MappingsStateProps['onChange']; + value?: { [key: string]: any }; indexSettings?: IndexSettings; } type TabName = 'fields' | 'advanced' | 'templates'; -export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSettings }: Props) => { +export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Props) => { const [selectedTab, selectTab] = useState('fields'); const { parsedDefaultValue, multipleMappingsDeclared } = useMemo(() => { - const mappingsDefinition = extractMappingsDefinition(defaultValue); + const mappingsDefinition = extractMappingsDefinition(value); if (mappingsDefinition === null) { return { multipleMappingsDeclared: true }; @@ -67,18 +67,18 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting }; return { parsedDefaultValue: parsed, multipleMappingsDeclared: false }; - }, [defaultValue]); + }, [value]); useEffect(() => { if (multipleMappingsDeclared) { // We set the data getter here as the user won't be able to make any changes - onUpdate({ - getData: () => defaultValue! as Types['Mappings'], + onChange({ + getData: () => value! as Types['Mappings'], validate: () => Promise.resolve(true), isValid: true, }); } - }, [multipleMappingsDeclared, onUpdate, defaultValue]); + }, [multipleMappingsDeclared, onChange, value]); const changeTab = async (tab: TabName, state: State) => { if (selectedTab === 'advanced') { @@ -108,12 +108,12 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting ) : ( - + {({ state }) => { const tabToContentMap = { fields: , - templates: , - advanced: , + templates: , + advanced: , }; return ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx index a9d26b953b96e..280ea5c3dd28c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx @@ -16,7 +16,7 @@ import { Dispatch, } from './reducer'; import { Field } from './types'; -import { normalize, deNormalize } from './lib'; +import { normalize, deNormalize, stripUndefinedValues } from './lib'; type Mappings = MappingsTemplates & MappingsConfiguration & { @@ -43,36 +43,34 @@ const DispatchContext = createContext(undefined); export interface Props { children: (params: { state: State }) => React.ReactNode; - defaultValue: { + value: { templates: MappingsTemplates; configuration: MappingsConfiguration; fields: { [key: string]: Field }; }; - onUpdate: OnUpdateHandler; + onChange: OnUpdateHandler; } -export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => { +export const MappingsState = React.memo(({ children, onChange, value }: Props) => { const didMountRef = useRef(false); - const parsedFieldsDefaultValue = useMemo(() => normalize(defaultValue.fields), [ - defaultValue.fields, - ]); + const parsedFieldsDefaultValue = useMemo(() => normalize(value.fields), [value.fields]); const initialState: State = { isValid: undefined, configuration: { - defaultValue: defaultValue.configuration, + defaultValue: value.configuration, data: { - raw: defaultValue.configuration, - format: () => defaultValue.configuration, + raw: value.configuration, + format: () => value.configuration, }, validate: () => Promise.resolve(true), }, templates: { - defaultValue: defaultValue.templates, + defaultValue: value.templates, data: { - raw: defaultValue.templates, - format: () => defaultValue.templates, + raw: value.templates, + format: () => value.templates, }, validate: () => Promise.resolve(true), }, @@ -105,7 +103,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P const bypassFieldFormValidation = state.documentFields.status === 'creatingField' && emptyNameValue; - onUpdate({ + onChange({ // Output a mappings object from the user's input. getData: (isValid: boolean) => { let nextState = state; @@ -135,8 +133,10 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P const templatesData = nextState.templates.data.format(); return { - ...configurationData, - ...templatesData, + ...stripUndefinedValues({ + ...configurationData, + ...templatesData, + }), properties: fields, }; }, @@ -169,26 +169,26 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P }, isValid: state.isValid, }); - }, [state, onUpdate]); + }, [state, onChange]); useEffect(() => { /** - * If the defaultValue has changed that probably means that we have loaded + * If the value has changed that probably means that we have loaded * new data from JSON. We need to update our state with the new mappings. */ if (didMountRef.current) { dispatch({ type: 'editor.replaceMappings', value: { - configuration: defaultValue.configuration, - templates: defaultValue.templates, + configuration: value.configuration, + templates: value.templates, fields: parsedFieldsDefaultValue, }, }); } else { didMountRef.current = true; } - }, [defaultValue, parsedFieldsDefaultValue]); + }, [value, parsedFieldsDefaultValue]); return ( diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx index cf9b57dcbcb14..d74dd435ecdae 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx @@ -101,8 +101,8 @@ export const StepMappings: React.FunctionComponent = ({ {/* Mappings code editor */} diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 2abef7d71e65a..6bbd67ce932c6 100644 --- a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -26,6 +26,7 @@ interface AutocompleteFieldProps { placeholder?: string; suggestions: QuerySuggestion[]; value: string; + disabled?: boolean; autoFocus?: boolean; 'aria-label'?: string; } @@ -55,6 +56,7 @@ export class AutocompleteField extends React.Component< isValid, placeholder, value, + disabled, 'aria-label': ariaLabel, } = this.props; const { areSuggestionsVisible, selectedIndex } = this.state; @@ -64,6 +66,7 @@ export class AutocompleteField extends React.Component< { return ( - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx index 5ff5cd4db7168..16751fabd6e96 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; - import { ColumnarPage } from '../../../components/page'; import { LogEntryRatePageContent } from './page_content'; import { LogEntryRatePageProviders } from './page_providers'; export const LogEntryRatePage = () => { return ( - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/page.tsx b/x-pack/plugins/infra/public/pages/logs/page.tsx index 08049183d0a18..018f89fbb23c4 100644 --- a/x-pack/plugins/infra/public/pages/logs/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page.tsx @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; - import { LogsPageContent } from './page_content'; import { LogsPageProviders } from './page_providers'; -export const LogsPage: React.FunctionComponent = ({ match }) => { +export const LogsPage: React.FunctionComponent = () => { return ( - - - + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx index 88b1441f0ba7c..363b1b7627104 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -7,6 +7,7 @@ import { EuiButton, EuiCallOut, + EuiErrorBoundary, EuiFlexGroup, EuiFlexItem, EuiPanel, @@ -74,7 +75,7 @@ export const LogsSettingsPage = () => { } return ( - <> + { - + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx index 712d625052140..bc25d7c49b129 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { ColumnarPage } from '../../../components/page'; @@ -15,11 +16,13 @@ export const StreamPage = () => { useTrackPageview({ app: 'infra_logs', path: 'stream' }); useTrackPageview({ app: 'infra_logs', path: 'stream', delay: 15000 }); return ( - - - - - - + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index 9667272eb2417..88e6ea8be4325 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -64,6 +64,7 @@ export const LogsToolbar = () => { isLoadingSuggestions={isLoadingSuggestions} isValid={isFilterQueryDraftValid} loadSuggestions={loadSuggestions} + disabled={isStreaming} onChange={(expression: string) => { setSurroundingLogsId(null); setLogFilterQueryDraft(expression); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index dbf71665ea869..91362d9098e34 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiErrorBoundary, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; @@ -36,103 +36,105 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; return ( - - - - - - - - + + + + + + + -
      - - - - - - - - - - - + - - - ( - - {({ configuration, createDerivedIndexPattern }) => ( - - - {configuration ? ( - - ) : ( - - )} - - )} - - )} +
      - - - - - - - + + + + + + + + + + + + + + + ( + + {({ configuration, createDerivedIndexPattern }) => ( + + + {configuration ? ( + + ) : ( + + )} + + )} + + )} + /> + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 3a2c33d1c824c..ebb8243369b3c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiErrorBoundary, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; @@ -41,65 +41,70 @@ export const SnapshotPage = () => { }); return ( - - - i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { - defaultMessage: '{previousTitle} | Inventory', - values: { - previousTitle, - }, - }) - } - /> - {isLoading ? ( - - ) : metricIndicesExist ? ( - <> - - - - ) : hasFailedLoadingSource ? ( - - ) : ( - - - - {i18n.translate('xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', { - defaultMessage: 'View setup instructions', - })} - - - {uiCapabilities?.infrastructure?.configureSource ? ( + + + + i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { + defaultMessage: '{previousTitle} | Inventory', + values: { + previousTitle, + }, + }) + } + /> + {isLoading ? ( + + ) : metricIndicesExist ? ( + <> + + + + ) : hasFailedLoadingSource ? ( + + ) : ( + - - {i18n.translate('xpack.infra.configureSourceActionLabel', { - defaultMessage: 'Change source configuration', - })} - + {i18n.translate( + 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', + { + defaultMessage: 'View setup instructions', + } + )} + - ) : null} - - } - data-test-subj="noMetricsIndicesPrompt" - /> - )} - + {uiCapabilities?.infrastructure?.configureSource ? ( + + + {i18n.translate('xpack.infra.configureSourceActionLabel', { + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + data-test-subj="noMetricsIndicesPrompt" + /> + )} + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx index 597977d9d2735..dcd1c1d949971 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; - import { Source } from '../../../containers/source'; import { MetricsTimeProvider } from './hooks/use_metrics_time'; export const withMetricPageProviders = (Component: React.ComponentType) => ( props: T ) => ( - - - - - + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index a213671e9436e..8b703b1177c8c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import React from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; +import { useTrackPageview } from '../../../../../observability/public'; +import { SourceQuery } from '../../../../common/graphql/types'; import { DocumentTitle } from '../../../components/document_title'; +import { NoData } from '../../../components/empty_states'; import { MetricsExplorerCharts } from './components/charts'; import { MetricsExplorerToolbar } from './components/toolbar'; -import { SourceQuery } from '../../../../common/graphql/types'; -import { NoData } from '../../../components/empty_states'; import { useMetricsExplorerState } from './hooks/use_metric_explorer_state'; -import { useTrackPageview } from '../../../../../observability/public'; interface MetricsExplorerPageProps { source: SourceQuery.Query['source']['configuration']; @@ -45,7 +45,7 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); return ( - + i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', { @@ -95,6 +95,6 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl onTimeChange={handleTimeChange} /> )} - + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings.tsx index 9414eb7d3e564..7d4f35b19da7d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings.tsx @@ -4,16 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; -import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings'; export const MetricsSettingsPage = () => { const uiCapabilities = useKibana().services.application?.capabilities; return ( - + + + ); }; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts new file mode 100644 index 0000000000000..995d415ef3c8f --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -0,0 +1,311 @@ +/* + * 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 { createLogThresholdExecutor } from './log_threshold_executor'; +import { + Comparator, + AlertStates, + LogDocumentCountAlertParams, + Criterion, +} from '../../../../common/alerting/logs/types'; +import { AlertExecutorOptions } from '../../../../../alerting/server'; +import { + alertsMock, + AlertInstanceMock, + AlertServicesMock, +} from '../../../../../alerting/server/mocks'; +import { libsMock } from './mocks'; + +interface AlertTestInstance { + instance: AlertInstanceMock; + actionQueue: any[]; + state: any; +} + +/* + * Mocks + */ +const alertInstances = new Map(); + +const services: AlertServicesMock = alertsMock.createAlertServices(); +services.alertInstanceFactory.mockImplementation((instanceId: string) => { + const alertInstance: AlertTestInstance = { + instance: alertsMock.createAlertInstanceFactory(), + actionQueue: [], + state: {}, + }; + alertInstance.instance.replaceState.mockImplementation((newState: any) => { + alertInstance.state = newState; + return alertInstance.instance; + }); + alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { + alertInstance.actionQueue.push({ id, action }); + return alertInstance.instance; + }); + + alertInstances.set(instanceId, alertInstance); + + return alertInstance.instance; +}); + +/* + * Helper functions + */ +function getAlertState(instanceId: string): AlertStates { + const alert = alertInstances.get(instanceId); + if (alert) { + return alert.state.alertState; + } else { + throw new Error('Could not find alert instance `' + instanceId + '`'); + } +} + +/* + * Executor instance (our test subject) + */ +const executor = (createLogThresholdExecutor('test', libsMock) as unknown) as (opts: { + params: LogDocumentCountAlertParams; + services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; +}) => Promise; + +// Wrapper to test +type Comparison = [number, Comparator, number]; +async function callExecutor( + [value, comparator, threshold]: Comparison, + criteria: Criterion[] = [] +) { + services.callCluster.mockImplementationOnce(async (..._) => ({ count: value })); + + return await executor({ + services, + params: { + count: { value: threshold, comparator }, + timeSize: 1, + timeUnit: 'm', + criteria, + }, + }); +} + +describe('Comparators trigger alerts correctly', () => { + it('does not alert when counts do not reach the threshold', async () => { + await callExecutor([0, Comparator.GT, 1]); + expect(getAlertState('test')).toBe(AlertStates.OK); + + await callExecutor([0, Comparator.GT_OR_EQ, 1]); + expect(getAlertState('test')).toBe(AlertStates.OK); + + await callExecutor([1, Comparator.LT, 0]); + expect(getAlertState('test')).toBe(AlertStates.OK); + + await callExecutor([1, Comparator.LT_OR_EQ, 0]); + expect(getAlertState('test')).toBe(AlertStates.OK); + }); + + it('alerts when counts reach the threshold', async () => { + await callExecutor([2, Comparator.GT, 1]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); + + await callExecutor([1, Comparator.GT_OR_EQ, 1]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); + + await callExecutor([1, Comparator.LT, 2]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); + + await callExecutor([2, Comparator.LT_OR_EQ, 2]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); + }); +}); + +describe('Comparators create the correct ES queries', () => { + beforeEach(() => { + services.callCluster.mockReset(); + }); + + it('Works with `Comparator.EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.EQ, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must: [{ term: { foo: { value: 'bar' } } }], + }, + }, + }); + }); + + it('works with `Comparator.NOT_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_EQ, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must_not: [{ term: { foo: { value: 'bar' } } }], + }, + }, + }); + }); + + it('works with `Comparator.MATCH`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.MATCH, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must: [{ match: { foo: 'bar' } }], + }, + }, + }); + }); + + it('works with `Comparator.NOT_MATCH`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_MATCH, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must_not: [{ match: { foo: 'bar' } }], + }, + }, + }); + }); + + it('works with `Comparator.MATCH_PHRASE`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.MATCH_PHRASE, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must: [{ match_phrase: { foo: 'bar' } }], + }, + }, + }); + }); + + it('works with `Comparator.NOT_MATCH_PHRASE`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_MATCH_PHRASE, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must_not: [{ match_phrase: { foo: 'bar' } }], + }, + }, + }); + }); + + it('works with `Comparator.GT`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.GT, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must: [{ range: { foo: { gt: 1 } } }], + }, + }, + }); + }); + + it('works with `Comparator.GT_OR_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.GT_OR_EQ, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must: [{ range: { foo: { gte: 1 } } }], + }, + }, + }); + }); + + it('works with `Comparator.LT`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.LT, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must: [{ range: { foo: { lt: 1 } } }], + }, + }, + }); + }); + + it('works with `Comparator.LT_OR_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.LT_OR_EQ, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must: [{ range: { foo: { lte: 1 } } }], + }, + }, + }); + }); +}); + +describe('Multiple criteria create the right ES query', () => { + beforeEach(() => { + services.callCluster.mockReset(); + }); + it('works', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [ + { field: 'foo', comparator: Comparator.EQ, value: 'bar' }, + { field: 'http.status', comparator: Comparator.LT, value: 400 }, + ] + ); + + const query = services.callCluster.mock.calls[0][1]!; + expect(query.body).toMatchObject({ + query: { + bool: { + must: [{ term: { foo: { value: 'bar' } } }, { range: { 'http.status': { lt: 400 } } }], + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts new file mode 100644 index 0000000000000..449bc03a922cf --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { InfraBackendLibs } from '../../../infra_types'; + +export const libsMock = { + sources: { + getSourceConfiguration: (savedObjectsClient: any, sourceId: string) => { + return Promise.resolve({ + id: sourceId, + configuration: { + logAlias: 'filebeat-*', + fields: { timestamp: '@timestamp' }, + }, + }); + }, + }, +} as InfraBackendLibs; diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index f451a6b74e299..0c27a3e4b44e3 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -109,7 +109,10 @@ export function PieComponent( return String(d); }, fillLabel: - isDarkMode && shape === 'treemap' && layerIndex < columnGroups.length - 1 + isDarkMode && + shape === 'treemap' && + layerIndex < columnGroups.length - 1 && + categoryDisplay !== 'hide' ? { ...fillLabel, textColor: euiDarkVars.euiTextColor } : fillLabel, shape: { @@ -252,6 +255,7 @@ export function PieComponent( valueFormatter={(d: number) => (hideLabels ? '' : formatters[metricColumn.id].convert(d))} layers={layers} config={config} + topGroove={hideLabels || categoryDisplay === 'hide' ? 0 : undefined} /> diff --git a/x-pack/plugins/lens/public/pie_visualization/settings_widget.tsx b/x-pack/plugins/lens/public/pie_visualization/settings_widget.tsx index 5a02b91efc749..bb63ceceb2b1b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/settings_widget.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/settings_widget.tsx @@ -66,6 +66,24 @@ const categoryOptions: Array<{ }, ]; +const categoryOptionsTreemap: Array<{ + value: SharedLayerState['categoryDisplay']; + inputDisplay: string; +}> = [ + { + value: 'default', + inputDisplay: i18n.translate('xpack.lens.pieChart.showTreemapCategoriesLabel', { + defaultMessage: 'Show labels', + }), + }, + { + value: 'hide', + inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', { + defaultMessage: 'Hide labels', + }), + }, +]; + const legendOptions: Array<{ value: SharedLayerState['legendDisplay']; label: string; @@ -113,7 +131,7 @@ export function SettingsWidget(props: VisualizationLayerWidgetProps { setState({ ...state, diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts index 7935d53f56845..20b267caa9074 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -508,7 +508,7 @@ describe('suggestions', () => { metric: 'b', numberDisplay: 'hidden', - categoryDisplay: 'inside', + categoryDisplay: 'default', // This is changed legendDisplay: 'show', percentDecimals: 0, nestedLegend: true, diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts index e363cf922b356..16c8fda3807db 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts @@ -115,6 +115,10 @@ export function suggestions({ layerId: table.layerId, groups: groups.map(col => col.columnId), metric: metrics[0].columnId, + categoryDisplay: + state.layers[0].categoryDisplay === 'inside' + ? 'default' + : state.layers[0].categoryDisplay, } : { layerId: table.layerId, diff --git a/x-pack/plugins/lists/server/services/mocks/lists_services_mock_constants.ts b/x-pack/plugins/lists/common/constants.mock.ts similarity index 100% rename from x-pack/plugins/lists/server/services/mocks/lists_services_mock_constants.ts rename to x-pack/plugins/lists/common/constants.mock.ts diff --git a/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts b/x-pack/plugins/lists/common/get_call_cluster.mock.ts similarity index 86% rename from x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts rename to x-pack/plugins/lists/common/get_call_cluster.mock.ts index 180ecbb797339..f036605a6a174 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts +++ b/x-pack/plugins/lists/common/get_call_cluster.mock.ts @@ -7,8 +7,8 @@ import { CreateDocumentResponse } from 'elasticsearch'; import { APICaller } from 'kibana/server'; -import { LIST_INDEX } from './lists_services_mock_constants'; -import { getShardMock } from './get_shard_mock'; +import { LIST_INDEX } from './constants.mock'; +import { getShardMock } from './get_shard.mock'; export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => ({ _id: 'elastic-id-123', diff --git a/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts b/x-pack/plugins/lists/common/get_shard.mock.ts similarity index 100% rename from x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts rename to x-pack/plugins/lists/common/get_shard.mock.ts diff --git a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.mock.ts similarity index 94% rename from x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts rename to x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.mock.ts index 574e4afcb36f0..1e27e48aac310 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.mock.ts @@ -5,8 +5,7 @@ */ import { IndexEsListItemSchema } from '../../../common/schemas'; - -import { DATE_NOW, LIST_ID, META, TIE_BREAKER, USER, VALUE } from './lists_services_mock_constants'; +import { DATE_NOW, LIST_ID, META, TIE_BREAKER, USER, VALUE } from '../../../common/constants.mock'; export const getIndexESListItemMock = (ip = VALUE): IndexEsListItemSchema => ({ created_at: DATE_NOW, diff --git a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.mock.ts similarity index 93% rename from x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts rename to x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.mock.ts index 4e4d8d9c572e4..a6411ebce84b6 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.mock.ts @@ -5,7 +5,6 @@ */ import { IndexEsListSchema } from '../../../common/schemas'; - import { DATE_NOW, DESCRIPTION, @@ -14,7 +13,7 @@ import { TIE_BREAKER, TYPE, USER, -} from './lists_services_mock_constants'; +} from '../../../common/constants.mock'; export const getIndexESListMock = (): IndexEsListSchema => ({ created_at: DATE_NOW, diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts similarity index 61% rename from x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts rename to x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts index 9f877c8168cca..ba69bee9ccf77 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts @@ -7,10 +7,29 @@ import { SearchResponse } from 'elasticsearch'; import { SearchEsListItemSchema } from '../../../common/schemas'; +import { + DATE_NOW, + LIST_ID, + LIST_INDEX, + LIST_ITEM_ID, + META, + TIE_BREAKER, + USER, + VALUE, +} from '../../../common/constants.mock'; +import { getShardMock } from '../../get_shard.mock'; -import { getShardMock } from './get_shard_mock'; -import { LIST_INDEX, LIST_ITEM_ID } from './lists_services_mock_constants'; -import { getSearchEsListItemMock } from './get_search_es_list_item_mock'; +export const getSearchEsListItemMock = (): SearchEsListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + ip: VALUE, + keyword: undefined, + list_id: LIST_ID, + meta: META, + tie_breaker_id: TIE_BREAKER, + updated_at: DATE_NOW, + updated_by: USER, +}); export const getSearchListItemMock = (): SearchResponse => ({ _scroll_id: '123', diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.mock.ts similarity index 61% rename from x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts rename to x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.mock.ts index 9728139eab42a..ca9c4e16c6939 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.mock.ts @@ -7,10 +7,30 @@ import { SearchResponse } from 'elasticsearch'; import { SearchEsListSchema } from '../../../common/schemas'; +import { + DATE_NOW, + DESCRIPTION, + LIST_ID, + LIST_INDEX, + META, + NAME, + TIE_BREAKER, + TYPE, + USER, +} from '../../../common/constants.mock'; +import { getShardMock } from '../../get_shard.mock'; -import { getShardMock } from './get_shard_mock'; -import { LIST_ID, LIST_INDEX } from './lists_services_mock_constants'; -import { getSearchEsListMock } from './get_search_es_list_mock'; +export const getSearchEsListMock = (): SearchEsListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + meta: META, + name: NAME, + tie_breaker_id: TIE_BREAKER, + type: TYPE, + updated_at: DATE_NOW, + updated_by: USER, +}); export const getSearchListMock = (): SearchResponse => ({ _scroll_id: '123', diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f0d4af520bdbb --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LIST_ID, LIST_ITEM_ID, META, VALUE } from '../../constants.mock'; + +import { CreateListItemSchema } from './create_list_item_schema'; + +export const getCreateListItemSchemaMock = (): CreateListItemSchema => ({ + id: LIST_ITEM_ID, + list_id: LIST_ID, + meta: META, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts new file mode 100644 index 0000000000000..8178d49690e39 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getCreateListItemSchemaMock } from './create_list_item_schema.mock'; +import { CreateListItemSchema, createListItemSchema } from './create_list_item_schema'; + +describe('create_list_item_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getCreateListItemSchemaMock(); + const decoded = createListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for an id', () => { + const payload = getCreateListItemSchemaMock(); + delete payload.id; + const decoded = createListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for meta', () => { + const payload = getCreateListItemSchemaMock(); + delete payload.meta; + const decoded = createListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: CreateListItemSchema & { extraKey?: string } = getCreateListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = createListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts index 8168e5a9838f2..6cba81e47fbcc 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -9,14 +9,17 @@ import * as t from 'io-ts'; import { idOrUndefined, list_id, metaOrUndefined, value } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; -export const createListItemSchema = t.exact( - t.type({ - id: idOrUndefined, - list_id, - meta: metaOrUndefined, - value, - }) -); +export const createListItemSchema = t.intersection([ + t.exact( + t.type({ + list_id, + value, + }) + ), + t.exact(t.partial({ id: idOrUndefined, meta: metaOrUndefined })), +]); -export type CreateListItemSchema = t.TypeOf; +export type CreateListItemSchemaPartial = Identity>; +export type CreateListItemSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.mock.ts new file mode 100644 index 0000000000000..7e6d8bb5ad803 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.mock.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 { DESCRIPTION, LIST_ID, META, NAME, TYPE } from '../../constants.mock'; + +import { CreateListSchema } from './create_list_schema'; + +export const getCreateListSchemaMock = (): CreateListSchema => ({ + description: DESCRIPTION, + id: LIST_ID, + meta: META, + name: NAME, + type: TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts index ba791a55d17eb..c4456bf97865a 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts @@ -9,23 +9,47 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; -import { getListRequest } from './mocks/utils'; -import { createListSchema } from './create_list_schema'; +import { CreateListSchema, createListSchema } from './create_list_schema'; +import { getCreateListSchemaMock } from './create_list_schema.mock'; describe('create_list_schema', () => { - // TODO: Finish the tests for this test('it should validate a typical lists request', () => { - const payload = getListRequest(); + const payload = getCreateListSchemaMock(); const decoded = createListSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - description: 'Description of a list item', - id: 'some-list-id', - name: 'Name of a list item', - type: 'ip', - }); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for an id', () => { + const payload = getCreateListSchemaMock(); + delete payload.id; + const decoded = createListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for meta', () => { + const payload = getCreateListSchemaMock(); + delete payload.meta; + const decoded = createListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: CreateListSchema & { extraKey?: string } = getCreateListSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = createListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); }); }); diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 353a4ecdafa0c..7a6e2a707873c 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -4,20 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable @typescript-eslint/camelcase */ - import * as t from 'io-ts'; import { description, idOrUndefined, metaOrUndefined, name, type } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; -export const createListSchema = t.exact( - t.type({ - description, - id: idOrUndefined, - meta: metaOrUndefined, - name, - type, - }) -); +export const createListSchema = t.intersection([ + t.exact( + t.type({ + description, + name, + type, + }) + ), + t.exact(t.partial({ id: idOrUndefined, meta: metaOrUndefined })), +]); -export type CreateListSchema = t.TypeOf; +export type CreateListSchemaPartial = Identity>; +export type CreateListSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts index f4c1fb5c43eb0..96f054b304962 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -9,13 +9,16 @@ import * as t from 'io-ts'; import { idOrUndefined, list_idOrUndefined, valueOrUndefined } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; -export const deleteListItemSchema = t.exact( - t.type({ - id: idOrUndefined, - list_id: list_idOrUndefined, - value: valueOrUndefined, - }) -); +export const deleteListItemSchema = t.intersection([ + t.exact( + t.type({ + value: valueOrUndefined, + }) + ), + t.exact(t.partial({ id: idOrUndefined, list_id: list_idOrUndefined })), +]); -export type DeleteListItemSchema = t.TypeOf; +export type DeleteListItemSchemaPartial = Identity>; +export type DeleteListItemSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.mock.ts similarity index 50% rename from x-pack/plugins/lists/common/schemas/request/mocks/utils.ts rename to x-pack/plugins/lists/common/schemas/request/delete_list_schema.mock.ts index e5d189db8490b..bc0fb7c479c50 100644 --- a/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.mock.ts @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CreateListSchema } from '../create_list_schema'; +import { LIST_ID } from '../../constants.mock'; -export const getListRequest = (): CreateListSchema => ({ - description: 'Description of a list item', - id: 'some-list-id', - meta: undefined, - name: 'Name of a list item', - type: 'ip', +import { DeleteListSchema } from './delete_list_schema'; + +export const getDeleteListSchemaMock = (): DeleteListSchema => ({ + id: LIST_ID, }); diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts new file mode 100644 index 0000000000000..278508305c6f0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { DeleteListSchema, deleteListSchema } from './delete_list_schema'; +import { getDeleteListSchemaMock } from './delete_list_schema.mock'; + +describe('delete_list_schema', () => { + test('it should validate a typical lists request', () => { + const payload = getDeleteListSchemaMock(); + const decoded = deleteListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for an id', () => { + const payload = getDeleteListSchemaMock(); + delete payload.id; + const decoded = deleteListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: DeleteListSchema & { extraKey?: string } = getDeleteListSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = deleteListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.mock.ts new file mode 100644 index 0000000000000..7914cc86328ed --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.mock.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LIST_ID } from '../../constants.mock'; + +import { ExportListItemQuerySchema } from './export_list_item_query_schema'; + +export const getExportListItemQuerySchemaMock = (): ExportListItemQuerySchema => ({ + list_id: LIST_ID, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts new file mode 100644 index 0000000000000..1ffe2e2fc4ecc --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { + ExportListItemQuerySchema, + exportListItemQuerySchema, +} from './export_list_item_query_schema'; +import { getExportListItemQuerySchemaMock } from './export_list_item_query_schema.mock'; + +describe('export_list_item_schema', () => { + test('it should validate a typical lists request', () => { + const payload = getExportListItemQuerySchemaMock(); + const decoded = exportListItemQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for an id', () => { + const payload = getExportListItemQuerySchemaMock(); + delete payload.list_id; + const decoded = exportListItemQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "list_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ExportListItemQuerySchema & { + extraKey?: string; + } = getExportListItemQuerySchemaMock(); + payload.extraKey = 'some new value'; + const decoded = exportListItemQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.mock.ts new file mode 100644 index 0000000000000..6713083e6a49b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LIST_ID, TYPE } from '../../constants.mock'; + +import { ImportListItemQuerySchema } from './import_list_item_query_schema'; + +export const getImportListItemQuerySchemaMock = (): ImportListItemQuerySchema => ({ + list_id: LIST_ID, + type: TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts new file mode 100644 index 0000000000000..ac007a704b92d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { + ImportListItemQuerySchema, + importListItemQuerySchema, +} from './import_list_item_query_schema'; +import { getImportListItemQuerySchemaMock } from './import_list_item_query_schema.mock'; + +describe('import_list_item_schema', () => { + test('it should validate a typical lists request', () => { + const payload = getImportListItemQuerySchemaMock(); + const decoded = importListItemQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "list_id"', () => { + const payload = getImportListItemQuerySchemaMock(); + delete payload.list_id; + const decoded = importListItemQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "type"', () => { + const payload = getImportListItemQuerySchemaMock(); + delete payload.type; + const decoded = importListItemQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "type" and "list_id', () => { + const payload = getImportListItemQuerySchemaMock(); + delete payload.type; + delete payload.list_id; + const decoded = importListItemQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ImportListItemQuerySchema & { + extraKey?: string; + } = getImportListItemQuerySchemaMock(); + payload.extraKey = 'some new value'; + const decoded = importListItemQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts index b8467d141bdd8..c1745dda7afab 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts @@ -9,9 +9,13 @@ import * as t from 'io-ts'; import { list_idOrUndefined, typeOrUndefined } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; export const importListItemQuerySchema = t.exact( - t.type({ list_id: list_idOrUndefined, type: typeOrUndefined }) + t.partial({ list_id: list_idOrUndefined, type: typeOrUndefined }) ); -export type ImportListItemQuerySchema = t.TypeOf; +export type ImportListItemQuerySchemaPartial = Identity>; +export type ImportListItemQuerySchema = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.mock.ts new file mode 100644 index 0000000000000..69e4d2f8293c7 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.mock.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ImportListItemSchema } from './import_list_item_schema'; + +export const getImportListItemSchemaMock = (): ImportListItemSchema => ({ + file: {}, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts new file mode 100644 index 0000000000000..7f7c6368a1c5e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { ImportListItemSchema, importListItemSchema } from './import_list_item_schema'; +import { getImportListItemSchemaMock } from './import_list_item_schema.mock'; + +describe('import_list_item_schema', () => { + test('it should validate a typical lists request', () => { + const payload = getImportListItemSchemaMock(); + const decoded = importListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for a file', () => { + const payload = getImportListItemSchemaMock(); + delete payload.file; + const decoded = importListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "file"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ImportListItemSchema & { + extraKey?: string; + } = getImportListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = importListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts index 0cf01db8617f0..94299c93b29d8 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts @@ -18,6 +18,8 @@ export const importListItemSchema = t.exact( }) ); +export type ImportListItemSchema = t.TypeOf; + export interface HapiReadableStream extends Readable { hapi: { filename: string; @@ -27,6 +29,6 @@ export interface HapiReadableStream extends Readable { /** * Special interface since we are streaming in a file through a reader */ -export interface ImportListItemSchema { +export interface ImportListItemHapiFileSchema { file: HapiReadableStream; } diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f5113bd55d44f --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LIST_ITEM_ID, META, VALUE } from '../../constants.mock'; + +import { PatchListItemSchema } from './patch_list_item_schema'; + +export const getPathListItemSchemaMock = (): PatchListItemSchema => ({ + id: LIST_ITEM_ID, + meta: META, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts new file mode 100644 index 0000000000000..58c19e8f9cb4f --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getPathListItemSchemaMock } from './patch_list_item_schema.mock'; +import { PatchListItemSchema, patchListItemSchema } from './patch_list_item_schema'; + +describe('patch_list_item_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getPathListItemSchemaMock(); + const decoded = patchListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "id"', () => { + const payload = getPathListItemSchemaMock(); + delete payload.id; + const decoded = patchListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should accept an undefined for "meta"', () => { + const payload = getPathListItemSchemaMock(); + delete payload.meta; + const decoded = patchListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "value"', () => { + const payload = getPathListItemSchemaMock(); + delete payload.value; + const decoded = patchListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "meta" and "value"', () => { + const payload = getPathListItemSchemaMock(); + delete payload.meta; + delete payload.value; + const decoded = patchListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: PatchListItemSchema & { extraKey?: string } = getPathListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = patchListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts index 3e8198a5109b3..536931f715f3f 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -9,13 +9,16 @@ import * as t from 'io-ts'; import { id, metaOrUndefined, valueOrUndefined } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; -export const patchListItemSchema = t.exact( - t.type({ - id, - meta: metaOrUndefined, - value: valueOrUndefined, - }) -); +export const patchListItemSchema = t.intersection([ + t.exact( + t.type({ + id, + }) + ), + t.exact(t.partial({ meta: metaOrUndefined, value: valueOrUndefined })), +]); -export type PatchListItemSchema = t.TypeOf; +export type PatchListItemSchemaPartial = Identity>; +export type PatchListItemSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.mock.ts new file mode 100644 index 0000000000000..70e02944a46de --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DESCRIPTION, LIST_ITEM_ID, META, NAME } from '../../constants.mock'; + +import { PatchListSchema } from './patch_list_schema'; + +export const getPathListSchemaMock = (): PatchListSchema => ({ + description: DESCRIPTION, + id: LIST_ITEM_ID, + meta: META, + name: NAME, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts new file mode 100644 index 0000000000000..3ab658014bbfa --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getPathListSchemaMock } from './patch_list_schema.mock'; +import { PatchListSchema, patchListSchema } from './patch_list_schema'; + +describe('patch_list_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getPathListSchemaMock(); + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "id"', () => { + const payload = getPathListSchemaMock(); + delete payload.id; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should accept an undefined for "meta"', () => { + const payload = getPathListSchemaMock(); + delete payload.meta; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "name"', () => { + const payload = getPathListSchemaMock(); + delete payload.name; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "description"', () => { + const payload = getPathListSchemaMock(); + delete payload.description; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "description", "meta", "name', () => { + const payload = getPathListSchemaMock(); + delete payload.description; + delete payload.name; + delete payload.meta; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "description", "meta"', () => { + const payload = getPathListSchemaMock(); + delete payload.description; + delete payload.meta; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "description", "name"', () => { + const payload = getPathListSchemaMock(); + delete payload.description; + delete payload.name; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "meta", "name"', () => { + const payload = getPathListSchemaMock(); + delete payload.meta; + delete payload.name; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: PatchListSchema & { extraKey?: string } = getPathListSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = patchListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts index efcb81fc8be2a..59d1a66a581a0 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -9,14 +9,18 @@ import * as t from 'io-ts'; import { descriptionOrUndefined, id, metaOrUndefined, nameOrUndefined } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; -export const patchListSchema = t.exact( - t.type({ - description: descriptionOrUndefined, - id, - meta: metaOrUndefined, - name: nameOrUndefined, - }) -); +export const patchListSchema = t.intersection([ + t.exact( + t.type({ + id, + }) + ), + t.exact( + t.partial({ description: descriptionOrUndefined, meta: metaOrUndefined, name: nameOrUndefined }) + ), +]); -export type PatchListSchema = t.TypeOf; +export type PatchListSchemaPartial = Identity>; +export type PatchListSchema = RequiredKeepUndefined>>; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.mock.ts new file mode 100644 index 0000000000000..51d5745b0364d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LIST_ID, LIST_ITEM_ID, VALUE } from '../../constants.mock'; + +import { ReadListItemSchema } from './read_list_item_schema'; + +export const getReadListItemSchemaMock = (): ReadListItemSchema => ({ + id: LIST_ITEM_ID, + list_id: LIST_ID, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts new file mode 100644 index 0000000000000..5c71c9820cc1e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts @@ -0,0 +1,117 @@ +/* + * 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getReadListItemSchemaMock } from './read_list_item_schema.mock'; +import { ReadListItemSchema, readListItemSchema } from './read_list_item_schema'; + +describe('read_list_item_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getReadListItemSchemaMock(); + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "id"', () => { + const payload = getReadListItemSchemaMock(); + delete payload.id; + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "list_id"', () => { + const payload = getReadListItemSchemaMock(); + delete payload.list_id; + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "value"', () => { + const payload = getReadListItemSchemaMock(); + delete payload.value; + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "id", "list_id", "value"', () => { + const payload = getReadListItemSchemaMock(); + delete payload.id; + delete payload.value; + delete payload.list_id; + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "id", "list_id"', () => { + const payload = getReadListItemSchemaMock(); + delete payload.id; + delete payload.list_id; + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "id", "value"', () => { + const payload = getReadListItemSchemaMock(); + delete payload.id; + delete payload.value; + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "list_id", "value"', () => { + const payload = getReadListItemSchemaMock(); + delete payload.value; + delete payload.list_id; + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ReadListItemSchema & { extraKey?: string } = getReadListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = readListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts index 9ea14a2a21ed8..b69523b664fd7 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts @@ -9,9 +9,11 @@ import * as t from 'io-ts'; import { idOrUndefined, list_idOrUndefined, valueOrUndefined } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; export const readListItemSchema = t.exact( - t.type({ id: idOrUndefined, list_id: list_idOrUndefined, value: valueOrUndefined }) + t.partial({ id: idOrUndefined, list_id: list_idOrUndefined, value: valueOrUndefined }) ); -export type ReadListItemSchema = t.TypeOf; +export type ReadListItemSchemaPartial = Identity>; +export type ReadListItemSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.mock.ts new file mode 100644 index 0000000000000..bbe71488f59de --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.mock.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LIST_ID } from '../../constants.mock'; + +import { ReadListSchema } from './read_list_schema'; + +export const getReadListSchemaMock = (): ReadListSchema => ({ + id: LIST_ID, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts new file mode 100644 index 0000000000000..a1ba2655dd723 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getReadListSchemaMock } from './read_list_schema.mock'; +import { ReadListSchema, readListSchema } from './read_list_schema'; + +describe('read_list_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getReadListSchemaMock(); + const decoded = readListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "id"', () => { + const payload = getReadListSchemaMock(); + delete payload.id; + const decoded = readListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ReadListSchema & { extraKey?: string } = getReadListSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = readListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts index e1f88bae66e0f..23701ff753bc0 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -9,13 +9,17 @@ import * as t from 'io-ts'; import { id, metaOrUndefined, value } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; -export const updateListItemSchema = t.exact( - t.type({ - id, - meta: metaOrUndefined, - value, - }) -); +export const updateListItemSchema = t.intersection([ + t.exact( + t.type({ + id, + value, + }) + ), + t.exact(t.partial({ meta: metaOrUndefined })), +]); -export type UpdateListItemSchema = t.TypeOf; +export type UpdateListItemSchemaPartial = Identity>; +export type UpdateListItemSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts index d51ed60c41b56..8223a6a34b771 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -9,14 +9,18 @@ import * as t from 'io-ts'; import { description, id, metaOrUndefined, name } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; -export const updateListSchema = t.exact( - t.type({ - description, - id, - meta: metaOrUndefined, - name, - }) -); +export const updateListSchema = t.intersection([ + t.exact( + t.type({ + description, + id, + name, + }) + ), + t.exact(t.partial({ meta: metaOrUndefined })), +]); -export type UpdateListSchema = t.TypeOf; +export type UpdateListSchemaPartial = Identity>; +export type UpdateListSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.mock.ts new file mode 100644 index 0000000000000..905b73cabda97 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.mock.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AcknowledgeSchema } from './acknowledge_schema'; + +export const getAcknowledgeSchemaResponseMock = (): AcknowledgeSchema => ({ + acknowledged: true, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts new file mode 100644 index 0000000000000..6e7fb158767b5 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getAcknowledgeSchemaResponseMock } from './acknowledge_schema.mock'; +import { AcknowledgeSchema, acknowledgeSchema } from './acknowledge_schema'; + +describe('acknowledge_schema', () => { + test('it should validate a typical response', () => { + const payload = getAcknowledgeSchemaResponseMock(); + const decoded = acknowledgeSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + test('it should NOT accept an undefined for "ok"', () => { + const payload = getAcknowledgeSchemaResponseMock(); + delete payload.acknowledged; + const decoded = acknowledgeSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "acknowledged"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: AcknowledgeSchema & { extraKey?: string } = getAcknowledgeSchemaResponseMock(); + payload.extraKey = 'some new value'; + const decoded = acknowledgeSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts index 55aaf587ac06b..bf74db516e1a9 100644 --- a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts @@ -6,6 +6,6 @@ import * as t from 'io-ts'; -export const acknowledgeSchema = t.type({ acknowledged: t.boolean }); +export const acknowledgeSchema = t.exact(t.type({ acknowledged: t.boolean })); export type AcknowledgeSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.mock.ts new file mode 100644 index 0000000000000..2551020e3b5a4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.mock.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 { ListItemIndexExistSchema } from './list_item_index_exist_schema'; + +export const getListItemIndexExistSchemaResponseMock = (): ListItemIndexExistSchema => ({ + list_index: true, + list_item_index: true, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts new file mode 100644 index 0000000000000..9cb130ec0e8ad --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getListItemIndexExistSchemaResponseMock } from './list_item_index_exist_schema.mock'; +import { ListItemIndexExistSchema, listItemIndexExistSchema } from './list_item_index_exist_schema'; + +describe('list_item_index_exist_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getListItemIndexExistSchemaResponseMock(); + const decoded = listItemIndexExistSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "list_index"', () => { + const payload = getListItemIndexExistSchemaResponseMock(); + delete payload.list_index; + const decoded = listItemIndexExistSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "list_index"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "list_item_index"', () => { + const payload = getListItemIndexExistSchemaResponseMock(); + delete payload.list_item_index; + const decoded = listItemIndexExistSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "list_item_index"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ListItemIndexExistSchema & { + extraKey?: string; + } = getListItemIndexExistSchemaResponseMock(); + payload.extraKey = 'some new value'; + const decoded = listItemIndexExistSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts index bf2bf21d2c216..4c7a1fdaf8d4b 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts @@ -6,9 +6,11 @@ import * as t from 'io-ts'; -export const listItemIndexExistSchema = t.type({ - list_index: t.boolean, - list_item_index: t.boolean, -}); +export const listItemIndexExistSchema = t.exact( + t.type({ + list_index: t.boolean, + list_item_index: t.boolean, + }) +); export type ListItemIndexExistSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.mock.ts similarity index 72% rename from x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts rename to x-pack/plugins/lists/common/schemas/response/list_item_schema.mock.ts index 1a30282ddaeba..309aeaa477c66 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.mock.ts @@ -5,17 +5,25 @@ */ import { ListItemSchema } from '../../../common/schemas'; - -import { DATE_NOW, LIST_ID, LIST_ITEM_ID, USER, VALUE } from './lists_services_mock_constants'; +import { + DATE_NOW, + LIST_ID, + LIST_ITEM_ID, + META, + TIE_BREAKER, + TYPE, + USER, + VALUE, +} from '../../../common/constants.mock'; export const getListItemResponseMock = (): ListItemSchema => ({ created_at: DATE_NOW, created_by: USER, id: LIST_ITEM_ID, list_id: LIST_ID, - meta: {}, - tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', - type: 'ip', + meta: META, + tie_breaker_id: TIE_BREAKER, + type: TYPE, updated_at: DATE_NOW, updated_by: USER, value: VALUE, diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts new file mode 100644 index 0000000000000..fbffd1d3ef245 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getListItemResponseMock } from './list_item_schema.mock'; +import { ListItemSchema, listItemSchema } from './list_item_schema'; + +describe('list_item_schema', () => { + test('it should validate a typical list item response', () => { + const payload = getListItemResponseMock(); + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "id"', () => { + const payload = getListItemResponseMock(); + delete payload.id; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "list_id"', () => { + const payload = getListItemResponseMock(); + delete payload.list_id; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "list_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should accept an undefined for "meta"', () => { + const payload = getListItemResponseMock(); + delete payload.meta; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "created_at"', () => { + const payload = getListItemResponseMock(); + delete payload.created_at; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "created_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "created_by"', () => { + const payload = getListItemResponseMock(); + delete payload.created_by; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "created_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "tie_breaker_id"', () => { + const payload = getListItemResponseMock(); + delete payload.tie_breaker_id; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "tie_breaker_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "type"', () => { + const payload = getListItemResponseMock(); + delete payload.type; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "updated_at"', () => { + const payload = getListItemResponseMock(); + delete payload.updated_at; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "updated_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "updated_by"', () => { + const payload = getListItemResponseMock(); + delete payload.updated_by; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "updated_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "value"', () => { + const payload = getListItemResponseMock(); + delete payload.value; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ListItemSchema & { extraKey?: string } = getListItemResponseMock(); + payload.extraKey = 'some new value'; + const decoded = listItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.mock.ts similarity index 72% rename from x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts rename to x-pack/plugins/lists/common/schemas/response/list_schema.mock.ts index ea068d774c4ed..5016252bc564a 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.mock.ts @@ -5,18 +5,26 @@ */ import { ListSchema } from '../../../common/schemas'; - -import { DATE_NOW, DESCRIPTION, LIST_ID, NAME, USER } from './lists_services_mock_constants'; +import { + DATE_NOW, + DESCRIPTION, + LIST_ID, + META, + NAME, + TIE_BREAKER, + TYPE, + USER, +} from '../../../common/constants.mock'; export const getListResponseMock = (): ListSchema => ({ created_at: DATE_NOW, created_by: USER, description: DESCRIPTION, id: LIST_ID, - meta: {}, + meta: META, name: NAME, - tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', - type: 'ip', + tie_breaker_id: TIE_BREAKER, + type: TYPE, updated_at: DATE_NOW, updated_by: USER, }); diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts new file mode 100644 index 0000000000000..a37207271c06e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getListResponseMock } from './list_schema.mock'; +import { ListSchema, listSchema } from './list_schema'; + +describe('list_schema', () => { + test('it should validate a typical list response', () => { + const payload = getListResponseMock(); + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "id"', () => { + const payload = getListResponseMock(); + delete payload.id; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should accept an undefined for "meta"', () => { + const payload = getListResponseMock(); + delete payload.meta; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "created_at"', () => { + const payload = getListResponseMock(); + delete payload.created_at; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "created_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "created_by"', () => { + const payload = getListResponseMock(); + delete payload.created_by; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "created_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "tie_breaker_id"', () => { + const payload = getListResponseMock(); + delete payload.tie_breaker_id; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "tie_breaker_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "type"', () => { + const payload = getListResponseMock(); + delete payload.type; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "updated_at"', () => { + const payload = getListResponseMock(); + delete payload.updated_at; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "updated_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "updated_by"', () => { + const payload = getListResponseMock(); + delete payload.updated_by; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "updated_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "name"', () => { + const payload = getListResponseMock(); + delete payload.name; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "description"', () => { + const payload = getListResponseMock(); + delete payload.description; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ListSchema & { extraKey?: string } = getListResponseMock(); + payload.extraKey = 'some new value'; + const decoded = listSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/server/services/mocks/test_readable.ts b/x-pack/plugins/lists/common/test_readable.mock.ts similarity index 100% rename from x-pack/plugins/lists/server/services/mocks/test_readable.ts rename to x-pack/plugins/lists/common/test_readable.mock.ts diff --git a/x-pack/plugins/lists/common/types.ts b/x-pack/plugins/lists/common/types.ts new file mode 100644 index 0000000000000..1539c5ae01ff5 --- /dev/null +++ b/x-pack/plugins/lists/common/types.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * This makes any optional property the same as Required would but also has the + * added benefit of keeping your undefined. + * + * For example: + * type A = RequiredKeepUndefined<{ a?: undefined; b: number }>; + * + * will yield a type of: + * type A = { a: undefined; b: number; } + * + */ +export type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infer U + ? U extends Record + ? { [K in keyof U]: U[K][0] } + : never + : never; + +/** + * This is just a helper to cleanup nasty intersections and unions to make them + * readable from io.ts, it's an identity that strips away the uglyness of them. + */ +export type Identity = { + [P in keyof T]: T[P]; +}; diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index 2498c36967a53..5facf981c098e 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -5,7 +5,7 @@ */ import { first } from 'rxjs/operators'; -import { Logger, PluginInitializerContext } from 'kibana/server'; +import { Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { CoreSetup } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -14,12 +14,19 @@ import { SpacesServiceSetup } from '../../spaces/server'; import { ConfigType } from './config'; import { initRoutes } from './routes/init_routes'; import { ListClient } from './services/lists/client'; -import { ContextProvider, ContextProviderReturn, PluginsSetup } from './types'; +import { + ContextProvider, + ContextProviderReturn, + ListPluginSetup, + ListsPluginStart, + PluginsSetup, +} from './types'; import { createConfig$ } from './create_config'; import { getSpaceId } from './get_space_id'; import { getUser } from './get_user'; -export class ListPlugin { +export class ListPlugin + implements Plugin, ListsPluginStart, PluginsSetup> { private readonly logger: Logger; private spaces: SpacesServiceSetup | undefined | null; private config: ConfigType | undefined | null; @@ -29,7 +36,7 @@ export class ListPlugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { const config = await createConfig$(this.initializerContext) .pipe(first()) .toPromise(); @@ -44,6 +51,17 @@ export class ListPlugin { core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext()); const router = core.http.createRouter(); initRoutes(router); + + return { + getListClient: (apiCaller, spaceId, user): ListClient => { + return new ListClient({ + callCluster: apiCaller, + config, + spaceId, + user, + }); + }, + }; } public start(): void { @@ -74,8 +92,6 @@ export class ListPlugin { new ListClient({ callCluster: callAsCurrentUser, config, - request, - security, spaceId, user, }), diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index a3b6a520a4ecf..36cf9bac373eb 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -14,7 +14,7 @@ import { validate, } from '../siem_server_deps'; import { - ImportListItemSchema, + ImportListItemHapiFileSchema, importListItemQuerySchema, importListItemSchema, listSchema, @@ -33,7 +33,7 @@ export const importListItemRoute = (router: IRouter): void => { }, path: `${LIST_ITEM_URL}/_import`, validate: { - body: buildRouteValidation( + body: buildRouteValidation( importListItemSchema ), query: buildRouteValidation(importListItemQuerySchema), diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts index 48deb3ee86820..50e690a3185a8 100644 --- a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TestReadable } from '../mocks'; +import { TestReadable } from '../../../common/test_readable.mock'; import { BufferLines } from './buffer_lines'; diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/items/create_list_item.mock.ts similarity index 85% rename from x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts rename to x-pack/plugins/lists/server/services/items/create_list_item.mock.ts index 17e3ad2f8de08..919aab5831440 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.mock.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { CreateListItemOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ID, @@ -16,7 +15,7 @@ import { TIE_BREAKER, TYPE, USER, -} from './lists_services_mock_constants'; +} from '../../../common/constants.mock'; export const getCreateListItemOptionsMock = (): CreateListItemOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts index abbb270149955..721d459bd7cc6 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LIST_ITEM_ID, - LIST_ITEM_INDEX, - getCreateListItemOptionsMock, - getIndexESListItemMock, - getListItemResponseMock, -} from '../mocks'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { getIndexESListItemMock } from '../../../common/schemas/elastic_query/index_es_list_item_schema.mock'; +import { LIST_ITEM_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; import { createListItem } from './create_list_item'; +import { getCreateListItemOptionsMock } from './create_list_item.mock'; describe('crete_list_item', () => { beforeEach(() => { diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.mock.ts similarity index 85% rename from x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts rename to x-pack/plugins/lists/server/services/items/create_list_items_bulk.mock.ts index fcdad66d65251..dd15d6f74a2ab 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.mock.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { CreateListItemsBulkOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ID, @@ -17,7 +16,7 @@ import { USER, VALUE, VALUE_2, -} from './lists_services_mock_constants'; +} from '../../../common/constants.mock'; export const getCreateListItemBulkOptionsMock = (): CreateListItemsBulkOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts index 94cc57b53b4e2..dbbb257f22d11 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -4,16 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexEsListItemSchema } from '../../../common/schemas'; -import { - LIST_ITEM_INDEX, - TIE_BREAKERS, - VALUE_2, - getCreateListItemBulkOptionsMock, - getIndexESListItemMock, -} from '../mocks'; +import { getIndexESListItemMock } from '../../../common/schemas/elastic_query/index_es_list_item_schema.mock'; +import { LIST_ITEM_INDEX, TIE_BREAKERS, VALUE_2 } from '../../../common/constants.mock'; import { createListItemsBulk } from './create_list_items_bulk'; +import { getCreateListItemBulkOptionsMock } from './create_list_items_bulk.mock'; describe('crete_list_item_bulk', () => { beforeEach(() => { @@ -27,8 +22,8 @@ describe('crete_list_item_bulk', () => { test('It calls "callCluster" with body, index, and the bulk items', async () => { const options = getCreateListItemBulkOptionsMock(); await createListItemsBulk(options); - const firstRecord: IndexEsListItemSchema = getIndexESListItemMock(); - const secondRecord: IndexEsListItemSchema = getIndexESListItemMock(VALUE_2); + const firstRecord = getIndexESListItemMock(); + const secondRecord = getIndexESListItemMock(VALUE_2); [firstRecord.tie_breaker_id, secondRecord.tie_breaker_id] = TIE_BREAKERS; expect(options.callCluster).toBeCalledWith('bulk', { body: [ diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.mock.ts similarity index 74% rename from x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts rename to x-pack/plugins/lists/server/services/items/delete_list_item.mock.ts index 271c185860b07..b62de4be9d24a 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.mock.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { DeleteListItemOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; -import { LIST_ITEM_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; +import { LIST_ITEM_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; export const getDeleteListItemOptionsMock = (): DeleteListItemOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts index 00fcefb2c379f..ea338d9dd3791 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LIST_ITEM_ID, - LIST_ITEM_INDEX, - getDeleteListItemOptionsMock, - getListItemResponseMock, -} from '../mocks'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { LIST_ITEM_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; import { getListItem } from './get_list_item'; import { deleteListItem } from './delete_list_item'; +import { getDeleteListItemOptionsMock } from './delete_list_item.mock'; jest.mock('./get_list_item', () => ({ getListItem: jest.fn(), diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.mock.ts similarity index 75% rename from x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts rename to x-pack/plugins/lists/server/services/items/delete_list_item_by_value.mock.ts index f6859e72d71b3..4aec27031f71b 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.mock.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { DeleteListItemByValueOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; -import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from '../../../common/constants.mock'; export const getDeleteListItemByValueOptionsMock = (): DeleteListItemByValueOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts index c7c80638e4c37..bf1608334ef24 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getDeleteListItemByValueOptionsMock, getListItemResponseMock } from '../mocks'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { getListItemByValues } from './get_list_item_by_values'; import { deleteListItemByValue } from './delete_list_item_by_value'; +import { getDeleteListItemByValueOptionsMock } from './delete_list_item_by_value.mock'; jest.mock('./get_list_item_by_values', () => ({ getListItemByValues: jest.fn(), diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts index 31a421c2e31bf..c39d6cdc00ee1 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LIST_ID, - LIST_INDEX, - getCallClusterMock, - getListItemResponseMock, - getSearchListItemMock, -} from '../mocks'; +import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { LIST_ID, LIST_INDEX } from '../../../common/constants.mock'; import { getListItem } from './get_list_item'; diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.mock.ts similarity index 75% rename from x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts rename to x-pack/plugins/lists/server/services/items/get_list_item_by_value.mock.ts index 96bc22ca7e6f2..bfa6b1c938073 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.mock.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { GetListItemByValueOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; -import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from '../../../common/constants.mock'; export const getListItemByValueOptionsMocks = (): GetListItemByValueOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts index d30b3c795550f..342984b4bc2ef 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getListItemByValueOptionsMocks, getListItemResponseMock } from '../mocks'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { getListItemByValues } from './get_list_item_by_values'; import { getListItemByValue } from './get_list_item_by_value'; +import { getListItemByValueOptionsMocks } from './get_list_item_by_value.mock'; jest.mock('./get_list_item_by_values', () => ({ getListItemByValues: jest.fn(), diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.mock.ts similarity index 84% rename from x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts rename to x-pack/plugins/lists/server/services/items/get_list_item_by_values.mock.ts index f21f97dc8d15f..fd5fa74383270 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.mock.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { GetListItemByValuesOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; -import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from './lists_services_mock_constants'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from '../../../common/constants.mock'; export const getListItemByValuesOptionsMocks = (): GetListItemByValuesOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts index 7f5fff4dc3147..5cf8b9e9d6c09 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts @@ -4,15 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { + DATE_NOW, LIST_ID, + LIST_ITEM_ID, LIST_ITEM_INDEX, + META, + TIE_BREAKER, TYPE, + USER, VALUE, VALUE_2, - getCallClusterMock, - getSearchListItemMock, -} from '../mocks'; +} from '../../../common/constants.mock'; import { getListItemByValues } from './get_list_item_by_values'; @@ -53,16 +58,16 @@ describe('get_list_item_by_values', () => { expect(listItem).toEqual([ { - created_at: '2020-04-20T15:25:31.830Z', - created_by: 'some user', - id: 'some-list-item-id', - list_id: 'some-list-id', - meta: {}, - tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', - type: 'ip', - updated_at: '2020-04-20T15:25:31.830Z', - updated_by: 'some user', - value: '127.0.0.1', + created_at: DATE_NOW, + created_by: USER, + id: LIST_ITEM_ID, + list_id: LIST_ID, + meta: META, + tie_breaker_id: TIE_BREAKER, + type: TYPE, + updated_at: DATE_NOW, + updated_by: USER, + value: VALUE, }, ]); }); diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts similarity index 83% rename from x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts rename to x-pack/plugins/lists/server/services/items/update_list_item.mock.ts index 0555997941baa..7ee8664b04d6b 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts @@ -3,9 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { UpdateListItemOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ITEM_ID, @@ -13,7 +12,7 @@ import { META, USER, VALUE, -} from './lists_services_mock_constants'; +} from '../../../common/constants.mock'; export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts index 4ef4110bc0742..95b99dc87bab6 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getListItemResponseMock, getUpdateListItemOptionsMock } from '../mocks'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { updateListItem } from './update_list_item'; import { getListItem } from './get_list_item'; +import { getUpdateListItemOptionsMock } from './update_list_item.mock'; jest.mock('./get_list_item', () => ({ getListItem: jest.fn(), diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts new file mode 100644 index 0000000000000..3d9902e1d43dd --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TestReadable } from '../../../common/test_readable.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { ImportListItemsToStreamOptions, WriteBufferToItemsOptions } from '../items'; +import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from '../../../common/constants.mock'; + +export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + stream: new TestReadable(), + type: TYPE, + user: USER, +}); + +export const getWriteBufferToItemsOptionsMock = (): WriteBufferToItemsOptions => ({ + buffer: [], + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + type: TYPE, + user: USER, +}); diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts index f064543f1ec93..71db6fa2cf62c 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getImportListItemsToStreamOptionsMock, - getListItemResponseMock, - getWriteBufferToItemsOptionsMock, -} from '../mocks'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { LinesResult, importListItemsToStream, writeBufferToItems, } from './write_lines_to_bulk_list_items'; +import { + getImportListItemsToStreamOptionsMock, + getWriteBufferToItemsOptionsMock, +} from './write_lines_to_bulk_list_items.mock'; import { getListItemByValues } from '.'; diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts index b08e5fa688b4b..2f04353e0989b 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { LIST_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; + import { - LIST_ID, - LIST_ITEM_INDEX, - getCallClusterMock, getExportListItemsToStreamOptionsMock, getResponseOptionsMock, - getSearchListItemMock, getWriteNextResponseOptions, getWriteResponseHitsToStreamOptionsMock, -} from '../mocks'; +} from './write_list_items_to_streams.mock'; import { exportListItemsToStream, diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts similarity index 83% rename from x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts rename to x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts index c945818a83e8a..34cdadd1e554f 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts @@ -6,16 +6,15 @@ import { Stream } from 'stream'; +import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { ExportListItemsToStreamOptions, GetResponseOptions, WriteNextResponseOptions, WriteResponseHitsToStreamOptions, } from '../items'; - -import { LIST_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; -import { getSearchListItemMock } from './get_search_list_item_mock'; -import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStreamOptions => ({ callCluster: getCallClusterMock(getSearchListItemMock()), diff --git a/x-pack/plugins/lists/server/services/lists/client_types.ts b/x-pack/plugins/lists/server/services/lists/client_types.ts index 2cc58c02dbfcf..d66575e7a30db 100644 --- a/x-pack/plugins/lists/server/services/lists/client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/client_types.ts @@ -6,9 +6,8 @@ import { PassThrough, Readable } from 'stream'; -import { APICaller, KibanaRequest } from 'kibana/server'; +import { APICaller } from 'kibana/server'; -import { SecurityPluginSetup } from '../../../../security/server'; import { Description, DescriptionOrUndefined, @@ -24,10 +23,8 @@ import { ConfigType } from '../../config'; export interface ConstructorOptions { callCluster: APICaller; config: ConfigType; - request: KibanaRequest; spaceId: string; user: string; - security: SecurityPluginSetup | undefined | null; } export interface GetListOptions { diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts b/x-pack/plugins/lists/server/services/lists/create_list.mock.ts similarity index 85% rename from x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts rename to x-pack/plugins/lists/server/services/lists/create_list.mock.ts index 0ea6533fc122a..f0fd023d018ae 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.mock.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { CreateListOptions } from '../lists'; - -import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, DESCRIPTION, @@ -17,7 +16,7 @@ import { TIE_BREAKER, TYPE, USER, -} from './lists_services_mock_constants'; +} from '../../../common/constants.mock'; export const getCreateListOptionsMock = (): CreateListOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts index 36284a70fb97d..ef610ece1acc9 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LIST_ID, - LIST_INDEX, - getCreateListOptionsMock, - getIndexESListMock, - getListResponseMock, -} from '../mocks'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; +import { getIndexESListMock } from '../../../common/schemas/elastic_query/index_es_list_schema.mock'; +import { LIST_ID, LIST_INDEX } from '../../../common/constants.mock'; import { createList } from './create_list'; +import { getCreateListOptionsMock } from './create_list.mock'; describe('crete_list', () => { beforeEach(() => { diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts b/x-pack/plugins/lists/server/services/lists/delete_list.mock.ts similarity index 74% rename from x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts rename to x-pack/plugins/lists/server/services/lists/delete_list.mock.ts index 8ec92dfa4ef77..fd2ab654b55f6 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.mock.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { DeleteListOptions } from '../lists'; - -import { getCallClusterMock } from './get_call_cluster_mock'; -import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from './lists_services_mock_constants'; +import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; export const getDeleteListOptionsMock = (): DeleteListOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts index 62b5e7c7aec4a..b9f1ec4d400be 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -4,16 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LIST_ID, - LIST_INDEX, - LIST_ITEM_INDEX, - getDeleteListOptionsMock, - getListResponseMock, -} from '../mocks'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; +import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; import { getList } from './get_list'; import { deleteList } from './delete_list'; +import { getDeleteListOptionsMock } from './delete_list.mock'; jest.mock('./get_list', () => ({ getList: jest.fn(), diff --git a/x-pack/plugins/lists/server/services/lists/get_list.test.ts b/x-pack/plugins/lists/server/services/lists/get_list.test.ts index c997d5325296a..9402856573288 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.test.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LIST_ID, - LIST_INDEX, - getCallClusterMock, - getListResponseMock, - getSearchListMock, -} from '../mocks'; +import { getSearchListMock } from '../../../common/schemas/elastic_response/search_es_list_schema.mock'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { LIST_ID, LIST_INDEX } from '../../../common/constants.mock'; import { getList } from './get_list'; diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts b/x-pack/plugins/lists/server/services/lists/update_list.mock.ts similarity index 83% rename from x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts rename to x-pack/plugins/lists/server/services/lists/update_list.mock.ts index fe6fc37eaf81e..ff974b6e7352b 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.mock.ts @@ -3,9 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { UpdateListOptions } from '../lists'; - -import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, DESCRIPTION, @@ -14,7 +13,7 @@ import { META, NAME, USER, -} from './lists_services_mock_constants'; +} from '../../../common/constants.mock'; export const getUpdateListOptionsMock = (): UpdateListOptions => ({ callCluster: getCallClusterMock(), diff --git a/x-pack/plugins/lists/server/services/lists/update_list.test.ts b/x-pack/plugins/lists/server/services/lists/update_list.test.ts index 09bf0ee69c981..1c4fde40a777a 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getListResponseMock, getUpdateListOptionsMock } from '../mocks'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; import { updateList } from './update_list'; import { getList } from './get_list'; +import { getUpdateListOptionsMock } from './update_list.mock'; jest.mock('./get_list', () => ({ getList: jest.fn(), diff --git a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts deleted file mode 100644 index d7541f3e09e6c..0000000000000 --- a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { ImportListItemsToStreamOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; -import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; -import { TestReadable } from './test_readable'; - -export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ - callCluster: getCallClusterMock(), - listId: LIST_ID, - listItemIndex: LIST_ITEM_INDEX, - meta: META, - stream: new TestReadable(), - type: TYPE, - user: USER, -}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts deleted file mode 100644 index 5e9fd8995c0eb..0000000000000 --- a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SearchEsListItemSchema } from '../../../common/schemas'; - -import { DATE_NOW, LIST_ID, USER, VALUE } from './lists_services_mock_constants'; - -export const getSearchEsListItemMock = (): SearchEsListItemSchema => ({ - created_at: DATE_NOW, - created_by: USER, - ip: VALUE, - keyword: undefined, - list_id: LIST_ID, - meta: {}, - tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', - updated_at: DATE_NOW, - updated_by: USER, -}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts deleted file mode 100644 index 6a565437617ba..0000000000000 --- a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SearchEsListSchema } from '../../../common/schemas'; - -import { DATE_NOW, DESCRIPTION, NAME, USER } from './lists_services_mock_constants'; - -export const getSearchEsListMock = (): SearchEsListSchema => ({ - created_at: DATE_NOW, - created_by: USER, - description: DESCRIPTION, - meta: {}, - name: NAME, - tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', - type: 'ip', - updated_at: DATE_NOW, - updated_by: USER, -}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts deleted file mode 100644 index d6b7d70c1aa77..0000000000000 --- a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts +++ /dev/null @@ -1,19 +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 { WriteBufferToItemsOptions } from '../items'; - -import { getCallClusterMock } from './get_call_cluster_mock'; -import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; - -export const getWriteBufferToItemsOptionsMock = (): WriteBufferToItemsOptions => ({ - buffer: [], - callCluster: getCallClusterMock(), - listId: LIST_ID, - listItemIndex: LIST_ITEM_INDEX, - meta: META, - type: TYPE, - user: USER, -}); diff --git a/x-pack/plugins/lists/server/services/mocks/index.ts b/x-pack/plugins/lists/server/services/mocks/index.ts deleted file mode 100644 index c555ba322fa2b..0000000000000 --- a/x-pack/plugins/lists/server/services/mocks/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './get_call_cluster_mock'; -export * from './get_delete_list_options_mock'; -export * from './get_create_list_options_mock'; -export * from './get_list_response_mock'; -export * from './get_search_list_mock'; -export * from './get_shard_mock'; -export * from './lists_services_mock_constants'; -export * from './get_update_list_options_mock'; -export * from './get_create_list_item_options_mock'; -export * from './get_list_item_response_mock'; -export * from './get_index_es_list_mock'; -export * from './get_index_es_list_item_mock'; -export * from './get_create_list_item_bulk_options_mock'; -export * from './get_delete_list_item_by_value_options_mock'; -export * from './get_delete_list_item_options_mock'; -export * from './get_list_item_by_values_options_mock'; -export * from './get_search_es_list_mock'; -export * from './get_search_es_list_item_mock'; -export * from './get_list_item_by_value_options_mock'; -export * from './get_update_list_item_options_mock'; -export * from './get_write_buffer_to_items_options_mock'; -export * from './get_import_list_items_to_stream_options_mock'; -export * from './get_write_list_items_to_stream_options_mock'; -export * from './get_search_list_item_mock'; -export * from './test_readable'; diff --git a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts index 3b6f58479a2f2..8240e2965755e 100644 --- a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSearchEsListItemMock } from '../mocks'; +import { getSearchEsListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; import { Type } from '../../../common/schemas'; import { deriveTypeFromItem } from './derive_type_from_es_type'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts index 3b9864be6df53..8b32f09400719 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { ListItemArraySchema } from '../../../common/schemas'; -import { getListItemResponseMock, getSearchListItemMock } from '../mocks'; import { transformElasticToListItem } from './transform_elastic_to_list_item'; diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts index e0e4495d47c34..d7c3208e556fa 100644 --- a/x-pack/plugins/lists/server/types.ts +++ b/x-pack/plugins/lists/server/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IContextProvider, RequestHandler } from 'kibana/server'; +import { APICaller, IContextProvider, RequestHandler } from 'kibana/server'; import { SecurityPluginSetup } from '../../security/server'; import { SpacesPluginSetup } from '../../spaces/server'; @@ -12,12 +12,21 @@ import { SpacesPluginSetup } from '../../spaces/server'; import { ListClient } from './services/lists/client'; export type ContextProvider = IContextProvider, 'lists'>; - +export type ListsPluginStart = void; export interface PluginsSetup { security: SecurityPluginSetup | undefined | null; spaces: SpacesPluginSetup | undefined | null; } +export type GetListClientType = ( + dataClient: APICaller, + spaceId: string, + user: string +) => ListClient; +export interface ListPluginSetup { + getListClient: GetListClientType; +} + export type ContextProviderReturn = Promise<{ getListClient: () => ListClient }>; declare module 'src/core/server' { interface RequestHandlerContext { diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json index 97dbf58865a88..1eb325dcc1610 100644 --- a/x-pack/plugins/logstash/kibana.json +++ b/x-pack/plugins/logstash/kibana.json @@ -9,6 +9,7 @@ ], "optionalPlugins": [ "home", + "monitoring", "security" ], "server": true, diff --git a/x-pack/plugins/logstash/public/application/index.tsx b/x-pack/plugins/logstash/public/application/index.tsx index 438038d6c885e..3588e1f6b2417 100644 --- a/x-pack/plugins/logstash/public/application/index.tsx +++ b/x-pack/plugins/logstash/public/application/index.tsx @@ -31,16 +31,12 @@ import * as Breadcrumbs from './breadcrumbs'; export const renderApp = async ( core: CoreStart, { basePath, element, setBreadcrumbs }: ManagementAppMountParams, + isMonitoringEnabled: boolean, licenseService$: Observable ) => { const logstashLicenseService = await licenseService$.pipe(first()).toPromise(); const clusterService = new ClusterService(core.http); - const monitoringService = new MonitoringService( - core.http, - // When monitoring is migrated this should be fetched from monitoring's plugin contract - core.injectedMetadata.getInjectedVar('monitoringUiEnabled'), - clusterService - ); + const monitoringService = new MonitoringService(core.http, isMonitoringEnabled, clusterService); const pipelinesService = new PipelinesService(core.http, monitoringService); const pipelineService = new PipelineService(core.http, pipelinesService); const upgradeService = new UpgradeService(core.http); diff --git a/x-pack/plugins/logstash/public/plugin.ts b/x-pack/plugins/logstash/public/plugin.ts index 91d1a39d3970c..7fbed5b3b8602 100644 --- a/x-pack/plugins/logstash/public/plugin.ts +++ b/x-pack/plugins/logstash/public/plugin.ts @@ -49,8 +49,9 @@ export class LogstashPlugin implements Plugin { mount: async params => { const [coreStart] = await core.getStartServices(); const { renderApp } = await import('./application'); + const isMonitoringEnabled = 'monitoring' in plugins; - return renderApp(coreStart, params, logstashLicense$); + return renderApp(coreStart, params, isMonitoringEnabled, logstashLicense$); }, }); diff --git a/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js b/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js index d551f4fba61d2..4db2838cb5354 100755 --- a/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js +++ b/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js @@ -9,14 +9,14 @@ import { ROUTES, MONITORING } from '../../../common/constants'; import { PipelineListItem } from '../../models/pipeline_list_item'; export class MonitoringService { - constructor(http, monitoringUiEnabled, clusterService) { + constructor(http, isMonitoringEnabled, clusterService) { this.http = http; - this.monitoringUiEnabled = monitoringUiEnabled; + this._isMonitoringEnabled = isMonitoringEnabled; this.clusterService = clusterService; } isMonitoringEnabled() { - return this.monitoringUiEnabled; + return this._isMonitoringEnabled; } getPipelineList() { @@ -27,6 +27,8 @@ export class MonitoringService { return this.clusterService .loadCluster() .then(cluster => { + // This API call should live within the Monitoring plugin + // https://github.com/elastic/kibana/issues/63931 const url = `${ROUTES.MONITORING_API_ROOT}/v1/clusters/${cluster.uuid}/logstash/pipeline_ids`; const now = moment.utc(); const body = JSON.stringify({ diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts index a9a9fa17c41fc..722fdd03ebc43 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts @@ -28,7 +28,7 @@ export type EMSTMSSourceDescriptor = AbstractSourceDescriptor & { export type EMSFileSourceDescriptor = AbstractSourceDescriptor & { // id: EMS file id - + id: string; tooltipProperties: string[]; }; diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index b8bad47327f22..077601204e3ee 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -12,7 +12,8 @@ "uiActions", "navigation", "visualizations", - "embeddable" + "embeddable", + "mapsLegacy" ], "ui": true } diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.js b/x-pack/plugins/maps/public/angular/get_initial_layers.js index f02ded1704533..09f66740af372 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.js @@ -16,7 +16,7 @@ import { KibanaTilemapSource } from '../layers/sources/kibana_tilemap_source'; import { TileLayer } from '../layers/tile_layer'; import { EMSTMSSource } from '../layers/sources/ems_tms_source'; import { VectorTileLayer } from '../layers/vector_tile_layer'; -import { getInjectedVarFunc } from '../kibana_services'; +import { getIsEmsEnabled } from '../kibana_services'; import { getKibanaTileMap } from '../meta'; export function getInitialLayers(layerListJSON, initialLayers = []) { @@ -32,7 +32,7 @@ export function getInitialLayers(layerListJSON, initialLayers = []) { return [layerDescriptor, ...initialLayers]; } - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); + const isEmsEnabled = getIsEmsEnabled(); if (isEmsEnabled) { const layerDescriptor = VectorTileLayer.createDescriptor({ sourceDescriptor: EMSTMSSource.createDescriptor({ isAutoSelect: true }), diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.test.js b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js index 4b5cad8d19260..867025cd70213 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.test.js +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js @@ -65,6 +65,7 @@ describe('EMS is enabled', () => { require('../meta').getKibanaTileMap = () => { return null; }; + require('../kibana_services').getIsEmsEnabled = () => true; require('../kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'emsTileLayerId': @@ -73,8 +74,6 @@ describe('EMS is enabled', () => { desaturated: 'road_map_desaturated', dark: 'dark_map', }; - case 'isEmsEnabled': - return true; default: throw new Error(`Unexpected call to getInjectedVarFunc with key ${key}`); } @@ -109,15 +108,7 @@ describe('EMS is not enabled', () => { require('../meta').getKibanaTileMap = () => { return null; }; - - require('../kibana_services').getInjectedVarFunc = () => key => { - switch (key) { - case 'isEmsEnabled': - return false; - default: - throw new Error(`Unexpected call to getInjectedVarFunc with key ${key}`); - } - }; + require('../kibana_services').getIsEmsEnabled = () => false; }); it('Should return empty layer list since there are no configured tile layers', () => { diff --git a/x-pack/plugins/maps/public/components/_index.scss b/x-pack/plugins/maps/public/components/_index.scss index 161b3fefdb8f9..76e27338bdcd4 100644 --- a/x-pack/plugins/maps/public/components/_index.scss +++ b/x-pack/plugins/maps/public/components/_index.scss @@ -1,3 +1,3 @@ @import 'metric_editors'; @import './geometry_filter'; -@import 'tooltip_selector'; +@import 'tooltip_selector/tooltip_selector'; diff --git a/x-pack/plugins/maps/public/components/tooltip_selector.js b/x-pack/plugins/maps/public/components/tooltip_selector.js deleted file mode 100644 index 953b711cef6c7..0000000000000 --- a/x-pack/plugins/maps/public/components/tooltip_selector.js +++ /dev/null @@ -1,229 +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, { Component } from 'react'; -import classNames from 'classnames'; -import { - EuiButtonIcon, - EuiDragDropContext, - EuiDraggable, - EuiDroppable, - EuiText, - EuiTextAlign, - EuiSpacer, -} from '@elastic/eui'; -import { AddTooltipFieldPopover } from './add_tooltip_field_popover'; -import { i18n } from '@kbn/i18n'; - -// TODO import reorder from EUI once its exposed as service -// https://github.com/elastic/eui/issues/2372 -const reorder = (list, startIndex, endIndex) => { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - - return result; -}; - -const getProps = async field => { - return new Promise(async (resolve, reject) => { - try { - const label = await field.getLabel(); - const type = await field.getDataType(); - resolve({ - label: label, - type: type, - name: field.getName(), - }); - } catch (e) { - reject(e); - } - }); -}; - -export class TooltipSelector extends Component { - state = { - fieldProps: [], - selectedFieldProps: [], - }; - - constructor() { - super(); - this._isMounted = false; - this._previousFields = null; - this._previousSelectedTooltips = null; - } - - componentDidMount() { - this._isMounted = true; - this._loadFieldProps(); - this._loadTooltipFieldProps(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - componentDidUpdate() { - this._loadTooltipFieldProps(); - this._loadFieldProps(); - } - - async _loadTooltipFieldProps() { - if (!this.props.tooltipFields || this.props.tooltipFields === this._previousSelectedTooltips) { - return; - } - - this._previousSelectedTooltips = this.props.tooltipFields; - const selectedProps = this.props.tooltipFields.map(getProps); - const selectedFieldProps = await Promise.all(selectedProps); - if (this._isMounted) { - this.setState({ selectedFieldProps }); - } - } - - async _loadFieldProps() { - if (!this.props.fields || this.props.fields === this._previousFields) { - return; - } - - this._previousFields = this.props.fields; - const props = this.props.fields.map(getProps); - const fieldProps = await Promise.all(props); - if (this._isMounted) { - this.setState({ fieldProps }); - } - } - - _getPropertyLabel = propertyName => { - if (!this.state.fieldProps.length) { - return propertyName; - } - const prop = this.state.fieldProps.find(field => { - return field.name === propertyName; - }); - return prop.label ? prop.label : propertyName; - }; - - _getTooltipProperties() { - return this.props.tooltipFields.map(field => field.getName()); - } - - _onAdd = properties => { - if (!this.props.tooltipFields) { - this.props.onChange([...properties]); - } else { - const existingProperties = this._getTooltipProperties(); - this.props.onChange([...existingProperties, ...properties]); - } - }; - - _removeProperty = index => { - if (!this.props.tooltipFields) { - this.props.onChange([]); - } else { - const tooltipProperties = this._getTooltipProperties(); - tooltipProperties.splice(index, 1); - this.props.onChange(tooltipProperties); - } - }; - - _onDragEnd = ({ source, destination }) => { - // Dragging item out of EuiDroppable results in destination of null - if (!destination) { - return; - } - - this.props.onChange(reorder(this._getTooltipProperties(), source.index, destination.index)); - }; - - _renderProperties() { - if (!this.state.selectedFieldProps.length) { - return null; - } - - return ( - - - {(provided, snapshot) => - this.state.selectedFieldProps.map((field, idx) => ( - - {(provided, state) => ( -
      - - {this._getPropertyLabel(field.name)} - -
      - - -
      -
      - )} -
      - )) - } -
      -
      - ); - } - - render() { - return ( -
      - {this._renderProperties()} - - - - - - -
      - ); - } -} diff --git a/x-pack/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap b/x-pack/plugins/maps/public/components/tooltip_selector/__snapshots__/add_tooltip_field_popover.test.tsx.snap similarity index 96% rename from x-pack/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap rename to x-pack/plugins/maps/public/components/tooltip_selector/__snapshots__/add_tooltip_field_popover.test.tsx.snap index d0cdbe7243abe..be362c2ae0422 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap +++ b/x-pack/plugins/maps/public/components/tooltip_selector/__snapshots__/add_tooltip_field_popover.test.tsx.snap @@ -30,7 +30,7 @@ exports[`Should remove selected fields from selectable 1`] = ` options={ Array [ Object { - "label": "@timestamp", + "label": "@timestamp-label", "prepend": {}, + onAdd: () => {}, }; test('Should render', () => { @@ -39,7 +41,10 @@ test('Should remove selected fields from selectable', () => { const component = shallow( ); diff --git a/x-pack/plugins/maps/public/components/add_tooltip_field_popover.js b/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx similarity index 79% rename from x-pack/plugins/maps/public/components/add_tooltip_field_popover.js rename to x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx index 984ace4fd8708..782e5e878164e 100644 --- a/x-pack/plugins/maps/public/components/add_tooltip_field_popover.js +++ b/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx @@ -3,6 +3,7 @@ * 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/consistent-type-definitions */ import React, { Component, Fragment } from 'react'; import { @@ -11,19 +12,26 @@ import { EuiPopoverTitle, EuiButtonEmpty, EuiSelectable, + EuiSelectableOption, EuiButton, EuiSpacer, EuiTextAlign, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { FieldIcon } from '../../../../../src/plugins/kibana_react/public'; +import { FieldIcon } from '../../../../../../src/plugins/kibana_react/public'; -const sortByLabel = (a, b) => { - return a.label.localeCompare(b.label); +export type FieldProps = { + label: string; + type: string; + name: string; }; -function getOptions(fields, selectedFields) { +function sortByLabel(a: EuiSelectableOption, b: EuiSelectableOption): number { + return a.label.localeCompare(b.label); +} + +function getOptions(fields: FieldProps[], selectedFields: FieldProps[]): EuiSelectableOption[] { if (!fields) { return []; } @@ -43,19 +51,33 @@ function getOptions(fields, selectedFields) { 'type' in field ? ( ) : null, - label: 'label' in field ? field.label : field.name, + label: field.label, }; }) .sort(sortByLabel); } -export class AddTooltipFieldPopover extends Component { - state = { +interface Props { + onAdd: (checkedFieldNames: string[]) => void; + fields: FieldProps[]; + selectedFields: FieldProps[]; +} + +interface State { + isPopoverOpen: boolean; + checkedFields: string[]; + options?: EuiSelectableOption[]; + prevFields?: FieldProps[]; + prevSelectedFields?: FieldProps[]; +} + +export class AddTooltipFieldPopover extends Component { + state: State = { isPopoverOpen: false, checkedFields: [], }; - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: Props, prevState: State) { if ( nextProps.fields !== prevState.prevFields || nextProps.selectedFields !== prevState.prevSelectedFields @@ -83,13 +105,13 @@ export class AddTooltipFieldPopover extends Component { }); }; - _onSelect = options => { - const checkedFields = options + _onSelect = (options: EuiSelectableOption[]) => { + const checkedFields: string[] = options .filter(option => { return option.checked === 'on'; }) .map(option => { - return option.value; + return option.value as string; }); this.setState({ diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/index.ts b/x-pack/plugins/maps/public/components/tooltip_selector/index.ts new file mode 100644 index 0000000000000..7c5dc3d8a4c00 --- /dev/null +++ b/x-pack/plugins/maps/public/components/tooltip_selector/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 { TooltipSelector } from './tooltip_selector'; diff --git a/x-pack/plugins/maps/public/components/tooltip_selector.test.js b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.test.tsx similarity index 77% rename from x-pack/plugins/maps/public/components/tooltip_selector.test.js rename to x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.test.tsx index 1a83f4a98bb6f..10d3f6af63370 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector.test.js +++ b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.test.tsx @@ -8,25 +8,19 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TooltipSelector } from './tooltip_selector'; +import { AbstractField } from '../../layers/fields/field'; +import { FIELD_ORIGIN } from '../../../common/constants'; -class MockField { - constructor({ name, label, type }) { - this._name = name; +class MockField extends AbstractField { + private _label?: string; + constructor({ name, label }: { name: string; label?: string }) { + super({ fieldName: name, origin: FIELD_ORIGIN.SOURCE }); this._label = label; - this._type = type; - } - - getName() { - return this._name; } async getLabel() { return this._label || 'foobar_label'; } - - async getDataType() { - return this._type || 'foobar_type'; - } } const defaultProps = { @@ -36,11 +30,9 @@ const defaultProps = { new MockField({ name: 'iso2', label: 'ISO 3166-1 alpha-2 code', - type: 'string', }), new MockField({ name: 'iso3', - type: 'string', }), ], }; diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx new file mode 100644 index 0000000000000..211276cda904a --- /dev/null +++ b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx @@ -0,0 +1,245 @@ +/* + * 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, { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiText, + EuiTextAlign, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AddTooltipFieldPopover, FieldProps } from './add_tooltip_field_popover'; +import { IField } from '../../layers/fields/field'; + +// TODO import reorder from EUI once its exposed as service +// https://github.com/elastic/eui/issues/2372 +const reorder = (list: string[], startIndex: number, endIndex: number) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; +}; + +async function getFieldProps(field: IField): Promise { + return { + label: await field.getLabel(), + type: await field.getDataType(), + name: field.getName(), + }; +} + +interface Props { + fields: IField[] | null; + onChange: (selectedFieldNames: string[]) => void; + tooltipFields: IField[]; +} + +interface State { + fieldProps: FieldProps[]; + selectedFieldProps: FieldProps[]; +} + +export class TooltipSelector extends Component { + private _isMounted: boolean; + private _previousFields: IField[] | null; + private _previousSelectedTooltips: IField[] | null; + + state = { + fieldProps: [], + selectedFieldProps: [], + }; + + constructor(props: Props) { + super(props); + this._isMounted = false; + this._previousFields = null; + this._previousSelectedTooltips = null; + } + + componentDidMount() { + this._isMounted = true; + this._loadFieldProps(); + this._loadTooltipFieldProps(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + this._loadTooltipFieldProps(); + this._loadFieldProps(); + } + + async _loadTooltipFieldProps() { + if (!this.props.tooltipFields || this.props.tooltipFields === this._previousSelectedTooltips) { + return; + } + + this._previousSelectedTooltips = this.props.tooltipFields; + const promises = this.props.tooltipFields.map(getFieldProps); + const selectedFieldProps = await Promise.all(promises); + if (this._isMounted) { + this.setState({ selectedFieldProps }); + } + } + + async _loadFieldProps() { + if (!this.props.fields || this.props.fields === this._previousFields) { + return; + } + + this._previousFields = this.props.fields; + const promises = this.props.fields.map(getFieldProps); + const fieldProps = await Promise.all(promises); + if (this._isMounted) { + this.setState({ fieldProps }); + } + } + + _getPropertyLabel = (propertyName: string) => { + if (!this.state.fieldProps.length) { + return propertyName; + } + const prop: FieldProps | undefined = this.state.fieldProps.find((field: FieldProps) => { + return field.name === propertyName; + }); + return prop ? prop!.label : propertyName; + }; + + _getTooltipFieldNames(): string[] { + return this.props.tooltipFields ? this.props.tooltipFields.map(field => field.getName()) : []; + } + + _onAdd = (properties: string[]) => { + if (!this.props.tooltipFields) { + this.props.onChange([...properties]); + } else { + const existingProperties = this._getTooltipFieldNames(); + this.props.onChange([...existingProperties, ...properties]); + } + }; + + _removeProperty = (index: number) => { + if (!this.props.tooltipFields) { + this.props.onChange([]); + } else { + const tooltipProperties = this._getTooltipFieldNames(); + tooltipProperties.splice(index, 1); + this.props.onChange(tooltipProperties); + } + }; + + _onDragEnd = ({ + source, + destination, + }: { + source: { index: number }; + destination?: { index: number }; + }) => { + // Dragging item out of EuiDroppable results in destination of null + if (!destination) { + return; + } + + this.props.onChange(reorder(this._getTooltipFieldNames(), source.index, destination.index)); + }; + + _renderProperties() { + if (!this.state.selectedFieldProps.length) { + return null; + } + + return ( + + + {(droppableProvided, snapshot) => ( + + {this.state.selectedFieldProps.map((field: FieldProps, idx: number) => ( + + {(provided, state) => ( +
      + + {this._getPropertyLabel(field.name)} + +
      + + +
      +
      + )} +
      + ))} +
      + )} +
      +
      + ); + } + + render() { + return ( +
      + {this._renderProperties()} + + + + + + +
      + ); + } +} diff --git a/x-pack/plugins/maps/public/kibana_services.d.ts b/x-pack/plugins/maps/public/kibana_services.d.ts index 3d346fe1acdd5..454ba6ededcbd 100644 --- a/x-pack/plugins/maps/public/kibana_services.d.ts +++ b/x-pack/plugins/maps/public/kibana_services.d.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { IIndexPattern, DataPublicPluginStart } from 'src/plugins/data/public'; +import _ from 'lodash'; export function getLicenseId(): any; export function getInspector(): any; @@ -30,6 +31,15 @@ export function getCore(): any; export function getNavigation(): any; export function getCoreI18n(): any; export function getSearchService(): DataPublicPluginStart['search']; +export function getMapConfig(): any; +export function getIsEmsEnabled(): any; +export function getEmsFontLibraryUrl(): any; +export function getEmsTileLayerId(): any; +export function getEmsFileApiUrl(): any; +export function getEmsTileApiUrl(): any; +export function getEmsLandingPageUrl(): any; +export function getRegionmapLayers(): any; +export function getTilemap(): any; export function setLicenseId(args: unknown): void; export function setInspector(args: unknown): void; @@ -54,3 +64,4 @@ export function setCore(args: unknown): void; export function setNavigation(args: unknown): void; export function setCoreI18n(args: unknown): void; export function setSearchService(args: DataPublicPluginStart['search']): void; +export function setMapConfig(args: unknown): void; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 431d7a3b339b7..2f07c1c5d086d 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { esFilters, search } from '../../../../src/plugins/data/public'; +import _ from 'lodash'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; const { getRequestInspectorStats, getResponseInspectorStats } = search; @@ -139,3 +140,16 @@ export const getCoreI18n = () => coreI18n; let dataSearchService; export const setSearchService = searchService => (dataSearchService = searchService); export const getSearchService = () => dataSearchService; + +let mapConfig; +export const setMapConfig = config => (mapConfig = config); +export const getMapConfig = () => mapConfig; + +export const getIsEmsEnabled = () => getMapConfig().includeElasticMapsService; +export const getEmsFontLibraryUrl = () => getMapConfig().emsFontLibraryUrl; +export const getEmsTileLayerId = () => getMapConfig().emsTileLayerId; +export const getEmsFileApiUrl = () => getMapConfig().emsFileApiUrl; +export const getEmsTileApiUrl = () => getMapConfig().emsTileApiUrl; +export const getEmsLandingPageUrl = () => getMapConfig().emsLandingPageUrl; +export const getRegionmapLayers = () => _.get(getMapConfig(), 'regionmap.layers', []); +export const getTilemap = () => _.get(getMapConfig(), 'tilemap', []); diff --git a/x-pack/plugins/maps/public/layers/fields/ems_file_field.ts b/x-pack/plugins/maps/public/layers/fields/ems_file_field.ts index c14886bc37bfb..7ed508199e64a 100644 --- a/x-pack/plugins/maps/public/layers/fields/ems_file_field.ts +++ b/x-pack/plugins/maps/public/layers/fields/ems_file_field.ts @@ -7,7 +7,7 @@ import { FIELD_ORIGIN } from '../../../common/constants'; import { IField, AbstractField } from './field'; import { IVectorSource } from '../sources/vector_source'; -import { IEmsFileSource } from '../sources/ems_file_source/ems_file_source'; +import { IEmsFileSource } from '../sources/ems_file_source'; export class EMSFileField extends AbstractField implements IField { private readonly _source: IEmsFileSource; diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx index dccf413b489f1..8ecaf4d903251 100644 --- a/x-pack/plugins/maps/public/layers/layer.tsx +++ b/x-pack/plugins/maps/public/layers/layer.tsx @@ -28,7 +28,7 @@ import { MapFilters, StyleDescriptor, } from '../../common/descriptor_types'; -import { Attribution, ImmutableSourceProperty, ISource } from './sources/source'; +import { Attribution, ImmutableSourceProperty, ISource, SourceEditorArgs } from './sources/source'; import { SyncContext } from '../actions/map_actions'; import { IStyle } from './styles/style'; @@ -58,7 +58,7 @@ export interface ILayer { getStyleForEditing(): IStyle; getCurrentStyle(): IStyle; getImmutableSourceProperties(): Promise; - renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement | null; + renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null; isLayerLoading(): boolean; hasErrors(): boolean; getErrors(): string; @@ -368,7 +368,7 @@ export class AbstractLayer implements ILayer { return await source.getImmutableProperties(); } - renderSourceSettingsEditor({ onChange }: { onChange: () => void }) { + renderSourceSettingsEditor({ onChange }: SourceEditorArgs) { const source = this.getSourceForEditing(); return source.renderSourceSettingsEditor({ onChange }); } diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.tsx similarity index 57% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.js rename to x-pack/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.tsx index 47a4879acb58c..b66918f93f521 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.tsx @@ -4,31 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiComboBox, EuiFormRow } from '@elastic/eui'; +import React, { Component } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore import { getEMSClient } from '../../../meta'; import { getEmsUnavailableMessage } from '../ems_unavailable_message'; -import { i18n } from '@kbn/i18n'; +import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types'; + +interface Props { + onSourceConfigChange: (sourceConfig: Partial) => void; +} + +interface State { + hasLoadedOptions: boolean; + emsFileOptions: Array>; + selectedOption: EuiComboBoxOptionOption | null; +} + +export class EMSFileCreateSourceEditor extends Component { + private _isMounted: boolean = false; -export class EMSFileCreateSourceEditor extends React.Component { state = { - emsFileOptionsRaw: null, + hasLoadedOptions: false, + emsFileOptions: [], selectedOption: null, }; _loadFileOptions = async () => { + // @ts-ignore const emsClient = getEMSClient(); - const fileLayers = await emsClient.getFileLayers(); + // @ts-ignore + const fileLayers: unknown[] = await emsClient.getFileLayers(); const options = fileLayers.map(fileLayer => { return { - id: fileLayer.getId(), - name: fileLayer.getDisplayName(), + // @ts-ignore + value: fileLayer.getId(), + // @ts-ignore + label: fileLayer.getDisplayName(), }; }); if (this._isMounted) { this.setState({ - emsFileOptionsRaw: options, + hasLoadedOptions: true, + emsFileOptions: options, }); } }; @@ -42,7 +62,7 @@ export class EMSFileCreateSourceEditor extends React.Component { this._loadFileOptions(); } - _onChange = selectedOptions => { + _onChange = (selectedOptions: Array>) => { if (selectedOptions.length === 0) { return; } @@ -54,32 +74,28 @@ export class EMSFileCreateSourceEditor extends React.Component { }; render() { - if (!this.state.emsFileOptionsRaw) { + if (!this.state.hasLoadedOptions) { // TODO display loading message return null; } - const options = this.state.emsFileOptionsRaw.map(({ id, name }) => { - return { label: name, value: id }; - }); - return ( diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index a6e2e7f42657c..cc7e04a7313ac 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -8,24 +8,22 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { VectorLayer } from '../../vector_layer'; import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; -// @ts-ignore import { EMSFileCreateSourceEditor } from './create_source_editor'; -// @ts-ignore import { EMSFileSource, sourceTitle } from './ems_file_source'; // @ts-ignore -import { isEmsEnabled } from '../../../meta'; +import { getIsEmsEnabled } from '../../../kibana_services'; +import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types'; export const emsBoundariesLayerWizardConfig: LayerWizard = { checkVisibility: () => { - return isEmsEnabled(); + return getIsEmsEnabled(); }, description: i18n.translate('xpack.maps.source.emsFileDescription', { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), icon: 'emsApp', renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: unknown) => { - // @ts-ignore + const onSourceConfigChange = (sourceConfig: Partial) => { const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayer(layerDescriptor); diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.d.ts b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.d.ts deleted file mode 100644 index 37c843d4a9060..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AbstractVectorSource, IVectorSource } from '../vector_source'; - -export interface IEmsFileSource extends IVectorSource { - getEMSFileLayer(): Promise; -} - -export class EMSFileSource extends AbstractVectorSource implements IEmsFileSource { - getEMSFileLayer(): Promise; -} diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.tsx similarity index 90% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js rename to x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.tsx index 93c9af98eb17f..03e3b2a8f4941 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.tsx @@ -9,11 +9,9 @@ import { EMSFileSource } from './ems_file_source'; jest.mock('ui/new_platform'); jest.mock('../../vector_layer', () => {}); -function makeEMSFileSource(tooltipProperties) { - const emsFileSource = new EMSFileSource({ - tooltipProperties: tooltipProperties, - }); - emsFileSource.getEMSFileLayer = () => { +function makeEMSFileSource(tooltipProperties: string[]) { + const emsFileSource = new EMSFileSource({ tooltipProperties }); + emsFileSource.getEMSFileLayer = async () => { return { getFieldsInLanguage() { return [ diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.tsx similarity index 63% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js rename to x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.tsx index 5802a223e4846..5115da510cc5b 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.tsx @@ -4,40 +4,56 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractVectorSource } from '../vector_source'; +import React, { ReactElement } from 'react'; +import { i18n } from '@kbn/i18n'; +import { Feature } from 'geojson'; +import { Adapters } from 'src/plugins/inspector/public'; +import { Attribution, ImmutableSourceProperty, SourceEditorArgs } from '../source'; +import { AbstractVectorSource, GeoJsonWithMeta, IVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; -import React from 'react'; import { SOURCE_TYPES, FIELD_ORIGIN } from '../../../../common/constants'; +// @ts-ignore import { getEMSClient } from '../../../meta'; -import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { UpdateSourceEditor } from './update_source_editor'; import { EMSFileField } from '../../fields/ems_file_field'; import { registerSource } from '../source_registry'; +import { IField } from '../../fields/field'; +import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types'; +import { ITooltipProperty } from '../../tooltips/tooltip_property'; + +export interface IEmsFileSource extends IVectorSource { + getEMSFileLayer(): Promise; + createField({ fieldName }: { fieldName: string }): IField; +} export const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', { defaultMessage: 'EMS Boundaries', }); -export class EMSFileSource extends AbstractVectorSource { +export class EMSFileSource extends AbstractVectorSource implements IEmsFileSource { static type = SOURCE_TYPES.EMS_FILE; - static createDescriptor({ id, tooltipProperties = [] }) { + static createDescriptor({ id, tooltipProperties = [] }: Partial) { return { type: EMSFileSource.type, - id, + id: id!, tooltipProperties, }; } - constructor(descriptor, inspectorAdapters) { + private readonly _tooltipFields: IField[]; + readonly _descriptor: EMSFileSourceDescriptor; + + constructor(descriptor: Partial, inspectorAdapters?: Adapters) { super(EMSFileSource.createDescriptor(descriptor), inspectorAdapters); + this._descriptor = EMSFileSource.createDescriptor(descriptor); this._tooltipFields = this._descriptor.tooltipProperties.map(propertyKey => this.createField({ fieldName: propertyKey }) ); } - createField({ fieldName }) { + createField({ fieldName }: { fieldName: string }): IField { return new EMSFileField({ fieldName, source: this, @@ -45,7 +61,7 @@ export class EMSFileSource extends AbstractVectorSource { }); } - renderSourceSettingsEditor({ onChange }) { + renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null { return ( { + // @ts-ignore const emsClient = getEMSClient(); + // @ts-ignore const emsFileLayers = await emsClient.getFileLayers(); + // @ts-ignore const emsFileLayer = emsFileLayers.find(fileLayer => fileLayer.getId() === this._descriptor.id); if (!emsFileLayer) { throw new Error( @@ -73,19 +92,23 @@ export class EMSFileSource extends AbstractVectorSource { return emsFileLayer; } - async getGeoJsonWithMeta() { + async getGeoJsonWithMeta(): Promise { const emsFileLayer = await this.getEMSFileLayer(); + // @ts-ignore const featureCollection = await AbstractVectorSource.getGeoJson({ + // @ts-ignore format: emsFileLayer.getDefaultFormatType(), featureCollectionPath: 'data', + // @ts-ignore fetchUrl: emsFileLayer.getDefaultFormatUrl(), }); + // @ts-ignore const emsIdField = emsFileLayer._config.fields.find(field => { return field.type === 'id'; }); - featureCollection.features.forEach((feature, index) => { - feature.id = emsIdField ? feature.properties[emsIdField.id] : index; + featureCollection.features.forEach((feature: Feature, index: number) => { + feature.id = emsIdField ? feature!.properties![emsIdField.id] : index; }); return { @@ -94,10 +117,11 @@ export class EMSFileSource extends AbstractVectorSource { }; } - async getImmutableProperties() { + async getImmutableProperties(): Promise { let emsLink; try { const emsFileLayer = await this.getEMSFileLayer(); + // @ts-ignore emsLink = emsFileLayer.getEMSHotLink(); } catch (error) { // ignore error if EMS layer id could not be found @@ -118,23 +142,27 @@ export class EMSFileSource extends AbstractVectorSource { ]; } - async getDisplayName() { + async getDisplayName(): Promise { try { const emsFileLayer = await this.getEMSFileLayer(); + // @ts-ignore return emsFileLayer.getDisplayName(); } catch (error) { return this._descriptor.id; } } - async getAttributions() { + async getAttributions(): Promise { const emsFileLayer = await this.getEMSFileLayer(); + // @ts-ignore return emsFileLayer.getAttributions(); } async getLeftJoinFields() { const emsFileLayer = await this.getEMSFileLayer(); + // @ts-ignore const fields = emsFileLayer.getFieldsInLanguage(); + // @ts-ignore return fields.map(f => this.createField({ fieldName: f.name })); } @@ -142,16 +170,17 @@ export class EMSFileSource extends AbstractVectorSource { return this._tooltipFields.length > 0; } - async filterAndFormatPropertiesToHtml(properties) { - const tooltipProperties = this._tooltipFields.map(field => { + async filterAndFormatPropertiesToHtml(properties: unknown): Promise { + const promises = this._tooltipFields.map(field => { + // @ts-ignore const value = properties[field.getName()]; return field.createTooltipProperty(value); }); - return Promise.all(tooltipProperties); + return Promise.all(promises); } - async getSupportedShapeTypes() { + async getSupportedShapeTypes(): Promise { return [VECTOR_SHAPE_TYPES.POLYGON]; } } diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.ts similarity index 82% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js rename to x-pack/plugins/maps/public/layers/sources/ems_file_source/index.ts index e9bf592c6d2b7..c1e6e0d76af1f 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.ts @@ -5,4 +5,4 @@ */ export { emsBoundariesLayerWizardConfig } from './ems_boundaries_layer_wizard'; -export { EMSFileSource } from './ems_file_source'; +export { EMSFileSource, IEmsFileSource } from './ems_file_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.tsx similarity index 61% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js rename to x-pack/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.tsx index b7687fec43272..806213b667ba4 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.tsx @@ -5,18 +5,28 @@ */ import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { TooltipSelector } from '../../../components/tooltip_selector'; -import { getEMSClient } from '../../../meta'; import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { TooltipSelector } from '../../../components/tooltip_selector'; +// @ts-ignore +import { getEMSClient } from '../../../meta'; +import { IEmsFileSource } from './ems_file_source'; +import { IField } from '../../fields/field'; +import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; -export class UpdateSourceEditor extends Component { - static propTypes = { - onChange: PropTypes.func.isRequired, - tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired, - source: PropTypes.object, - }; +interface Props { + layerId: string; + onChange: (args: OnSourceChangeArgs) => void; + source: IEmsFileSource; + tooltipFields: IField[]; +} + +interface State { + fields: IField[] | null; +} + +export class UpdateSourceEditor extends Component { + private _isMounted: boolean = false; state = { fields: null, @@ -34,23 +44,29 @@ export class UpdateSourceEditor extends Component { async loadFields() { let fields; try { + // @ts-ignore const emsClient = getEMSClient(); + // @ts-ignore const emsFiles = await emsClient.getFileLayers(); - const emsFile = emsFiles.find(emsFile => emsFile.getId() === this.props.layerId); - const emsFields = emsFile.getFieldsInLanguage(); + // @ts-ignore + const taregetEmsFile = emsFiles.find(emsFile => emsFile.getId() === this.props.layerId); + // @ts-ignore + const emsFields = taregetEmsFile.getFieldsInLanguage(); + // @ts-ignore fields = emsFields.map(field => this.props.source.createField({ fieldName: field.name })); } catch (e) { - //swallow this error. when a matching EMS-config cannot be found, the source already will have thrown errors during the data request. This will propagate to the vector-layer and be displayed in the UX + // When a matching EMS-config cannot be found, the source already will have thrown errors during the data request. + // This will propagate to the vector-layer and be displayed in the UX fields = []; } if (this._isMounted) { - this.setState({ fields: fields }); + this.setState({ fields }); } } - _onTooltipPropertiesSelect = propertyNames => { - this.props.onChange({ propName: 'tooltipProperties', value: propertyNames }); + _onTooltipPropertiesSelect = (selectedFieldNames: string[]) => { + this.props.onChange({ propName: 'tooltipProperties', value: selectedFieldNames }); }; render() { diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index fc745edbabee8..391ab5691938d 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -12,12 +12,11 @@ import { EMSTMSSource, sourceTitle } from './ems_tms_source'; import { VectorTileLayer } from '../../vector_tile_layer'; // @ts-ignore import { TileServiceSelect } from './tile_service_select'; -// @ts-ignore -import { isEmsEnabled } from '../../../meta'; +import { getIsEmsEnabled } from '../../../kibana_services'; export const emsBaseMapLayerWizardConfig: LayerWizard = { checkVisibility: () => { - return isEmsEnabled(); + return getIsEmsEnabled(); }, description: i18n.translate('xpack.maps.source.emsTileDescription', { defaultMessage: 'Tile map service from Elastic Maps Service', diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js index 3bed9b2c09570..b20a3c80e0510 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js @@ -7,13 +7,12 @@ import _ from 'lodash'; import React from 'react'; import { AbstractTMSSource } from '../tms_source'; - import { getEMSClient } from '../../../meta'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { SOURCE_TYPES } from '../../../../common/constants'; -import { getInjectedVarFunc, getUiSettings } from '../../../kibana_services'; +import { getEmsTileLayerId, getUiSettings } from '../../../kibana_services'; import { registerSource } from '../source_registry'; export const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { @@ -125,7 +124,7 @@ export class EMSTMSSource extends AbstractTMSSource { } const isDarkMode = getUiSettings().get('theme:darkMode', false); - const emsTileLayerId = getInjectedVarFunc()('emsTileLayerId'); + const emsTileLayerId = getEmsTileLayerId(); return isDarkMode ? emsTileLayerId.dark : emsTileLayerId.bright; } } diff --git a/x-pack/plugins/maps/public/layers/sources/ems_unavailable_message.js b/x-pack/plugins/maps/public/layers/sources/ems_unavailable_message.ts similarity index 81% rename from x-pack/plugins/maps/public/layers/sources/ems_unavailable_message.js rename to x-pack/plugins/maps/public/layers/sources/ems_unavailable_message.ts index bc50890a0f4a3..748016cf889e2 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_unavailable_message.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_unavailable_message.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getInjectedVarFunc } from '../../kibana_services'; import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { getIsEmsEnabled } from '../../kibana_services'; -export function getEmsUnavailableMessage() { - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); +export function getEmsUnavailableMessage(): string { + const isEmsEnabled = getIsEmsEnabled(); if (isEmsEnabled) { return i18n.translate('xpack.maps.source.ems.noAccessDescription', { defaultMessage: diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index 58e6e39aaa1f9..5f6061b38678c 100644 --- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -20,6 +20,7 @@ import { VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor'; +import { ITooltipProperty } from '../../tooltips/tooltip_property'; export const sourceTitle = i18n.translate( 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', @@ -152,6 +153,10 @@ export class MVTSingleLayerVectorSource extends AbstractSource getApplyGlobalQuery(): boolean { return false; } + + async filterAndFormatPropertiesToHtml(properties: unknown): Promise { + return []; + } } registerSource({ diff --git a/x-pack/plugins/maps/public/layers/sources/source.ts b/x-pack/plugins/maps/public/layers/sources/source.ts index af934d7464f61..f53cf689fbfe5 100644 --- a/x-pack/plugins/maps/public/layers/sources/source.ts +++ b/x-pack/plugins/maps/public/layers/sources/source.ts @@ -16,10 +16,16 @@ import { copyPersistentState } from '../../reducers/util'; import { SourceDescriptor } from '../../../common/descriptor_types'; import { IField } from '../fields/field'; import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; +import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view'; + +export type SourceEditorArgs = { + onChange: (args: OnSourceChangeArgs) => void; +}; export type ImmutableSourceProperty = { label: string; value: string; + link?: string; }; export type Attribution = { @@ -48,7 +54,7 @@ export interface ISource { getImmutableProperties(): Promise; getAttributions(): Promise; isESSource(): boolean; - renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement | null; + renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null; supportsFitToBounds(): Promise; isJoinable(): boolean; cloneDescriptor(): SourceDescriptor; @@ -124,7 +130,7 @@ export class AbstractSource implements ISource { return []; } - renderSourceSettingsEditor() { + renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null { return null; } diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts index 804915dd73052..2dd6bcd858137 100644 --- a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts @@ -15,6 +15,7 @@ import { VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; +import { ITooltipProperty } from '../../tooltips/tooltip_property'; export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; @@ -24,6 +25,7 @@ export type GeoJsonWithMeta = { }; export interface IVectorSource extends ISource { + filterAndFormatPropertiesToHtml(properties: unknown): Promise; getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; getGeoJsonWithMeta( layerName: 'string', @@ -39,6 +41,7 @@ export interface IVectorSource extends ISource { } export class AbstractVectorSource extends AbstractSource implements IVectorSource { + filterAndFormatPropertiesToHtml(properties: unknown): Promise; getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; getGeoJsonWithMeta( layerName: 'string', diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js index c3245e8e98db2..77183e334cb11 100644 --- a/x-pack/plugins/maps/public/meta.js +++ b/x-pack/plugins/maps/public/meta.js @@ -13,17 +13,27 @@ import { } from '../common/constants'; import { i18n } from '@kbn/i18n'; import { EMSClient } from '@elastic/ems-client'; -import { getInjectedVarFunc, getLicenseId } from './kibana_services'; +import { + getInjectedVarFunc, + getLicenseId, + getIsEmsEnabled, + getRegionmapLayers, + getTilemap, + getEmsFileApiUrl, + getEmsTileApiUrl, + getEmsLandingPageUrl, + getEmsFontLibraryUrl, +} from './kibana_services'; import fetch from 'node-fetch'; const GIS_API_RELATIVE = `../${GIS_API_PATH}`; export function getKibanaRegionList() { - return getInjectedVarFunc()('regionmapLayers'); + return getRegionmapLayers(); } export function getKibanaTileMap() { - return getInjectedVarFunc()('tilemap'); + return getTilemap(); } function relativeToAbsolute(url) { @@ -36,15 +46,12 @@ function fetchFunction(...args) { return fetch(...args); } -export function isEmsEnabled() { - return getInjectedVarFunc()('isEmsEnabled', true); -} - let emsClient = null; let latestLicenseId = null; export function getEMSClient() { if (!emsClient) { - if (isEmsEnabled()) { + const isEmsEnabled = getIsEmsEnabled(); + if (isEmsEnabled) { const proxyElasticMapsServiceInMaps = getInjectedVarFunc()( 'proxyElasticMapsServiceInMaps', false @@ -52,10 +59,10 @@ export function getEMSClient() { const proxyPath = ''; const tileApiUrl = proxyElasticMapsServiceInMaps ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_TILES_CATALOGUE_PATH}`) - : getInjectedVarFunc()('emsTileApiUrl'); + : getEmsTileApiUrl(); const fileApiUrl = proxyElasticMapsServiceInMaps ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_FILES_CATALOGUE_PATH}`) - : getInjectedVarFunc()('emsFileApiUrl'); + : getEmsFileApiUrl(); emsClient = new EMSClient({ language: i18n.getLocale(), @@ -63,7 +70,7 @@ export function getEMSClient() { appName: EMS_APP_NAME, tileApiUrl, fileApiUrl, - landingPageUrl: getInjectedVarFunc()('emsLandingPageUrl'), + landingPageUrl: getEmsLandingPageUrl(), fetchFunction: fetchFunction, //import this from client-side, so the right instance is returned (bootstrapped from common/* would not work proxyPath, }); @@ -89,13 +96,13 @@ export function getEMSClient() { } export function getGlyphUrl() { - if (!isEmsEnabled()) { + if (!getIsEmsEnabled()) { return ''; } return getInjectedVarFunc()('proxyElasticMapsServiceInMaps', false) ? relativeToAbsolute(`../${GIS_API_PATH}/${EMS_TILES_CATALOGUE_PATH}/${EMS_GLYPHS_PATH}`) + `/{fontstack}/{range}` - : getInjectedVarFunc()('emsFontLibraryUrl', true); + : getEmsFontLibraryUrl(); } export function isRetina() { diff --git a/x-pack/plugins/maps/public/meta.test.js b/x-pack/plugins/maps/public/meta.test.js index d83f2adb35ef7..c6cc9b53b9301 100644 --- a/x-pack/plugins/maps/public/meta.test.js +++ b/x-pack/plugins/maps/public/meta.test.js @@ -25,6 +25,11 @@ describe('default use without proxy', () => { require('./kibana_services').getLicenseId = () => { return 'foobarlicenseid'; }; + require('./kibana_services').getIsEmsEnabled = () => true; + require('./kibana_services').getEmsTileLayerId = () => '123'; + require('./kibana_services').getEmsFileApiUrl = () => 'https://file-api'; + require('./kibana_services').getEmsTileApiUrl = () => 'https://tile-api'; + require('./kibana_services').getEmsLandingPageUrl = () => 'http://test.com'; }); it('should construct EMSClient with absolute file and tile API urls', async () => { diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 21bff95731580..8fe16c0d99d76 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -32,6 +32,7 @@ import { setUiSettings, setVisualizations, setSearchService, + setMapConfig, } from './kibana_services'; import { featureCatalogueEntry } from './feature_catalogue_entry'; // @ts-ignore @@ -47,12 +48,13 @@ export interface MapsPluginSetupDependencies { home: HomePublicPluginSetup; visualizations: VisualizationsSetup; embeddable: EmbeddableSetup; + mapsLegacy: { config: unknown }; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsPluginStartDependencies {} export const bindSetupCoreAndPlugins = (core: CoreSetup, plugins: any) => { - const { licensing } = plugins; + const { licensing, mapsLegacy } = plugins; const { injectedMetadata, uiSettings, http, notifications } = core; if (licensing) { licensing.license$.subscribe(({ uid }: { uid: string }) => setLicenseId(uid)); @@ -63,6 +65,7 @@ export const bindSetupCoreAndPlugins = (core: CoreSetup, plugins: any) => { setInjectedVarFunc(injectedMetadata.getInjectedVar); setVisualizations(plugins.visualizations); setUiSettings(uiSettings); + setMapConfig(mapsLegacy.config); }; export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts index 2463da054d140..9221f8c500326 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts @@ -92,6 +92,7 @@ describe('Analytics job clone action', () => { training_percent: 20, randomize_seed: -2228827740028660200, num_top_feature_importance_values: 4, + loss_function: 'mse', }, }, analyzed_fields: { @@ -192,6 +193,7 @@ describe('Analytics job clone action', () => { training_percent: 20, randomize_seed: -2228827740028660200, num_top_feature_importance_values: 4, + loss_function: 'mse', }, }, analyzed_fields: { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index cc75ddbe08cfb..cfb11856670c4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -179,6 +179,10 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo // By default it is randomly generated ignore: true, }, + loss_function: { + optional: true, + defaultValue: 'mse', + }, }, } : {}), diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index cfcba081983c2..a46f35cbd4d20 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -1,3 +1,7 @@ +.ml-swimlane-selector { + visibility: hidden; +} + .ml-explorer { width: 100%; display: inline-block; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 86d16776b68e2..8fd2479817807 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import DragSelect from 'dragselect/dist/ds.min.js'; import { Subject } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { takeUntil } from 'rxjs/operators'; import { EuiFlexGroup, @@ -120,6 +120,7 @@ export class Explorer extends React.Component { disableDragSelectOnMouseLeave = true; dragSelect = new DragSelect({ + selectorClass: 'ml-swimlane-selector', selectables: document.getElementsByClassName('sl-cell'), callback(elements) { if (elements.length > 1 && !ALLOW_CELL_RANGE_SELECTION) { @@ -169,12 +170,7 @@ export class Explorer extends React.Component { }; componentDidMount() { - limit$ - .pipe( - takeUntil(this._unsubscribeAll), - map(d => d.val) - ) - .subscribe(explorerService.setSwimlaneLimit); + limit$.pipe(takeUntil(this._unsubscribeAll)).subscribe(explorerService.setSwimlaneLimit); // Required to redraw the time series chart when the container is resized. this.resizeChecker = new ResizeChecker(this.resizeRef.current); diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx index 657f1c6c7af2e..cf65419e4bd80 100644 --- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx +++ b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx @@ -9,8 +9,6 @@ import { act } from 'react-dom/test-utils'; import { shallow } from 'enzyme'; import { SelectLimit } from './select_limit'; -jest.useFakeTimers(); - describe('SelectLimit', () => { test('creates correct initial selected value', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx index 383d07eb7a9f6..03e3273b80832 100644 --- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx +++ b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx @@ -9,7 +9,7 @@ */ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { Subject } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { EuiSelect } from '@elastic/eui'; @@ -20,13 +20,13 @@ const euiOptions = limitOptions.map(limit => ({ text: `${limit}`, })); -export const limit$ = new Subject(); export const defaultLimit = limitOptions[1]; +export const limit$ = new BehaviorSubject(defaultLimit); export const useSwimlaneLimit = (): [number, (newLimit: number) => void] => { const limit = useObservable(limit$, defaultLimit); - return [limit, (newLimit: number) => limit$.next(newLimit)]; + return [limit!, (newLimit: number) => limit$.next(newLimit)]; }; export const SelectLimit = () => { diff --git a/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts new file mode 100644 index 0000000000000..2d650b1bbd9d1 --- /dev/null +++ b/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { serviceNowConnector } from '../objects/case'; + +import { TOASTER } from '../screens/configure_cases'; + +import { goToEditExternalConnection } from '../tasks/all_cases'; +import { + addServiceNowConnector, + openAddNewConnectorOption, + saveChanges, + selectLastConnectorCreated, +} from '../tasks/configure_cases'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { CASES } from '../urls/navigation'; + +describe('Cases connectors', () => { + before(() => { + cy.server(); + cy.route('POST', '**/api/action').as('createConnector'); + cy.route('POST', '**/api/cases/configure').as('saveConnector'); + }); + + it('Configures a new connector', () => { + loginAndWaitForPageWithoutDateRange(CASES); + goToEditExternalConnection(); + openAddNewConnectorOption(); + addServiceNowConnector(serviceNowConnector); + + cy.wait('@createConnector') + .its('status') + .should('eql', 200); + cy.get(TOASTER).should('have.text', "Created 'New connector'"); + + selectLastConnectorCreated(); + saveChanges(); + + cy.wait('@saveConnector', { timeout: 10000 }) + .its('status') + .should('eql', 200); + cy.get(TOASTER).should('have.text', 'Saved external connection settings'); + }); +}); diff --git a/x-pack/plugins/siem/cypress/objects/case.ts b/x-pack/plugins/siem/cypress/objects/case.ts index 1c7bc34bca417..12d3f925169af 100644 --- a/x-pack/plugins/siem/cypress/objects/case.ts +++ b/x-pack/plugins/siem/cypress/objects/case.ts @@ -14,6 +14,13 @@ export interface TestCase { reporter: string; } +export interface Connector { + connectorName: string; + URL: string; + username: string; + password: string; +} + const caseTimeline: Timeline = { title: 'SIEM test', description: 'description', @@ -27,3 +34,10 @@ export const case1: TestCase = { timeline: caseTimeline, reporter: 'elastic', }; + +export const serviceNowConnector: Connector = { + connectorName: 'New connector', + URL: 'https://www.test.service-now.com', + username: 'Username Name', + password: 'password', +}; diff --git a/x-pack/plugins/siem/cypress/screens/all_cases.ts b/x-pack/plugins/siem/cypress/screens/all_cases.ts index b1e4c66515352..4fa6b69eea7c3 100644 --- a/x-pack/plugins/siem/cypress/screens/all_cases.ts +++ b/x-pack/plugins/siem/cypress/screens/all_cases.ts @@ -39,3 +39,5 @@ export const ALL_CASES_TAGS = (index: number) => { }; export const ALL_CASES_TAGS_COUNT = '[data-test-subj="options-filter-popover-button-Tags"]'; + +export const EDIT_EXTERNAL_CONNECTION = '[data-test-subj="configure-case-button"]'; diff --git a/x-pack/plugins/siem/cypress/screens/configure_cases.ts b/x-pack/plugins/siem/cypress/screens/configure_cases.ts new file mode 100644 index 0000000000000..5a1e897c43e27 --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/configure_cases.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ADD_NEW_CONNECTOR_OPTION_LINK = + '[data-test-subj="case-configure-add-connector-button"]'; + +export const CONNECTOR = (id: string) => { + return `[data-test-subj='dropdown-connector-${id}']`; +}; + +export const CONNECTOR_NAME = '[data-test-subj="nameInput"]'; + +export const CONNECTORS_DROPDOWN = '[data-test-subj="dropdown-connectors"]'; + +export const PASSWORD = '[data-test-subj="connector-servicenow-password-form-input"]'; + +export const SAVE_BTN = '[data-test-subj="saveNewActionButton"]'; + +export const SAVE_CHANGES_BTN = '[data-test-subj="case-configure-action-bottom-bar-save-button"]'; + +export const SERVICE_NOW_CONNECTOR_CARD = '[data-test-subj=".servicenow-card"]'; + +export const TOASTER = '[data-test-subj="euiToastHeader"]'; + +export const URL = '[data-test-subj="apiUrlFromInput"]'; + +export const USERNAME = '[data-test-subj="connector-servicenow-username-form-input"]'; diff --git a/x-pack/plugins/siem/cypress/tasks/all_cases.ts b/x-pack/plugins/siem/cypress/tasks/all_cases.ts index f374532201324..8ebe35e173e59 100644 --- a/x-pack/plugins/siem/cypress/tasks/all_cases.ts +++ b/x-pack/plugins/siem/cypress/tasks/all_cases.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ALL_CASES_NAME, ALL_CASES_CREATE_NEW_CASE_BTN } from '../screens/all_cases'; +import { + ALL_CASES_NAME, + ALL_CASES_CREATE_NEW_CASE_BTN, + EDIT_EXTERNAL_CONNECTION, +} from '../screens/all_cases'; export const goToCreateNewCase = () => { cy.get(ALL_CASES_CREATE_NEW_CASE_BTN).click({ force: true }); @@ -13,3 +17,7 @@ export const goToCreateNewCase = () => { export const goToCaseDetails = () => { cy.get(ALL_CASES_NAME).click({ force: true }); }; + +export const goToEditExternalConnection = () => { + cy.get(EDIT_EXTERNAL_CONNECTION).click({ force: true }); +}; diff --git a/x-pack/plugins/siem/cypress/tasks/configure_cases.ts b/x-pack/plugins/siem/cypress/tasks/configure_cases.ts new file mode 100644 index 0000000000000..9172e02708ae7 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/configure_cases.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ADD_NEW_CONNECTOR_OPTION_LINK, + CONNECTOR, + CONNECTOR_NAME, + CONNECTORS_DROPDOWN, + PASSWORD, + SAVE_BTN, + SAVE_CHANGES_BTN, + SERVICE_NOW_CONNECTOR_CARD, + URL, + USERNAME, +} from '../screens/configure_cases'; +import { MAIN_PAGE } from '../screens/siem_main'; + +import { Connector } from '../objects/case'; + +export const addServiceNowConnector = (connector: Connector) => { + cy.get(SERVICE_NOW_CONNECTOR_CARD).click(); + cy.get(CONNECTOR_NAME).type(connector.connectorName); + cy.get(URL).type(connector.URL); + cy.get(USERNAME).type(connector.username); + cy.get(PASSWORD).type(connector.password); + cy.get(SAVE_BTN).click({ force: true }); +}; + +export const openAddNewConnectorOption = () => { + cy.get(MAIN_PAGE).then($page => { + if ($page.find(SERVICE_NOW_CONNECTOR_CARD).length !== 1) { + cy.wait(1000); + cy.get(ADD_NEW_CONNECTOR_OPTION_LINK).click({ force: true }); + } + }); +}; + +export const saveChanges = () => { + cy.get(SAVE_CHANGES_BTN).click(); +}; + +export const selectLastConnectorCreated = () => { + cy.get(CONNECTORS_DROPDOWN).click({ force: true }); + cy.get('@createConnector') + .its('response') + .then(response => { + cy.get(CONNECTOR(response.body.id)).click(); + }); +}; diff --git a/x-pack/plugins/siem/public/containers/case/translations.ts b/x-pack/plugins/siem/public/containers/case/translations.ts index d5ea287fd2cdd..79edcc56b0362 100644 --- a/x-pack/plugins/siem/public/containers/case/translations.ts +++ b/x-pack/plugins/siem/public/containers/case/translations.ts @@ -50,19 +50,11 @@ export const REOPENED_CASES = ({ defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', }); -export const TAG_FETCH_FAILURE = i18n.translate( - 'xpack.siem.containers.case.tagFetchFailDescription', - { - defaultMessage: 'Failed to fetch Tags', - } -); - -export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = i18n.translate( - 'xpack.siem.containers.case.pushToExterService', - { - defaultMessage: 'Successfully sent to ServiceNow', - } -); +export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) => + i18n.translate('xpack.siem.containers.case.pushToExternalService', { + values: { serviceName }, + defaultMessage: 'Successfully sent to { serviceName }', + }); export const ERROR_PUSH_TO_SERVICE = i18n.translate( 'xpack.siem.case.configure.errorPushingToService', diff --git a/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx index 0848d12c8d308..1603beddbb1dc 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx @@ -122,13 +122,14 @@ describe('useGetCaseUserActions', () => { ...basicPush, firstPushIndex: 3, lastPushIndex: 3, + commentsToUpdate: [], hasDataToPush: false, }, }, }); }); - it('Correctly marks first/last index - hasDataToPush: true', () => { + it('Correctly marks first/last index and comment id - hasDataToPush: true', () => { const userActions = [ ...caseUserActions, getUserAction(['pushed'], 'push-to-service'), @@ -142,6 +143,83 @@ describe('useGetCaseUserActions', () => { ...basicPush, firstPushIndex: 3, lastPushIndex: 3, + commentsToUpdate: [userActions[userActions.length - 1].commentId], + hasDataToPush: true, + }, + }, + }); + }); + + it('Correctly marks first/last index and multiple comment ids, both needs push', () => { + const userActions = [ + ...caseUserActions, + getUserAction(['pushed'], 'push-to-service'), + getUserAction(['comment'], 'create'), + { ...getUserAction(['comment'], 'create'), commentId: 'muahaha' }, + ]; + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: true, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 3, + lastPushIndex: 3, + commentsToUpdate: [ + userActions[userActions.length - 2].commentId, + userActions[userActions.length - 1].commentId, + ], + hasDataToPush: true, + }, + }, + }); + }); + + it('Correctly marks first/last index and multiple comment ids, one needs push', () => { + const userActions = [ + ...caseUserActions, + getUserAction(['pushed'], 'push-to-service'), + getUserAction(['comment'], 'create'), + getUserAction(['pushed'], 'push-to-service'), + { ...getUserAction(['comment'], 'create'), commentId: 'muahaha' }, + ]; + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: true, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 3, + lastPushIndex: 5, + commentsToUpdate: [userActions[userActions.length - 1].commentId], + hasDataToPush: true, + }, + }, + }); + }); + + it('Correctly marks first/last index and multiple comment ids, one needs push and one needs update', () => { + const userActions = [ + ...caseUserActions, + getUserAction(['pushed'], 'push-to-service'), + getUserAction(['comment'], 'create'), + getUserAction(['pushed'], 'push-to-service'), + { ...getUserAction(['comment'], 'create'), commentId: 'muahaha' }, + getUserAction(['comment'], 'update'), + getUserAction(['comment'], 'update'), + ]; + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: true, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 3, + lastPushIndex: 5, + commentsToUpdate: [ + userActions[userActions.length - 3].commentId, + userActions[userActions.length - 1].commentId, + ], hasDataToPush: true, }, }, @@ -162,6 +240,7 @@ describe('useGetCaseUserActions', () => { ...basicPush, firstPushIndex: 3, lastPushIndex: 3, + commentsToUpdate: [], hasDataToPush: false, }, }, @@ -182,11 +261,34 @@ describe('useGetCaseUserActions', () => { ...basicPush, firstPushIndex: 3, lastPushIndex: 5, + commentsToUpdate: [], hasDataToPush: false, }, }, }); }); + it('Correctly handles comment update with multiple push actions', () => { + const userActions = [ + ...caseUserActions, + getUserAction(['pushed'], 'push-to-service'), + getUserAction(['comment'], 'create'), + getUserAction(['pushed'], 'push-to-service'), + getUserAction(['comment'], 'update'), + ]; + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: true, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 3, + lastPushIndex: 5, + commentsToUpdate: [userActions[userActions.length - 1].commentId], + hasDataToPush: true, + }, + }, + }); + }); it('Multiple connector tracking - hasDataToPush: true', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); @@ -215,6 +317,7 @@ describe('useGetCaseUserActions', () => { ...basicPush, firstPushIndex: 3, lastPushIndex: 3, + commentsToUpdate: [userActions[userActions.length - 2].commentId], hasDataToPush: true, }, '456': { @@ -224,6 +327,7 @@ describe('useGetCaseUserActions', () => { externalId: 'other_external_id', firstPushIndex: 5, lastPushIndex: 5, + commentsToUpdate: [], hasDataToPush: false, }, }, @@ -257,6 +361,7 @@ describe('useGetCaseUserActions', () => { ...basicPush, firstPushIndex: 3, lastPushIndex: 3, + commentsToUpdate: [userActions[userActions.length - 2].commentId], hasDataToPush: true, }, '456': { @@ -266,6 +371,7 @@ describe('useGetCaseUserActions', () => { externalId: 'other_external_id', firstPushIndex: 5, lastPushIndex: 5, + commentsToUpdate: [], hasDataToPush: false, }, }, diff --git a/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx index a2290f946be9b..5afe06a9828e5 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx @@ -14,9 +14,10 @@ import { CaseExternalService, CaseUserActions, ElasticUser } from './types'; import { convertToCamelCase, parseString } from './utils'; import { CaseFullExternalService } from '../../../../case/common/api/cases'; -interface CaseService extends CaseExternalService { +export interface CaseService extends CaseExternalService { firstPushIndex: number; lastPushIndex: number; + commentsToUpdate: string[]; hasDataToPush: boolean; } @@ -48,6 +49,10 @@ export interface UseGetCaseUserActions extends CaseUserActionsState { const getExternalService = (value: string): CaseExternalService | null => convertToCamelCase(parseString(`${value}`)); +interface CommentsAndIndex { + commentId: string; + commentIndex: number; +} export const getPushedInfo = ( caseUserActions: CaseUserActions[], @@ -69,11 +74,25 @@ export const getPushedInfo = ( .action !== 'push-to-service' ); }; + const commentsAndIndex = caseUserActions.reduce( + (bacc, mua, index) => + mua.actionField[0] === 'comment' && mua.commentId != null + ? [ + ...bacc, + { + commentId: mua.commentId, + commentIndex: index, + }, + ] + : bacc, + [] + ); - const caseServices = caseUserActions.reduce((acc, cua, i) => { + let caseServices = caseUserActions.reduce((acc, cua, i) => { if (cua.action !== 'push-to-service') { return acc; } + const externalService = getExternalService(`${cua.newValue}`); if (externalService === null) { return acc; @@ -87,6 +106,7 @@ export const getPushedInfo = ( ...acc[externalService.connectorId], ...externalService, lastPushIndex: i, + commentsToUpdate: [], }, } : { @@ -95,11 +115,31 @@ export const getPushedInfo = ( firstPushIndex: i, lastPushIndex: i, hasDataToPush: hasDataToPushForConnector(externalService.connectorId), + commentsToUpdate: [], }, }), }; }, {}); + caseServices = Object.keys(caseServices).reduce((acc, key) => { + return { + ...acc, + [key]: { + ...caseServices[key], + // if the comment happens after the lastUpdateToCaseIndex, it should be included in commentsToUpdate + commentsToUpdate: commentsAndIndex.reduce( + (bacc, currentComment) => + currentComment.commentIndex > caseServices[key].lastPushIndex + ? bacc.indexOf(currentComment.commentId) > -1 + ? [...bacc.filter(e => e !== currentComment.commentId), currentComment.commentId] + : [...bacc, currentComment.commentId] + : bacc, + [] + ), + }, + }; + }, {}); + const hasDataToPush = caseServices[caseConnectorId] != null ? caseServices[caseConnectorId].hasDataToPush : true; return { diff --git a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx index 72609e15d1ec4..96fa824c1cadd 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx @@ -19,6 +19,7 @@ import { serviceConnectorUser, } from './mock'; import * as api from './api'; +import { CaseServices } from './use_get_case_user_actions'; jest.mock('./api'); @@ -32,6 +33,7 @@ describe('usePostPushToService', () => { ...basicPush, firstPushIndex: 1, lastPushIndex: 1, + commentsToUpdate: [basicComment.id], hasDataToPush: false, }, }, @@ -64,6 +66,7 @@ describe('usePostPushToService', () => { ...basicPush, firstPushIndex: 1, lastPushIndex: 1, + commentsToUpdate: [basicComment.id], hasDataToPush: true, }, '456': { @@ -71,6 +74,7 @@ describe('usePostPushToService', () => { connectorId: '456', externalId: 'other_external_id', firstPushIndex: 4, + commentsToUpdate: [basicComment.id], lastPushIndex: 6, hasDataToPush: false, }, @@ -127,6 +131,31 @@ describe('usePostPushToService', () => { await waitForNextUpdate(); expect(spyOnPushToService).toBeCalledWith( samplePush.connectorId, + formatServiceRequestData(basicCase, '123', sampleCaseServices as CaseServices), + abortCtrl.signal + ); + }); + }); + + it('calls pushToService with correct arguments when no push history', async () => { + const samplePush2 = { + caseId: pushedCase.id, + caseServices: {}, + connectorName: 'connector name', + connectorId: 'none', + updateCase, + }; + const spyOnPushToService = jest.spyOn(api, 'pushToService'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush2); + await waitForNextUpdate(); + expect(spyOnPushToService).toBeCalledWith( + samplePush2.connectorId, formatServiceRequestData(basicCase, 'none', {}), abortCtrl.signal ); diff --git a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx index 3d0836cdc8adf..7f4c4a4276172 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx @@ -122,7 +122,10 @@ export const usePostPushToService = (): UsePostPushToService => { dispatch({ type: 'FETCH_SUCCESS_PUSH_SERVICE', payload: responseService }); dispatch({ type: 'FETCH_SUCCESS_PUSH_CASE', payload: responseCase }); updateCase(responseCase); - displaySuccessToast(i18n.SUCCESS_SEND_TO_EXTERNAL_SERVICE, dispatchToaster); + displaySuccessToast( + i18n.SUCCESS_SEND_TO_EXTERNAL_SERVICE(connectorName), + dispatchToaster + ); } } catch (error) { if (!cancel) { @@ -156,25 +159,12 @@ export const formatServiceRequestData = ( createdBy, comments, description, - externalService, title, updatedAt, updatedBy, } = myCase; - let actualExternalService = externalService; - if ( - externalService != null && - externalService.connectorId !== connectorId && - caseServices[connectorId] - ) { - actualExternalService = caseServices[connectorId]; - } else if ( - externalService != null && - externalService.connectorId !== connectorId && - !caseServices[connectorId] - ) { - actualExternalService = null; - } + const actualExternalService = caseServices[connectorId] ?? null; + return { caseId, createdAt, @@ -183,17 +173,9 @@ export const formatServiceRequestData = ( username: createdBy?.username ?? '', }, comments: comments - .filter(c => { - const lastPush = c.pushedAt != null ? new Date(c.pushedAt) : null; - const lastUpdate = c.updatedAt != null ? new Date(c.updatedAt) : null; - if ( - lastPush === null || - (lastPush != null && lastUpdate != null && lastPush.getTime() < lastUpdate?.getTime()) - ) { - return true; - } - return false; - }) + .filter( + c => actualExternalService == null || actualExternalService.commentsToUpdate.includes(c.id) + ) .map(c => ({ commentId: c.id, comment: c.comment, diff --git a/x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx index 718eb95767f2e..f48d9a68ffaf0 100644 --- a/x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx @@ -20,6 +20,7 @@ import * as i18n from '../case_view/translations'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; import { CaseViewActions } from '../case_view/actions'; import { Case } from '../../../../containers/case/types'; +import { CaseService } from '../../../../containers/case/use_get_case_user_actions'; const MyDescriptionList = styled(EuiDescriptionList)` ${({ theme }) => css` @@ -35,6 +36,7 @@ interface CaseStatusProps { badgeColor: string; buttonLabel: string; caseData: Case; + currentExternalIncident: CaseService | null; disabled?: boolean; icon: string; isLoading: boolean; @@ -50,6 +52,7 @@ const CaseStatusComp: React.FC = ({ badgeColor, buttonLabel, caseData, + currentExternalIncident, disabled = false, icon, isLoading, @@ -100,7 +103,11 @@ const CaseStatusComp: React.FC = ({ /> - + diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx index 8b6ee76dd783d..24fbd59b3282b 100644 --- a/x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx @@ -9,8 +9,9 @@ import { mount } from 'enzyme'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; import { TestProviders } from '../../../../mock'; -import { basicCase } from '../../../../containers/case/mock'; +import { basicCase, basicPush } from '../../../../containers/case/mock'; import { CaseViewActions } from './actions'; +import * as i18n from './translations'; jest.mock('../../../../containers/case/use_delete_cases'); const useDeleteCasesMock = useDeleteCases as jest.Mock; @@ -34,7 +35,7 @@ describe('CaseView actions', () => { it('clicking trash toggles modal', () => { const wrapper = mount( - + ); @@ -54,7 +55,7 @@ describe('CaseView actions', () => { })); const wrapper = mount( - + ); @@ -64,4 +65,33 @@ describe('CaseView actions', () => { { id: basicCase.id, title: basicCase.title }, ]); }); + it('displays active incident link', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); + + wrapper + .find('button[data-test-subj="property-actions-ellipses"]') + .first() + .simulate('click'); + expect( + wrapper + .find('[data-test-subj="property-actions-popout"]') + .first() + .prop('aria-label') + ).toEqual(i18n.VIEW_INCIDENT(basicPush.externalTitle)); + }); }); diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx index 216180eb2cf0a..4acdaef6ca51f 100644 --- a/x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx @@ -13,13 +13,19 @@ import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { SiemPageName } from '../../../home/types'; import { PropertyActions } from '../property_actions'; import { Case } from '../../../../containers/case/types'; +import { CaseService } from '../../../../containers/case/use_get_case_user_actions'; interface CaseViewActions { caseData: Case; + currentExternalIncident: CaseService | null; disabled?: boolean; } -const CaseViewActionsComponent: React.FC = ({ caseData, disabled = false }) => { +const CaseViewActionsComponent: React.FC = ({ + caseData, + currentExternalIncident, + disabled = false, +}) => { // Delete case const { handleToggleModal, @@ -48,17 +54,17 @@ const CaseViewActionsComponent: React.FC = ({ caseData, disable label: i18n.DELETE_CASE, onClick: handleToggleModal, }, - ...(caseData.externalService != null && !isEmpty(caseData.externalService?.externalUrl) + ...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl) ? [ { iconType: 'popout', - label: i18n.VIEW_INCIDENT(caseData.externalService?.externalTitle ?? ''), - onClick: () => window.open(caseData.externalService?.externalUrl, '_blank'), + label: i18n.VIEW_INCIDENT(currentExternalIncident?.externalTitle ?? ''), + onClick: () => window.open(currentExternalIncident?.externalUrl, '_blank'), }, ] : []), ], - [disabled, handleToggleModal, caseData] + [disabled, handleToggleModal, currentExternalIncident] ); if (isDeleted) { diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx index 7ce9d7b8533e4..a6e6b19a071ce 100644 --- a/x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -70,6 +70,7 @@ describe('CaseView ', () => { const defaultUseGetCaseUserActions = { caseUserActions, + caseServices: {}, fetchCaseUserActions, firstIndexPushToService: -1, hasDataToPush: false, diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx index 14039dc2cbc30..fed8ec8edbe8b 100644 --- a/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -164,6 +164,15 @@ export const CaseComponent = React.memo( () => connectors.find(c => c.id === caseData.connectorId)?.name ?? 'none', [connectors, caseData.connectorId] ); + + const currentExternalIncident = useMemo( + () => + caseServices != null && caseServices[caseData.connectorId] != null + ? caseServices[caseData.connectorId] + : null, + [caseServices, caseData.connectorId] + ); + const { pushButton, pushCallouts } = usePushToService({ caseConnectorId: caseData.connectorId, caseConnectorName, @@ -254,6 +263,7 @@ export const CaseComponent = React.memo( title={caseData.title} > = ({ /> - - {connector.name} - + {connector.name} ), diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx index 1e4fd92058e8d..0613c40d1181d 100644 --- a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx @@ -32,7 +32,7 @@ export const getKibanaConfigError = () => ({ title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE, description: ( { ...basicPush, firstPushIndex: 0, lastPushIndex: 0, + commentsToUpdate: [], hasDataToPush: true, }, }; diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts index 2a36fcf8a6bc4..bdd6ae98a5d01 100644 --- a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts +++ b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts @@ -60,7 +60,7 @@ export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE = i18n.translate( export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate( 'xpack.siem.case.caseView.pushToServiceDisableByConfigTitle', { - defaultMessage: 'Enable ServiceNow in Kibana configuration file', + defaultMessage: 'Enable external service in Kibana configuration file', } ); diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx index 736974545a1df..b9a94f83fded1 100644 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx @@ -86,6 +86,7 @@ describe('UserActionTree ', () => { ...basicPush, firstPushIndex: 0, lastPushIndex: 0, + commentsToUpdate: [`${ourActions[ourActions.length - 1].commentId}`], hasDataToPush: true, }, }, @@ -111,6 +112,7 @@ describe('UserActionTree ', () => { ...basicPush, firstPushIndex: 0, lastPushIndex: 0, + commentsToUpdate: [], hasDataToPush: false, }, }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 67eec939701c3..0d050f7bf9842 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4386,7 +4386,6 @@ "xpack.apm.serviceMap.emptyBanner.title": "単一のサービスしかないようです。", "xpack.apm.serviceMap.focusMapButtonText": "焦点マップ", "xpack.apm.serviceMap.invalidLicenseMessage": "サービスマップを利用するには、Elastic Platinum ライセンスが必要です。これにより、APM データとともにアプリケーションスタック全てを可視化することができるようになります。", - "xpack.apm.serviceMap.numInstancesMetric": "{numInstances}インスタンス", "xpack.apm.serviceMap.serviceDetailsButtonText": "サービス詳細", "xpack.apm.serviceMap.subtypePopoverMetric": "サブタイプ", "xpack.apm.serviceMap.typePopoverMetric": "タイプ", @@ -6590,7 +6589,6 @@ "xpack.graph.sidebar.selectionsTitle": "選択項目", "xpack.graph.sidebar.styleVerticesTitle": "スタイルが選択された頂点", "xpack.graph.sidebar.topMenu.addLinksButtonTooltip": "既存の用語の間にリンクを追加します", - "xpack.graph.sidebar.topMenu.blacklistButtonTooltip": "選択項目がワークスペースに戻らないようブラックリストに追加します", "xpack.graph.sidebar.topMenu.customStyleButtonTooltip": "選択された頂点のカスタムスタイル", "xpack.graph.sidebar.topMenu.drillDownButtonTooltip": "ドリルダウン", "xpack.graph.sidebar.topMenu.expandSelectionButtonTooltip": "選択項目を拡張", @@ -13282,8 +13280,6 @@ "xpack.siem.containers.anomalies.stackByJobId": "ジョブ", "xpack.siem.containers.anomalies.title": "異常", "xpack.siem.containers.case.errorTitle": "データの取得中にエラーが発生", - "xpack.siem.containers.case.pushToExterService": "ServiceNow への送信が正常に完了しました", - "xpack.siem.containers.case.tagFetchFailDescription": "タグを取得できませんでした", "xpack.siem.containers.detectionEngine.addRuleFailDescription": "ルールを追加できませんでした", "xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription": "Elasticから事前にパッケージ化されているルールをインストールすることができませんでした", "xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "Elasticから事前にパッケージ化されているルールをインストールしました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 486bb747a15e0..21113d55b4641 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4387,7 +4387,6 @@ "xpack.apm.serviceMap.emptyBanner.title": "似乎仅有一个服务。", "xpack.apm.serviceMap.focusMapButtonText": "聚焦地图", "xpack.apm.serviceMap.invalidLicenseMessage": "要访问服务地图,必须订阅 Elastic 白金级许可证。使用该许可证,您将能够可视化整个应用程序堆栈以及 APM 数据。", - "xpack.apm.serviceMap.numInstancesMetric": "{numInstances} 个实例", "xpack.apm.serviceMap.serviceDetailsButtonText": "服务详情", "xpack.apm.serviceMap.subtypePopoverMetric": "子类型", "xpack.apm.serviceMap.typePopoverMetric": "类型", @@ -6595,7 +6594,6 @@ "xpack.graph.sidebar.selectionsTitle": "选择的内容", "xpack.graph.sidebar.styleVerticesTitle": "样式选择的顶点", "xpack.graph.sidebar.topMenu.addLinksButtonTooltip": "在现有字词之间添加链接", - "xpack.graph.sidebar.topMenu.blacklistButtonTooltip": "返回工作空间时选择的黑名单", "xpack.graph.sidebar.topMenu.customStyleButtonTooltip": "定制样式选择的顶点", "xpack.graph.sidebar.topMenu.drillDownButtonTooltip": "向下钻取", "xpack.graph.sidebar.topMenu.expandSelectionButtonTooltip": "展开选择内容", @@ -13289,8 +13287,6 @@ "xpack.siem.containers.anomalies.stackByJobId": "作业", "xpack.siem.containers.anomalies.title": "异常", "xpack.siem.containers.case.errorTitle": "提取数据时出错", - "xpack.siem.containers.case.pushToExterService": "已成功发送到 ServiceNow", - "xpack.siem.containers.case.tagFetchFailDescription": "无法提取标记", "xpack.siem.containers.detectionEngine.addRuleFailDescription": "无法添加规则", "xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription": "无法安装 elastic 的预打包规则", "xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "已安装 elastic 的预打包规则", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx index 567e96e05881d..04dc7b484ed48 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx @@ -11,7 +11,6 @@ import { registerBuiltInActionTypes } from './index'; import { ActionTypeModel, ActionParamsProps } from '../../../types'; import { IndexActionParams, EsIndexActionConnector } from './types'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; jest.mock('../../../common/index_controls', () => ({ firstFieldOption: jest.fn(), getFields: jest.fn(), @@ -165,25 +164,13 @@ describe('IndexActionConnectorFields renders', () => { }, } as EsIndexActionConnector; const wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - docLinks: deps!.docLinks, - }} - > - {}} - editActionSecrets={() => {}} - /> - + {}} + editActionSecrets={() => {}} + http={deps!.http} + /> ); await act(async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 028638a403893..861d6ad7284c2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -33,7 +33,6 @@ import { getIndexPatterns, } from '../../../common/index_controls'; import { AddMessageVariables } from '../add_message_variables'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; export function getActionType(): ActionTypeModel { return { @@ -79,8 +78,7 @@ export function getActionType(): ActionTypeModel { const IndexActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, errors }) => { - const { http } = useActionsConnectorsContext(); +>> = ({ action, editActionConfig, errors, http }) => { const { index, refresh, executionTimeField } = action.config; const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState( executionTimeField != null diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx index ae894346be59c..f628457dc5162 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx @@ -6,7 +6,6 @@ import React, { FunctionComponent } from 'react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { act } from 'react-dom/test-utils'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; import { TypeRegistry } from '../../type_registry'; import { registerBuiltInActionTypes } from './index'; import { ActionTypeModel, ActionParamsProps } from '../../../types'; @@ -16,7 +15,6 @@ import { SeverityActionOptions, PagerDutyActionConnector, } from './types'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; const ACTION_TYPE_ID = '.pagerduty'; let actionTypeModel: ActionTypeModel; @@ -29,24 +27,7 @@ beforeAll(async () => { if (getResult !== null) { actionTypeModel = getResult; } - const mocks = coreMock.createSetup(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); deps = { - toastNotifications: mocks.notifications.toasts, - http: mocks.http, - capabilities: { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }, - actionTypeRegistry: actionTypeRegistry as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; }); @@ -148,25 +129,13 @@ describe('PagerDutyActionConnectorFields renders', () => { }, } as PagerDutyActionConnector; const wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - docLinks: deps!.docLinks, - }} - > - {}} - editActionSecrets={() => {}} - /> - + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> ); await act(async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx index 6f30cd41590ed..5ad1f2fffecce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx @@ -25,7 +25,6 @@ import { PagerDutyActionParams, PagerDutyActionConnector } from './types'; import pagerDutySvg from './pagerduty.svg'; import { AddMessageVariables } from '../add_message_variables'; import { hasMustacheTokens } from '../../lib/has_mustache_tokens'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; export function getActionType(): ActionTypeModel { return { @@ -105,8 +104,7 @@ export function getActionType(): ActionTypeModel { const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets }) => { - const { docLinks } = useActionsConnectorsContext(); +>> = ({ errors, action, editActionConfig, editActionSecrets, docLinks }) => { const { apiUrl } = action.config; const { routingKey } = action.secrets; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx index 0c9204ae5e176..a2865b27bc06c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx @@ -6,12 +6,10 @@ import React, { FunctionComponent } from 'react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { act } from 'react-dom/test-utils'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; import { TypeRegistry } from '../../type_registry'; import { registerBuiltInActionTypes } from './index'; import { ActionTypeModel, ActionParamsProps } from '../../../types'; import { SlackActionParams, SlackActionConnector } from './types'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; const ACTION_TYPE_ID = '.slack'; let actionTypeModel: ActionTypeModel; @@ -25,24 +23,7 @@ beforeAll(async () => { if (getResult !== null) { actionTypeModel = getResult; } - const mocks = coreMock.createSetup(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); deps = { - toastNotifications: mocks.notifications.toasts, - http: mocks.http, - capabilities: { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }, - actionTypeRegistry: actionTypeRegistry as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; }); @@ -119,25 +100,13 @@ describe('SlackActionFields renders', () => { config: {}, } as SlackActionConnector; const wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - docLinks: deps!.docLinks, - }} - > - {}} - editActionSecrets={() => {}} - /> - + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> ); await act(async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx index 1cdde6dd77975..03f7a2f492d54 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx @@ -15,7 +15,6 @@ import { } from '../../../types'; import { SlackActionParams, SlackActionConnector } from './types'; import { AddMessageVariables } from '../add_message_variables'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; export function getActionType(): ActionTypeModel { return { @@ -76,8 +75,7 @@ export function getActionType(): ActionTypeModel { const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors }) => { - const { docLinks } = useActionsConnectorsContext(); +>> = ({ action, editActionSecrets, errors, docLinks }) => { const { webhookUrl } = action.secrets; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx index 09547f5c8ea66..95620a5be8474 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx @@ -49,7 +49,7 @@ export const AlertsContextProvider = ({ export const useAlertsContext = () => { const ctx = useContext(AlertsContext); if (!ctx) { - throw new Error('ActionsConnectorsContext has not been set.'); + throw new Error('AlertsContext has not been set.'); } return ctx; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index 3b78096c4c644..17a1d929a0def 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -9,29 +9,14 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, ActionConnector } from '../../../types'; import { ActionConnectorForm } from './action_connector_form'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('action_connector_form', () => { let deps: any; beforeAll(async () => { const mocks = coreMock.createSetup(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); deps = { - toastNotifications: mocks.notifications.toasts, http: mocks.http, - capabilities: { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }, actionTypeRegistry: actionTypeRegistry as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; @@ -63,25 +48,15 @@ describe('action_connector_form', () => { let wrapper; if (deps) { wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - docLinks: deps!.docLinks, - }} - > - {}} - errors={{ name: [] }} - /> - + {}} + errors={{ name: [] }} + http={deps!.http} + actionTypeRegistry={deps!.actionTypeRegistry} + docLinks={deps!.docLinks} + /> ); } const connectorNameField = wrapper?.find('[data-test-subj="nameInput"]'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index 564b38bd0516a..6bb8a8f4e4c10 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -15,9 +15,10 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { HttpSetup, DocLinksStart } from 'kibana/public'; import { ReducerAction } from './connector_reducer'; -import { ActionConnector, IErrorObject } from '../../../types'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; +import { ActionConnector, IErrorObject, ActionTypeModel } from '../../../types'; +import { TypeRegistry } from '../../type_registry'; export function validateBaseProperties(actionObject: ActionConnector) { const validationResult = { errors: {} }; @@ -46,6 +47,9 @@ interface ActionConnectorProps { body: { message: string; error: string }; }; errors: IErrorObject; + http: HttpSetup; + actionTypeRegistry: TypeRegistry; + docLinks: DocLinksStart; } export const ActionConnectorForm = ({ @@ -54,8 +58,10 @@ export const ActionConnectorForm = ({ actionTypeName, serverError, errors, + http, + actionTypeRegistry, + docLinks, }: ActionConnectorProps) => { - const { actionTypeRegistry, docLinks } = useActionsConnectorsContext(); const setActionProperty = (key: string, value: any) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }; @@ -150,6 +156,8 @@ export const ActionConnectorForm = ({ errors={errors} editActionConfig={setActionConfigProperty} editActionSecrets={setActionSecretsProperty} + http={http} + docLinks={docLinks} /> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 80294e8b73dc8..c9844f4e10864 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -52,6 +52,7 @@ export const ConnectorAddFlyout = ({ capabilities, actionTypeRegistry, reloadConnectors, + docLinks, } = useActionsConnectorsContext(); const [actionType, setActionType] = useState(undefined); const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); @@ -114,6 +115,9 @@ export const ConnectorAddFlyout = ({ connector={connector} dispatch={dispatch} errors={errors} + actionTypeRegistry={actionTypeRegistry} + http={http} + docLinks={docLinks} /> ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index a31336f38bdcd..8312f2b151082 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -25,7 +25,6 @@ import { createActionConnector } from '../../lib/action_connector_api'; import { TypeRegistry } from '../../type_registry'; import './connector_add_modal.scss'; import { PLUGIN } from '../../constants/plugin'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; import { hasSaveActionsCapability } from '../../lib/capabilities'; interface ConnectorAddModalProps { @@ -156,23 +155,16 @@ export const ConnectorAddModal = ({ - - - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index b86524efe19ea..4a0effcbd6825 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -182,6 +182,9 @@ export const ConnectorEditFlyout = ({ errors={errors} actionTypeName={connector.actionType} dispatch={dispatch} + actionTypeRegistry={actionTypeRegistry} + http={http} + docLinks={docLinks} /> ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 47cb7067296ce..6f33bcb8b226d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'kibana/public'; +import { HttpSetup, DocLinksStart } from 'kibana/public'; import { ActionGroup } from '../../alerting/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; @@ -27,6 +27,7 @@ export interface ActionConnectorFieldsProps { editActionConfig: (property: string, value: any) => void; editActionSecrets: (property: string, value: any) => void; errors: { [key: string]: string[] }; + docLinks: DocLinksStart; http?: HttpSetup; } diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts index 90aa692f89a42..b3c39e5180adf 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; export const CheckMonitorType = t.intersection([ t.partial({ name: t.string, - ip: t.union([t.array(t.string), t.string]), + ip: t.union([t.array(t.union([t.string, t.null])), t.string, t.null]), }), t.type({ status: t.string, diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 9442beda3501d..0412192808700 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -199,7 +199,11 @@ export default function({ getService }) { 'ilm', // data enricher 'isRollupIndex', // data enricher ]; - expect(Object.keys(body[0])).to.eql(expectedKeys); + // We need to sort the keys before comparing then, because race conditions + // can cause enrichers to register in non-deterministic order. + const sortedExpectedKeys = expectedKeys.sort(); + const sortedReceivedKeys = Object.keys(body[0]).sort(); + expect(sortedReceivedKeys).to.eql(sortedExpectedKeys); }); }); @@ -225,7 +229,11 @@ export default function({ getService }) { 'ilm', // data enricher 'isRollupIndex', // data enricher ]; - expect(Object.keys(body[0])).to.eql(expectedKeys); + // We need to sort the keys before comparing then, because race conditions + // can cause enrichers to register in non-deterministic order. + const sortedExpectedKeys = expectedKeys.sort(); + const sortedReceivedKeys = Object.keys(body[0]).sort(); + expect(sortedReceivedKeys).to.eql(sortedExpectedKeys); expect(body.length > 1).to.be(true); // to contrast it with the next test }); }); diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts index 046b8ec44b9fa..5ed6064314af8 100644 --- a/x-pack/test/functional/apps/index_management/home_page.ts +++ b/x-pack/test/functional/apps/index_management/home_page.ts @@ -34,7 +34,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Index templates', () => { it('renders the index templates tab', async () => { // Navigate to the index templates tab - pageObjects.indexManagement.changeTabs('templatesTab'); + await pageObjects.indexManagement.changeTabs('templatesTab'); await pageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 857cbe15463b9..53e800bae3f6d 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -15,7 +15,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { describe('lens app', () => { before(async () => { log.debug('Starting lens before method'); - browser.setWindowSize(1280, 800); + await browser.setWindowSize(1280, 800); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('lens/basic'); }); diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index 93f225989592e..d87d7d654f5c4 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -13,7 +13,8 @@ export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('jobs cloning supported by UI form', function() { + // TODO add fix for https://github.com/elastic/elasticsearch/pull/56118 + describe.skip('jobs cloning supported by UI form', function() { const testDataList: Array<{ suiteTitle: string; archive: string; diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index 05967e0f3acaf..59e9dda7b184f 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -25,7 +25,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can navigate to cert page', async () => { - await uptimeService.navigation.refreshApp(); await uptimeService.cert.hasViewCertButton(); await uptimeService.navigation.goToCertificates(); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index f6b80b1b9fc67..4c78758de448c 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -146,9 +146,6 @@ export default async function({ readConfigFile }) { uptime: { pathname: '/app/uptime', }, - apm: { - pathname: '/app/apm', - }, ml: { pathname: '/app/ml', }, diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts index 1ae23b24156d0..453b283ab969d 100644 --- a/x-pack/test/functional/page_objects/index_management_page.ts +++ b/x-pack/test/functional/page_objects/index_management_page.ts @@ -57,7 +57,7 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext) }); }, async changeTabs(tab: 'indicesTab' | 'templatesTab') { - return await testSubjects.click(tab); + await testSubjects.click(tab); }, }; } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index c4dcf63941cd5..7425ed25728c2 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -150,7 +150,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont } await testSubjects.click('confirmSaveSavedObjectButton'); - retry.waitForWithTimeout('Save modal to disappear', 1000, () => + await retry.waitForWithTimeout('Save modal to disappear', 1000, () => testSubjects .missingOrFail('confirmSaveSavedObjectButton') .then(() => true) diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 08895de815b39..ae26a831d4172 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -394,9 +394,9 @@ export function SecurityPageProvider({ getService, getPageObjects }) { }); } }) //clicking save button - .then(function() { + .then(async () => { log.debug('click save button'); - testSubjects.click('roleFormSaveButton'); + await testSubjects.click('roleFormSaveButton'); }) .then(function() { return PageObjects.common.sleep(5000); diff --git a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts index b9a400b155679..70d8622e620ef 100644 --- a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts +++ b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts @@ -13,7 +13,7 @@ export function LogEntryCategoriesPageProvider({ getPageObjects, getService }: F return { async navigateTo() { - pageObjects.infraLogs.navigateToTab('log-categories'); + await pageObjects.infraLogs.navigateToTab('log-categories'); }, async getSetupScreen(): Promise { diff --git a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts index 96c69e85aa0a4..ffaa6ce08a1dc 100644 --- a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts +++ b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts @@ -13,7 +13,7 @@ export function LogEntryRatePageProvider({ getPageObjects, getService }: FtrProv return { async navigateTo() { - pageObjects.infraLogs.navigateToTab('log-rate'); + await pageObjects.infraLogs.navigateToTab('log-rate'); }, async getSetupScreen(): Promise { diff --git a/x-pack/test/functional/services/logs_ui/log_stream.ts b/x-pack/test/functional/services/logs_ui/log_stream.ts index 75486534cf5cc..5fa950a86e696 100644 --- a/x-pack/test/functional/services/logs_ui/log_stream.ts +++ b/x-pack/test/functional/services/logs_ui/log_stream.ts @@ -15,7 +15,7 @@ export function LogStreamPageProvider({ getPageObjects, getService }: FtrProvide return { async navigateTo(params?: TabsParams['stream']) { - pageObjects.infraLogs.navigateToTab('stream', params); + await pageObjects.infraLogs.navigateToTab('stream', params); }, async getColumnHeaderLabels(): Promise { diff --git a/x-pack/test/functional/services/uptime/alerts.ts b/x-pack/test/functional/services/uptime/alerts.ts index dc10fcccaa6ce..c4f75b843d781 100644 --- a/x-pack/test/functional/services/uptime/alerts.ts +++ b/x-pack/test/functional/services/uptime/alerts.ts @@ -11,10 +11,14 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { const browser = getService('browser'); return { - async openFlyout() { + async openFlyout(alertType: 'monitorStatus' | 'tls') { await testSubjects.click('xpack.uptime.alertsPopover.toggleButton', 5000); await testSubjects.click('xpack.uptime.openAlertContextPanel', 5000); - await testSubjects.click('xpack.uptime.toggleAlertFlyout', 5000); + if (alertType === 'monitorStatus') { + await testSubjects.click('xpack.uptime.toggleAlertFlyout', 5000); + } else if (alertType === 'tls') { + await testSubjects.click('xpack.uptime.toggleTlsAlertFlyout'); + } }, async openMonitorStatusAlertType(alertType: string) { return testSubjects.click(`xpack.uptime.alerts.${alertType}-SelectOption`, 5000); diff --git a/x-pack/test/functional/services/uptime/monitor.ts b/x-pack/test/functional/services/uptime/monitor.ts index a3e3d953e2eb7..b6689737e8618 100644 --- a/x-pack/test/functional/services/uptime/monitor.ts +++ b/x-pack/test/functional/services/uptime/monitor.ts @@ -38,8 +38,9 @@ export function UptimeMonitorProvider({ getService }: FtrProviderContext) { async checkForPingListTimestamps(timestamps: string[]): Promise { return retry.tryForTime(10000, async () => { await Promise.all( - timestamps.map(timestamp => - testSubjects.existOrFail(`xpack.uptime.pingList.ping-${timestamp}`) + timestamps.map( + async timestamp => + await testSubjects.existOrFail(`xpack.uptime.pingList.ping-${timestamp}`) ) ); }); diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index 13d3cc62183bd..37cc71d6865b0 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -65,8 +65,8 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv }, goToCertificates: async () => { - await testSubjects.click('uptimeCertificatesLink'); - return retry.tryForTime(30 * 1000, async () => { + await testSubjects.click('uptimeCertificatesLink', 10000); + return retry.tryForTime(60 * 1000, async () => { await testSubjects.existOrFail('uptimeCertificatesPage'); }); }, diff --git a/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts index c54eafdd8b787..28a7cbd2e3c30 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts @@ -5,45 +5,86 @@ */ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; export default function({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'endpoint']); const testSubjects = getService('testSubjects'); + const policyTestResources = getService('policyTestResources'); - // FIXME: Skipped until we can figure out how to load data for Ingest - describe.skip('Endpoint Policy List', function() { + describe('When on the Endpoint Policy List', function() { this.tags(['ciGroup7']); before(async () => { await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/policy'); - await pageObjects.endpoint.waitForTableToHaveData('policyTable'); }); it('loads the Policy List Page', async () => { await testSubjects.existOrFail('policyListPage'); }); it('displays page title', async () => { - const policyTitle = await testSubjects.getVisibleText('policyViewTitle'); + const policyTitle = await testSubjects.getVisibleText('pageViewHeaderLeftTitle'); expect(policyTitle).to.equal('Policies'); }); it('shows policy count total', async () => { const policyTotal = await testSubjects.getVisibleText('policyTotalCount'); - expect(policyTotal).to.equal('100 Policies'); - }); - it('includes policy list table', async () => { - await testSubjects.existOrFail('policyTable'); + expect(policyTotal).to.equal('0 Policies'); }); it('has correct table headers', async () => { const allHeaderCells = await pageObjects.endpoint.tableHeaderVisibleText('policyTable'); expect(allHeaderCells).to.eql([ 'Policy Name', - 'Total', - 'Pending', - 'Failed', - 'Created By', - 'Created', - 'Last Updated By', - 'Last Updated', + 'Revision', + 'Version', + 'Description', + 'Agent Configuration', ]); }); + it('should show empty table results message', async () => { + const [, [noItemsFoundMessage]] = await pageObjects.endpoint.getEndpointAppTableData( + 'policyTable' + ); + expect(noItemsFoundMessage).to.equal('No items found'); + }); + + describe('and policies exists', () => { + let policyInfo: PolicyTestResourceInfo; + + before(async () => { + // load/create a policy and then navigate back to the policy view so that the list is refreshed + policyInfo = await policyTestResources.createPolicy(); + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/policy'); + await pageObjects.endpoint.waitForTableToHaveData('policyTable'); + }); + after(async () => { + if (policyInfo) { + await policyInfo.cleanup(); + } + }); + + it('should show policy on the list', async () => { + const [, policyRow] = await pageObjects.endpoint.getEndpointAppTableData('policyTable'); + expect(policyRow).to.eql([ + 'Protect East Coast', + '1', + 'Elastic Endpoint v1.0.0', + 'Protect the worlds data - but in the East Coast', + policyInfo.agentConfig.id, + ]); + }); + it('should show policy name as link', async () => { + const policyNameLink = await testSubjects.find('policyNameLink'); + expect(await policyNameLink.getTagName()).to.equal('a'); + expect(await policyNameLink.getAttribute('href')).to.match( + new RegExp(`\/endpoint\/policy\/${policyInfo.datasource.id}$`) + ); + }); + it('should show agent configuration as link', async () => { + const agentConfigLink = await testSubjects.find('agentConfigLink'); + expect(await agentConfigLink.getTagName()).to.equal('a'); + expect(await agentConfigLink.getAttribute('href')).to.match( + new RegExp(`\/app\/ingestManager\#\/configs\/${policyInfo.datasource.config_id}$`) + ); + }); + }); }); } diff --git a/x-pack/test/functional_endpoint/config.ts b/x-pack/test/functional_endpoint/config.ts index d7f1cc21828d1..a371c548f3022 100644 --- a/x-pack/test/functional_endpoint/config.ts +++ b/x-pack/test/functional_endpoint/config.ts @@ -7,6 +7,7 @@ import { resolve } from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { pageObjects } from './page_objects'; +import { services } from './services'; export default async function({ readConfigFile }: FtrConfigProviderContext) { const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); @@ -18,6 +19,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { junit: { reportName: 'X-Pack Endpoint Functional Tests', }, + services, apps: { ...xpackFunctionalConfig.get('apps'), endpoint: { diff --git a/x-pack/test/functional_endpoint/ftr_provider_context.d.ts b/x-pack/test/functional_endpoint/ftr_provider_context.d.ts index 21ab5d5a4e554..bb257cdcbfe1b 100644 --- a/x-pack/test/functional_endpoint/ftr_provider_context.d.ts +++ b/x-pack/test/functional_endpoint/ftr_provider_context.d.ts @@ -7,6 +7,6 @@ import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; import { pageObjects } from './page_objects'; -import { services } from '../functional/services'; +import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/functional_endpoint/services/endpoint_policy.ts b/x-pack/test/functional_endpoint/services/endpoint_policy.ts new file mode 100644 index 0000000000000..e8e2d9957aa38 --- /dev/null +++ b/x-pack/test/functional_endpoint/services/endpoint_policy.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; +import { + CreateAgentConfigResponse, + CreateDatasourceResponse, +} from '../../../plugins/ingest_manager/common'; +import { Immutable } from '../../../plugins/endpoint/common/types'; +import { factory as policyConfigFactory } from '../../../plugins/endpoint/common/models/policy_config'; + +const INGEST_API_ROOT = '/api/ingest_manager'; +const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; +const INGEST_API_AGENT_CONFIGS_DELETE = `${INGEST_API_AGENT_CONFIGS}/delete`; +const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; +const INGEST_API_DATASOURCES_DELETE = `${INGEST_API_DATASOURCES}/delete`; + +/** + * Holds information about the test resources created to support an Endpoint Policy + */ +export interface PolicyTestResourceInfo { + /** The Ingest agent configuration created */ + agentConfig: Immutable; + /** The Ingest datasource created and added to agent configuration. + * This is where Endpoint Policy is stored. + */ + datasource: Immutable; + /** will clean up (delete) the objects created (agent config + datasource) */ + cleanup: () => Promise; +} + +export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + return { + /** + * Creates an Ingest Agent Configuration and adds to it the Endpoint Datasource that + * stores the Policy configuration data + */ + async createPolicy(): Promise { + // FIXME: Refactor after https://github.com/elastic/kibana/issues/64822 is fixed. `isInitialized` setup below should be deleted + // Due to an issue in Ingest API, we first create the Fleet user. This avoids the Agent Config api throwing a 500 + const isFleetSetupResponse = await supertest + .get('/api/ingest_manager/fleet/setup') + .set('kbn-xsrf', 'xxx') + .expect(200); + if (!isFleetSetupResponse.body.isInitialized) { + await supertest + .post('/api/ingest_manager/fleet/setup') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } + + // create agent config + const { + body: { item: agentConfig }, + }: { body: CreateAgentConfigResponse } = await supertest + .post(INGEST_API_AGENT_CONFIGS) + .set('kbn-xsrf', 'xxx') + .send({ name: 'East Coast', description: 'East Coast call center', namespace: '' }) + .expect(200); + + // create datasource and associated it to agent config + const { + body: { item: datasource }, + }: { body: CreateDatasourceResponse } = await supertest + .post(INGEST_API_DATASOURCES) + .set('kbn-xsrf', 'xxx') + .send({ + name: 'Protect East Coast', + description: 'Protect the worlds data - but in the East Coast', + config_id: agentConfig.id, + enabled: true, + output_id: '', + inputs: [ + // TODO: should we retrieve the latest Endpoint Package and build the input (policy) from that? + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: policyConfigFactory(), + }, + }, + }, + ], + namespace: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '1.0.0', + }, + }) + .expect(200); + + return { + agentConfig, + datasource, + async cleanup() { + // Delete Datasource + await supertest + .post(INGEST_API_DATASOURCES_DELETE) + .set('kbn-xsrf', 'xxx') + .send({ datasourceIds: [datasource.id] }) + .expect(200); + + // Delete Agent config + await supertest + .post(INGEST_API_AGENT_CONFIGS_DELETE) + .set('kbn-xsrf', 'xxx') + .send({ agentConfigId: agentConfig.id }) + .expect(200); + }, + }; + }, + }; +} diff --git a/x-pack/test/functional_endpoint/services/index.ts b/x-pack/test/functional_endpoint/services/index.ts new file mode 100644 index 0000000000000..0247d9b00968a --- /dev/null +++ b/x-pack/test/functional_endpoint/services/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { services as xPackFunctionalServices } from '../../functional/services'; +import { EndpointPolicyTestResourcesProvider } from './endpoint_policy'; + +export const services = { + ...xPackFunctionalServices, + policyTestResources: EndpointPolicyTestResourcesProvider, +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 2edab1b164a1b..bd793883eed90 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -29,7 +29,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Connectors tab', () => { it('renders the connectors tab', async () => { // Navigate to the connectors tab - pageObjects.triggersActionsUI.changeTabs('connectorsTab'); + await pageObjects.triggersActionsUI.changeTabs('connectorsTab'); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -45,7 +45,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Alerts tab', () => { it('renders the alerts tab', async () => { // Navigate to the alerts tab - pageObjects.triggersActionsUI.changeTabs('alertsTab'); + await pageObjects.triggersActionsUI.changeTabs('alertsTab'); await pageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index fb4f34d65f9b0..f1883733b02c9 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -8,123 +8,208 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - describe('overview page alert flyout controls', function() { - const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; - const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; + describe('uptime alerts', () => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); - let alerts: any; - before(async () => { - alerts = getService('uptime').alerts; - }); + describe('overview page alert flyout controls', function() { + const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; + const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; + let alerts: any; - it('can open alert flyout', async () => { - await pageObjects.uptime.goToUptimeOverviewAndLoadData(DEFAULT_DATE_START, DEFAULT_DATE_END); - await alerts.openFlyout(); - }); + before(async () => { + alerts = getService('uptime').alerts; + }); - it('can set alert name', async () => { - await alerts.setAlertName('uptime-test'); - }); + it('can open alert flyout', async () => { + await pageObjects.uptime.goToUptimeOverviewAndLoadData( + DEFAULT_DATE_START, + DEFAULT_DATE_END + ); + await alerts.openFlyout('monitorStatus'); + }); - it('can set alert tags', async () => { - await alerts.setAlertTags(['uptime', 'another']); - }); + it('can set alert name', async () => { + await alerts.setAlertName('uptime-test'); + }); - it('can set alert interval', async () => { - await alerts.setAlertInterval('11'); - }); + it('can set alert tags', async () => { + await alerts.setAlertTags(['uptime', 'another']); + }); - it('can set alert throttle interval', async () => { - await alerts.setAlertThrottleInterval('30'); - }); + it('can set alert interval', async () => { + await alerts.setAlertInterval('11'); + }); - it('can set alert status number of time', async () => { - await alerts.setAlertStatusNumTimes('3'); - }); - it('can set alert time range', async () => { - await alerts.setAlertTimerangeSelection('1'); - }); - it('can set monitor hours', async () => { - await alerts.setMonitorStatusSelectableToHours(); - }); + it('can set alert throttle interval', async () => { + await alerts.setAlertThrottleInterval('30'); + }); - it('can set kuery bar filters', async () => { - await pageObjects.uptime.setAlertKueryBarText('monitor.id: "0001-up"'); - }); + it('can set alert status number of time', async () => { + await alerts.setAlertStatusNumTimes('3'); + }); - it('can select location filter', async () => { - await alerts.clickAddFilterLocation(); - await alerts.clickLocationExpression('mpls'); - }); + it('can set alert time range', async () => { + await alerts.setAlertTimerangeSelection('1'); + }); - it('can select port filter', async () => { - await alerts.clickAddFilterPort(); - await alerts.clickPortExpression('5678'); - }); + it('can set monitor hours', async () => { + await alerts.setMonitorStatusSelectableToHours(); + }); - it('can select type/scheme filter', async () => { - await alerts.clickAddFilterType(); - await alerts.clickTypeExpression('http'); - }); + it('can set kuery bar filters', async () => { + await pageObjects.uptime.setAlertKueryBarText('monitor.id: "0001-up"'); + }); + + it('can select location filter', async () => { + await alerts.clickAddFilterLocation(); + await alerts.clickLocationExpression('mpls'); + }); + + it('can select port filter', async () => { + await alerts.clickAddFilterPort(); + await alerts.clickPortExpression('5678'); + }); + + it('can select type/scheme filter', async () => { + await alerts.clickAddFilterType(); + await alerts.clickTypeExpression('http'); + }); + + it('can save alert', async () => { + await alerts.clickSaveAlertButton(); + }); - it('can save alert', async () => { - await alerts.clickSaveAlertButton(); + it('posts an alert, verifies its presence, and deletes the alert', async () => { + // The creation of the alert could take some time, so the first few times we query after + // the previous line resolves, the API may not be done creating the alert yet, so we + // put the fetch code in a retry block with a timeout. + let alert: any; + await retry.tryForTime(15000, async () => { + const apiResponse = await supertest.get('/api/alert/_find?search=uptime-test'); + const alertsFromThisTest = apiResponse.body.data.filter( + ({ name }: { name: string }) => name === 'uptime-test' + ); + expect(alertsFromThisTest).to.have.length(1); + alert = alertsFromThisTest[0]; + }); + + // Ensure the parameters and other stateful data + // on the alert match up with the values we provided + // for our test helper to input into the flyout. + const { + actions, + alertTypeId, + consumer, + id, + params: { numTimes, timerange, locations, filters }, + schedule: { interval }, + tags, + } = alert; + + try { + // we're not testing the flyout's ability to associate alerts with action connectors + expect(actions).to.eql([]); + + expect(alertTypeId).to.eql('xpack.uptime.alerts.monitorStatus'); + expect(consumer).to.eql('uptime'); + expect(interval).to.eql('11m'); + expect(tags).to.eql(['uptime', 'another']); + expect(numTimes).to.be(3); + expect(timerange.from).to.be('now-1h'); + expect(timerange.to).to.be('now'); + expect(locations).to.eql(['mpls']); + expect(filters).to.eql( + '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + + '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + + '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + + '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' + ); + } finally { + await supertest + .delete(`/api/alert/${id}`) + .set('kbn-xsrf', 'true') + .expect(204); + } + }); }); - it('posts an alert, verifies its presence, and deletes the alert', async () => { - // The creation of the alert could take some time, so the first few times we query after - // the previous line resolves, the API may not be done creating the alert yet, so we - // put the fetch code in a retry block with a timeout. - let alert: any; - await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get('/api/alert/_find?search=uptime-test'); - const alertsFromThisTest = apiResponse.body.data.filter( - ({ name }: { name: string }) => name === 'uptime-test' - ); - expect(alertsFromThisTest).to.have.length(1); - alert = alertsFromThisTest[0]; - }); - - // Ensure the parameters and other stateful data - // on the alert match up with the values we provided - // for our test helper to input into the flyout. - const { - actions, - alertTypeId, - consumer, - id, - params: { numTimes, timerange, locations, filters }, - schedule: { interval }, - tags, - } = alert; - - try { - // we're not testing the flyout's ability to associate alerts with action connectors - expect(actions).to.eql([]); - - expect(alertTypeId).to.eql('xpack.uptime.alerts.monitorStatus'); - expect(consumer).to.eql('uptime'); - expect(interval).to.eql('11m'); - expect(tags).to.eql(['uptime', 'another']); - expect(numTimes).to.be(3); - expect(timerange.from).to.be('now-1h'); - expect(timerange.to).to.be('now'); - expect(locations).to.eql(['mpls']); - expect(filters).to.eql( - '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + - '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' + describe('tls alert', function() { + const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; + const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; + let alerts: any; + const alertId = 'uptime-tls'; + + before(async () => { + alerts = getService('uptime').alerts; + }); + + it('can open alert flyout', async () => { + await pageObjects.uptime.goToUptimeOverviewAndLoadData( + DEFAULT_DATE_START, + DEFAULT_DATE_END ); - } finally { - await supertest - .delete(`/api/alert/${id}`) - .set('kbn-xsrf', 'true') - .expect(204); - } + await alerts.openFlyout('tls'); + }); + + it('can set alert name', async () => { + await alerts.setAlertName(alertId); + }); + + it('can set alert tags', async () => { + await alerts.setAlertTags(['uptime', 'certs']); + }); + + it('can set alert interval', async () => { + await alerts.setAlertInterval('11'); + }); + + it('can set alert throttle interval', async () => { + await alerts.setAlertThrottleInterval('30'); + }); + + it('can save alert', async () => { + await alerts.clickSaveAlertButton(); + }); + + it('has created a valid alert with expected parameters', async () => { + let alert: any; + await retry.tryForTime(15000, async () => { + const apiResponse = await supertest.get(`/api/alert/_find?search=${alertId}`); + const alertsFromThisTest = apiResponse.body.data.filter( + ({ name }: { name: string }) => name === alertId + ); + expect(alertsFromThisTest).to.have.length(1); + alert = alertsFromThisTest[0]; + }); + + // Ensure the parameters and other stateful data + // on the alert match up with the values we provided + // for our test helper to input into the flyout. + const { + actions, + alertTypeId, + consumer, + id, + params, + schedule: { interval }, + tags, + } = alert; + try { + expect(actions).to.eql([]); + expect(alertTypeId).to.eql('xpack.uptime.alerts.tls'); + expect(consumer).to.eql('uptime'); + expect(tags).to.eql(['uptime', 'certs']); + expect(params).to.eql({}); + expect(interval).to.eql('11m'); + } finally { + await supertest + .delete(`/api/alert/${id}`) + .set('kbn-xsrf', 'true') + .expect(204); + } + }); }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index ca7f064e20690..2cd094f9045c5 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -120,7 +120,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) await find.clickDisplayedByCssSelector(`[data-test-subj="alertsList"] [title="${name}"]`); }, async changeTabs(tab: 'alertsTab' | 'connectorsTab') { - return await testSubjects.click(tab); + await testSubjects.click(tab); }, async toggleSwitch(testSubject: string) { const switchBtn = await testSubjects.find(testSubject); diff --git a/x-pack/test/kerberos_api_integration/anonymous_access.config.ts b/x-pack/test/kerberos_api_integration/anonymous_access.config.ts index 90d47ec61a4dc..8b712afe6c4d6 100644 --- a/x-pack/test/kerberos_api_integration/anonymous_access.config.ts +++ b/x-pack/test/kerberos_api_integration/anonymous_access.config.ts @@ -21,7 +21,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...kerberosAPITestsConfig.get('esTestCluster.serverArgs'), 'xpack.security.authc.anonymous.username=anonymous_user', - 'xpack.security.authc.anonymous.roles=superuser', + 'xpack.security.authc.anonymous.roles=superuser_anonymous', ], }, }; diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index fbf9a977e8b1f..e81db7e2b02f3 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -100,9 +100,7 @@ export default function({ getService }: FtrProviderContext) { }); }); - // Preventing ES Snapshot to be promoted - // https://github.com/elastic/kibana/issues/65114 - describe.skip('finishing SPNEGO', () => { + describe('finishing SPNEGO', () => { it('should properly set cookie and authenticate user', async () => { const response = await supertest .get('/internal/security/me') @@ -120,13 +118,22 @@ export default function({ getService }: FtrProviderContext) { const sessionCookie = request.cookie(cookies[0])!; checkCookieIsSet(sessionCookie); + const isAnonymousAccessEnabled = (config.get( + 'esTestCluster.serverArgs' + ) as string[]).some(setting => setting.startsWith('xpack.security.authc.anonymous')); + + // `superuser_anonymous` role is derived from the enabled anonymous access. + const expectedUserRoles = isAnonymousAccessEnabled + ? ['kibana_admin', 'superuser_anonymous'] + : ['kibana_admin']; + await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200, { username: 'tester@TEST.ELASTIC.CO', - roles: ['kibana_admin'], + roles: expectedUserRoles, full_name: null, email: null, metadata: { diff --git a/x-pack/test/licensing_plugin/config.public.ts b/x-pack/test/licensing_plugin/config.public.ts index 42209aa49bcb4..adde6320119d1 100644 --- a/x-pack/test/licensing_plugin/config.public.ts +++ b/x-pack/test/licensing_plugin/config.public.ts @@ -14,9 +14,9 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...commonConfig.getAll(), testFiles: [require.resolve('./public')], kbnTestServer: { + ...commonConfig.get('kbnTestServer'), serverArgs: [ ...commonConfig.get('kbnTestServer.serverArgs'), - // Required to load new platform plugin provider via `--plugin-path` flag. '--env.name=development', `--plugin-path=${path.resolve( diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 0b127288e7958..0684a5e572f55 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -515,7 +515,9 @@ export default function({ getService }: FtrProviderContext) { describe('API access with expired access token.', () => { let sessionCookie: Cookie; - beforeEach(async () => { + beforeEach(async function() { + this.timeout(40000); + const captureURLResponse = await supertest .get('/abc/xyz/handshake?one=two three') .expect(302); @@ -539,6 +541,10 @@ export default function({ getService }: FtrProviderContext) { .expect(302); sessionCookie = request.cookie(samlAuthenticationResponse.headers['set-cookie'][0])!; + + // Access token expiration is set to 15s for API integration tests. + // Let's wait for 20s to make sure token expires. + await delay(20000); }); const expectNewSessionCookie = (cookie: Cookie) => { @@ -549,13 +555,7 @@ export default function({ getService }: FtrProviderContext) { expect(cookie.value).to.not.be(sessionCookie.value); }; - it('expired access token should be automatically refreshed', async function() { - this.timeout(40000); - - // Access token expiration is set to 15s for API integration tests. - // Let's wait for 20s to make sure token expires. - await delay(20000); - + it('expired access token should be automatically refreshed', async () => { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const firstResponse = await supertest @@ -600,6 +600,19 @@ export default function({ getService }: FtrProviderContext) { .set('Cookie', secondNewCookie.cookieString()) .expect(200); }); + + it('should refresh access token even if multiple concurrent requests try to refresh it', async () => { + // Send 5 concurrent requests with a cookie that contains an expired access token. + await Promise.all( + Array.from({ length: 5 }).map((value, index) => + supertest + .get(`/internal/security/me?a=${index}`) + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .expect(200) + ) + ); + }); }); describe('API access with missing access token document.', () => { @@ -629,9 +642,7 @@ export default function({ getService }: FtrProviderContext) { .expect(302); sessionCookie = request.cookie(samlAuthenticationResponse.headers['set-cookie'][0])!; - }); - it('should properly set cookie and start new SAML handshake', async function() { // Let's delete tokens from `.security` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index // after some period of time. @@ -643,7 +654,9 @@ export default function({ getService }: FtrProviderContext) { expect(esResponse) .to.have.property('deleted') .greaterThan(0); + }); + it('should properly set cookie and start new SAML handshake', async () => { const handshakeResponse = await supertest .get('/abc/xyz/handshake?one=two three') .set('Cookie', sessionCookie.cookieString()) @@ -662,6 +675,19 @@ export default function({ getService }: FtrProviderContext) { '/internal/security/saml/capture-url-fragment' ); }); + + it('should start new SAML handshake even if multiple concurrent requests try to refresh access token', async () => { + // Issue 5 concurrent requests with a cookie that contains access/refresh token pair without + // a corresponding document in Elasticsearch. + await Promise.all( + Array.from({ length: 5 }).map((value, index) => + supertest + .get(`/abc/xyz/handshake?one=two three&a=${index}`) + .set('Cookie', sessionCookie.cookieString()) + .expect(302) + ) + ); + }); }); describe('IdP initiated login with active session', () => { diff --git a/x-pack/test_utils/testbed/testbed.ts b/x-pack/test_utils/testbed/testbed.ts index 9bf07f953595c..b6ec0f8997e1c 100644 --- a/x-pack/test_utils/testbed/testbed.ts +++ b/x-pack/test_utils/testbed/testbed.ts @@ -5,6 +5,7 @@ */ import { ComponentType, ReactWrapper } from 'enzyme'; + import { findTestSubject } from '../find_test_subject'; import { reactRouterMock } from '../router_helpers'; import { @@ -138,33 +139,23 @@ export const registerTestBed = ( }); }; - const waitFor: TestBed['waitFor'] = async (testSubject: T, count = 1) => { + const waitForFn: TestBed['waitForFn'] = async (predicate, errMessage) => { const triggeredAt = Date.now(); - /** - * The way jest run tests in parallel + the not deterministic DOM update from React "hooks" - * add flakiness to the tests. This is especially true for component integration tests that - * make many update to the DOM. - * - * For this reason, when we _know_ that an element should be there after we updated some state, - * we will give it 30 seconds to appear in the DOM, checking every 100 ms for its presence. - */ const MAX_WAIT_TIME = 30000; - const WAIT_INTERVAL = 100; + const WAIT_INTERVAL = 50; const process = async (): Promise => { - const elemFound = exists(testSubject, count); + const isOK = await predicate(); - if (elemFound) { + if (isOK) { // Great! nothing else to do here. return; } const timeElapsed = Date.now() - triggeredAt; if (timeElapsed > MAX_WAIT_TIME) { - throw new Error( - `I waited patiently for the "${testSubject}" test subject to appear with no luck. It is nowhere to be found!` - ); + throw new Error(errMessage); } return new Promise(resolve => setTimeout(resolve, WAIT_INTERVAL)).then(() => { @@ -176,6 +167,13 @@ export const registerTestBed = ( return process(); }; + const waitFor: TestBed['waitFor'] = (testSubject: T, count = 1) => { + return waitForFn( + () => Promise.resolve(exists(testSubject, count)), + `I waited patiently for the "${testSubject}" test subject to appear with no luck. It is nowhere to be found!` + ); + }; + /** * ---------------------------------------------------------------- * Forms @@ -201,6 +199,18 @@ export const registerTestBed = ( return new Promise(resolve => setTimeout(resolve)); }; + const setSelectValue: TestBed['form']['setSelectValue'] = (select, value) => { + const formSelect = typeof select === 'string' ? find(select) : (select as ReactWrapper); + + if (!formSelect.length) { + throw new Error(`Select "${select}" was not found.`); + } + + formSelect.simulate('change', { target: { value } }); + + component.update(); + }; + const selectCheckBox: TestBed['form']['selectCheckBox'] = ( testSubject, isChecked = true @@ -293,11 +303,13 @@ export const registerTestBed = ( find, setProps, waitFor, + waitForFn, table: { getMetaData, }, form: { setInputValue, + setSelectValue, selectCheckBox, toggleEuiSwitch, setComboBoxValue, diff --git a/x-pack/test_utils/testbed/types.ts b/x-pack/test_utils/testbed/types.ts index f3704bb463ecf..4cc7deac60156 100644 --- a/x-pack/test_utils/testbed/types.ts +++ b/x-pack/test_utils/testbed/types.ts @@ -41,7 +41,7 @@ export interface TestBed { * * @example * - ```ts + ```typescript find('nameInput'); // or more specific, // "nameInput" is a child of "myForm" @@ -61,6 +61,7 @@ export interface TestBed { * and we need to wait for the data to be fetched (and bypass any "loading" state). */ waitFor: (testSubject: T, count?: number) => Promise; + waitForFn: (predicate: () => Promise, errMessage: string) => Promise; form: { /** * Set the value of a form text input. @@ -79,6 +80,28 @@ export interface TestBed { value: string, isAsync?: boolean ) => Promise | void; + /** + * Set the value of a or a mocked + * For the you need to mock it like this + * + ```typescript + jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + EuiSuperSelect: (props: any) => ( + { + props.onChange(e.target.value); + }} + /> + ), + })); + ``` + * @param select The form select. Can either be a data-test-subj or a reactWrapper (can be a nested path. e.g. "myForm.myInput"). + * @param value The value to set + */ + setSelectValue: (select: T | ReactWrapper, value: string) => void; /** * Select or unselect a form checkbox. *