diff --git a/.backportrc.json b/.backportrc.json index 0894909d2aac40..87bc3a1be583ba 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -25,7 +25,7 @@ ], "targetPRLabels": ["backport"], "branchLabelMapping": { - "^v7.8.0$": "7.x", + "^v7.9.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" } } diff --git a/.eslintrc.js b/.eslintrc.js index dde0ce010d4d44..f1e0b7d9353e8b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -112,7 +112,6 @@ module.exports = { files: ['x-pack/plugins/lens/**/*.{js,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', - 'react-hooks/rules-of-hooks': 'off', }, }, { @@ -238,6 +237,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 be3c043b6e52f7..034b9da799d3ec 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -34,7 +34,7 @@ "kibana_utils": "src/plugins/kibana_utils", "navigation": "src/plugins/navigation", "newsfeed": "src/plugins/newsfeed", - "regionMap": "src/legacy/core_plugins/region_map", + "regionMap": "src/plugins/region_map", "savedObjects": "src/plugins/saved_objects", "savedObjectsManagement": "src/plugins/saved_objects_management", "server": "src/legacy/server", @@ -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/apm/images/apm-service-map-anomaly.png b/docs/apm/images/apm-service-map-anomaly.png new file mode 100644 index 00000000000000..b661e8f09d1a12 Binary files /dev/null and b/docs/apm/images/apm-service-map-anomaly.png differ diff --git a/docs/apm/images/green-service.png b/docs/apm/images/green-service.png new file mode 100644 index 00000000000000..bbc00a3543b08f Binary files /dev/null and b/docs/apm/images/green-service.png differ diff --git a/docs/apm/images/red-service.png b/docs/apm/images/red-service.png new file mode 100644 index 00000000000000..be7a62b1774ab4 Binary files /dev/null and b/docs/apm/images/red-service.png differ diff --git a/docs/apm/images/service-maps.png b/docs/apm/images/service-maps.png index 454ae9bb720fbd..d4272e89999916 100644 Binary files a/docs/apm/images/service-maps.png and b/docs/apm/images/service-maps.png differ diff --git a/docs/apm/images/yellow-service.png b/docs/apm/images/yellow-service.png new file mode 100644 index 00000000000000..43afd6250be724 Binary files /dev/null and b/docs/apm/images/yellow-service.png differ diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc index 9d347fc4f1111c..03f7e13c985791 100644 --- a/docs/apm/machine-learning.asciidoc +++ b/docs/apm/machine-learning.asciidoc @@ -6,13 +6,20 @@ Integrate with machine learning ++++ -The Machine Learning integration will initiate a new job predefined to calculate anomaly scores on transaction response times. -The response time graph will show the expected bounds and add an annotation when the anomaly score is 75 or above. -Jobs can be created per transaction type, and based on the average response time. -Manage jobs in the *Machine Learning jobs management*. +The Machine Learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. +Jobs can be created per transaction type, and are based on the service's average response time. + +After a machine learning job is created, results are shown in two places: + +The transaction duration graph will show the expected bounds and add an annotation when the anomaly score is 75 or above. + +[role="screenshot"] +image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in the APM app] + +Service maps will display a color-coded anomaly indicator based on the detected anomaly score. [role="screenshot"] -image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in APM app in Kibana] +image::apm/images/apm-service-map-anomaly.png[Example view of anomaly scores on service maps in the APM app] [float] [[create-ml-integration]] @@ -20,8 +27,10 @@ image::apm/images/apm-ml-integration.png[Example view of anomaly scores on respo To enable machine learning anomaly detection, first choose a service to monitor. Then, select **Integrations** > **Enable ML anomaly detection** and click **Create job**. + That's it! After a few minutes, the job will begin calculating results; it might take additional time for results to appear on your graph. +Jobs can be managed in *Machine Learning jobs management*. APM specific anomaly detection wizards are also available for certain Agents. See the machine learning {ml-docs}/ootb-ml-jobs-apm.html[APM anomaly detection configurations] for more information. diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index be86b9d522ac5e..3a6a96fca9d097 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -9,7 +9,9 @@ Please use Chrome or Firefox if available. A service map is a real-time visual representation of the instrumented services in your application's architecture. It shows you how these services are connected, along with high-level metrics like average transaction duration, -requests per minute, and errors per minute, that allow you to quickly assess the status of your services. +requests per minute, and errors per minute. +If enabled, service maps also integrate with machine learning--for real time health indicators based on anomaly detection scores. +All of these features can help you to quickly and visually assess the status and health of your services. We currently surface two types of service maps: @@ -52,6 +54,26 @@ Additional filters are not currently available for service maps. [role="screenshot"] image::apm/images/service-maps-java.png[Example view of service maps with Java highlighted in the APM app in Kibana] +[float] +[[service-map-anomaly-detection]] +=== Anomaly detection with machine learning + +Machine learning jobs can be created to calculate anomaly scores on APM transaction durations within the selected service. +When these jobs are active, service maps will display a color-coded anomaly indicator based on the detected anomaly score: + +[horizontal] +image:apm/images/green-service.png[APM green service]:: Max anomaly score **<=25**. Service is healthy. +image:apm/images/yellow-service.png[APM yellow service]:: Max anomaly score **26-74**. Anomalous activity detected. Service may be degraded. +image:apm/images/red-service.png[APM red service]:: Max anomaly score **>=75**. Anomalous activity detected. Service is unhealthy. + +[role="screenshot"] +image::apm/images/apm-service-map-anomaly.png[Example view of anomaly scores on service maps in the APM app] + +If an anomaly has been detected, click *view anomalies* to view the anomaly detection metric viewier in the Machine learning app. +This time series analysis will display additional details on the severity and time of the detected anomalies. + +To learn how to create a machine learning job, see <>. + [float] [[service-maps-legend]] === Legend diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.error.md new file mode 100644 index 00000000000000..7536cd2b07ae6c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-core-server.savedobjectsmigrationlogger.md) > [error](./kibana-plugin-core-server.savedobjectsmigrationlogger.error.md) + +## SavedObjectsMigrationLogger.error property + +Signature: + +```typescript +error: (msg: string, meta: LogMeta) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.md index 066643516b213a..1b691ee8cb16dc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.md @@ -16,6 +16,7 @@ export interface SavedObjectsMigrationLogger | Property | Type | Description | | --- | --- | --- | | [debug](./kibana-plugin-core-server.savedobjectsmigrationlogger.debug.md) | (msg: string) => void | | +| [error](./kibana-plugin-core-server.savedobjectsmigrationlogger.error.md) | (msg: string, meta: LogMeta) => void | | | [info](./kibana-plugin-core-server.savedobjectsmigrationlogger.info.md) | (msg: string) => void | | | [warn](./kibana-plugin-core-server.savedobjectsmigrationlogger.warn.md) | (msg: string) => void | | | [warning](./kibana-plugin-core-server.savedobjectsmigrationlogger.warning.md) | (msg: string) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 21a155ba977c96..60cbfd30e667d7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -28,7 +28,6 @@ export declare class IndexPattern implements IIndexPattern | [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | any | | | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | -| [routes](./kibana-plugin-plugins-data-public.indexpattern.routes.md) | | {
edit: string;
addField: string;
indexedFields: string;
scriptedFields: string;
sourceFilters: string;
} | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | | [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) | | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.routes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.routes.md deleted file mode 100644 index 81e7abd4f9609e..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.routes.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [routes](./kibana-plugin-plugins-data-public.indexpattern.routes.md) - -## IndexPattern.routes property - -Signature: - -```typescript -get routes(): { - edit: string; - addField: string; - indexedFields: string; - scriptedFields: string; - sourceFilters: string; - }; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterns.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterns.md index fa97666a61b939..39c8b0a700c8a3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterns.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterns.md @@ -18,7 +18,6 @@ indexPatterns: { validate: typeof validateIndexPattern; getFromSavedObject: typeof getFromSavedObject; flattenHitWrapper: typeof flattenHitWrapper; - getRoutes: typeof getRoutes; formatHitProvider: typeof formatHitProvider; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md new file mode 100644 index 00000000000000..a4d6abcf86a940 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md) + +## IIndexPattern.getTimeField() method + +Signature: + +```typescript +getTimeField?(): IFieldType | undefined; +``` +Returns: + +`IFieldType | undefined` + 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 24b56a9b986216..a79244a24acf57 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 acf3f3b1f0be95..d46df21e6f6b04 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 9d26fad419e4f2..fd59e79d07279a 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 d6895bd4952b8c..7506d1cb8c65ea 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 b049b4ba082f66..a757d59e123600 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 9b47d82a746398..29cdf707b4912a 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 94336a20e1d6cc..c0e495a87ecd31 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 75ffb3ff69babc..859274e9b6613c 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 ca00240a4af618..dc187c97de3097 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 8794c389d72bcb..09878b3059ac87 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 794fc14005f2ff..81b4e210961f6e 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] +-- + 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 625b8f704b7c6d..c71412210c535f 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] +-- + 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 673b4f6263e18f..cd51ec2e3301e9 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] +-- + 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 8f888785626c9b..eadca229bc19c6 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] +-- + 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 c0965d65bfdbec..afa616ba77b3a7 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] +-- + 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 64bfa6a1d6364b..27609652288b5a 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] +-- + 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 f05afac34e5957..d05a727016455f 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 e4448ca5f3fcd3..2df2a031c66614 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 f3e8f42ff0f374..8e0eff0224363c 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 a0a25dc5f1bbc9..ccd3f07f07c943 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 db56c16c1c308f..883c2348ecc8ad 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 356732dfb97775..5d66123ac733ea 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 fc40da74365474..055b643ec34586 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 ea3a3849c8927f..5be81b45612bcc 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 8f818f70012784..b13201ce5d38a5 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 b5d9c38d998108..70e1355004c473 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 9c51807b8d2199..7e9432d8c8678e 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 24e4e03f829ce6..4b1eaa631dc988 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 54054159584855..b4b272d2a241ad 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 3709f162b612b2..b3b868dbc41e80 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 0c2776e01b962b..18c28a703e9669 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 383bc8c2ce015d..b4ec6f3df60281 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 5f555f851cd816..29e5a29edc7c06 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 780a2119037b1a..00000000000000 --- 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 4c408da92f5791..d5c20d1853d421 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,20 +13,24 @@ 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: - - id: 'my-slack1' <1> + my-slack1: <1> actionTypeId: .slack <2> name: 'Slack #xyz' <3> config: <4> webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' - - id: 'webhook-service' + webhook-service: actionTypeId: .webhook name: 'Email service' config: @@ -41,7 +44,7 @@ The following example shows a valid configuration 2 out-of-the box connector. password: changeme ``` -<1> `id` is the action connector identifier. +<1> the key is the action connector identifier, eg `my-slack1` in this example. <2> `actionTypeId` is the action type identifier. <3> `name` is the name of the preconfigured connector. <4> `config` is the action type specific to the configuration. @@ -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> + 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 852c3e1ecdeca1..9e41cce561454b 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/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc index 36709c2cc64370..9a1e81670b6541 100644 --- a/docs/visualize/tsvb.asciidoc +++ b/docs/visualize/tsvb.asciidoc @@ -122,3 +122,17 @@ Edit the source for the Markdown visualization. . To insert the mustache template variable into the editor, click the variable name. + The http://mustache.github.io/mustache.5.html[mustache syntax] uses the Handlebar.js processor, which is an extended version of the Mustache template language. + +[float] +[[tsvb-style-markdown]] +==== Style Markdown text + +Style your Markdown visualization using http://lesscss.org/features/[less syntax]. + +. Select *Markdown*. + +. Select *Panel options*. + +. Enter styling rules in *Custom CSS* section ++ +Less in TSVB does not support custom plugins or inline JavaScript. diff --git a/package.json b/package.json index 8a92b464893081..0c83cb429b651e 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,7 @@ "leaflet-responsive-popup": "0.6.4", "leaflet-vega": "^0.8.6", "leaflet.heat": "0.2.0", - "less": "^2.7.3", + "less": "npm:@elastic/less@2.7.3-kibana", "less-loader": "5.0.0", "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "lodash.clonedeep": "^4.5.0", diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/README.md b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md index 6133f9871699f9..c7b98224c4e570 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/README.md +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md @@ -8,7 +8,7 @@ This class integrates with the `ciStats.trackBuild {}` Jenkins Pipeline function To create an instance of the reporter, import the class and call `CiStatsReporter.fromEnv(log)` (passing it a tooling log). -#### `CiStatsReporter#metric(name: string, subName: string, value: number)` +#### `CiStatsReporter#metrics(metrics: Array<{ group: string, id: string, value: number }>)` Use this method to record metrics in the Kibana CI Stats service. @@ -19,5 +19,11 @@ import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; const log = new ToolingLog(...); const reporter = CiStatsReporter.fromEnv(log) -reporter.metric('Build speed', specificBuildName, timeToRunBuild) +reporter.metrics([ + { + group: 'Build size', + id: specificBuildName, + value: sizeOfBuild + } +]) ``` \ No newline at end of file diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index 5fe1844a85563c..4e912896104328 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -84,13 +84,16 @@ export class CiStatsReporter { return !!this.config; } - async metric(name: string, subName: string, value: number) { + async metrics(metrics: Array<{ group: string; id: string; value: number }>) { if (!this.config) { return; } let attempt = 0; const maxAttempts = 5; + const bodySummary = metrics + .map(({ group, id, value }) => `[${group}/${id}=${value}]`) + .join(' '); while (true) { attempt += 1; @@ -98,18 +101,14 @@ export class CiStatsReporter { try { await Axios.request({ method: 'POST', - url: '/metric', + url: '/v1/metrics', baseURL: this.config.apiUrl, - params: { - buildId: this.config.buildId, - }, headers: { Authorization: `token ${this.config.apiToken}`, }, data: { - name, - subName, - value, + buildId: this.config.buildId, + metrics, }, }); @@ -125,14 +124,14 @@ export class CiStatsReporter { this.log.warning( `error recording metric [status=${error.response.status}] [resp=${inspect( error.response.data - )}] [${name}/${subName}=${value}]` + )}] ${bodySummary}` ); return; } if (attempt === maxAttempts) { this.log.warning( - `failed to reach kibana-ci-stats service too many times, unable to record metric [${name}/${subName}=${value}]` + `failed to reach kibana-ci-stats service too many times, unable to record metric ${bodySummary}` ); return; } diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index e46075eff63a77..a2fbe969e34d82 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -21,7 +21,7 @@ import 'source-map-support/register'; import Path from 'path'; -import { run, REPO_ROOT, createFlagError, createFailError, CiStatsReporter } from '@kbn/dev-utils'; +import { run, REPO_ROOT, createFlagError, CiStatsReporter } from '@kbn/dev-utils'; import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; @@ -82,9 +82,9 @@ run( throw createFlagError('expected --scan-dir to be a string'); } - const reportStatsName = flags['report-stats']; - if (reportStatsName !== undefined && typeof reportStatsName !== 'string') { - throw createFlagError('expected --report-stats to be a string'); + const reportStats = flags['report-stats'] ?? false; + if (typeof reportStats !== 'boolean') { + throw createFlagError('expected --report-stats to have no value'); } const config = OptimizerConfig.create({ @@ -103,22 +103,32 @@ run( let update$ = runOptimizer(config); - if (reportStatsName) { + if (reportStats) { const reporter = CiStatsReporter.fromEnv(log); if (!reporter.isEnabled()) { - throw createFailError('Unable to initialize CiStatsReporter from env'); + log.warning('Unable to initialize CiStatsReporter from env'); } - update$ = update$.pipe(reportOptimizerStats(reporter, reportStatsName)); + update$ = update$.pipe(reportOptimizerStats(reporter, config)); } await update$.pipe(logOptimizerState(log, config)).toPromise(); }, { flags: { - boolean: ['core', 'watch', 'oss', 'examples', 'dist', 'cache', 'profile', 'inspect-workers'], - string: ['workers', 'scan-dir', 'report-stats'], + boolean: [ + 'core', + 'watch', + 'oss', + 'examples', + 'dist', + 'cache', + 'profile', + 'inspect-workers', + 'report-stats', + ], + string: ['workers', 'scan-dir'], default: { core: true, examples: true, @@ -136,7 +146,7 @@ run( --dist create bundles that are suitable for inclusion in the Kibana distributable --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) --no-inspect-workers when inspecting the parent process, don't inspect the workers - --report-stats=[name] attempt to report stats about this execution of the build to the kibana-ci-stats service using this name + --report-stats attempt to report stats about this execution of the build to the kibana-ci-stats service using this name `, }, } diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts index 375978b9b79447..06161fb2567b94 100644 --- a/packages/kbn-optimizer/src/report_optimizer_stats.ts +++ b/packages/kbn-optimizer/src/report_optimizer_stats.ts @@ -21,10 +21,10 @@ import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; import { CiStatsReporter } from '@kbn/dev-utils'; import { OptimizerUpdate$ } from './run_optimizer'; -import { OptimizerState } from './optimizer'; +import { OptimizerState, OptimizerConfig } from './optimizer'; import { pipeClosure } from './common'; -export function reportOptimizerStats(reporter: CiStatsReporter, name: string) { +export function reportOptimizerStats(reporter: CiStatsReporter, config: OptimizerConfig) { return pipeClosure((update$: OptimizerUpdate$) => { let lastState: OptimizerState | undefined; return update$.pipe( @@ -35,7 +35,18 @@ export function reportOptimizerStats(reporter: CiStatsReporter, name: string) { } if (n.kind === 'C' && lastState) { - await reporter.metric('@kbn/optimizer build time', name, lastState.durSec); + await reporter.metrics( + config.bundles.map(bundle => { + // make the cache read from the cache file since it was likely updated by the worker + bundle.cache.refresh(); + + return { + group: `@kbn/optimizer bundle module count`, + id: bundle.id, + value: bundle.cache.getModuleCount() || 0, + }; + }) + ); } return n; diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 95e826e7620aa6..49bcc6e7e704c2 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -137,9 +137,9 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { // or which have require() statements that should be ignored because the file is // already bundled with all its necessary depedencies noParse: [ - /[\///]node_modules[\///]elasticsearch-browser[\///]/, - /[\///]node_modules[\///]lodash[\///]index\.js$/, - /[\///]node_modules[\///]vega-lib[\///]build[\///]vega\.js$/, + /[\/\\]node_modules[\/\\]elasticsearch-browser[\/\\]/, + /[\/\\]node_modules[\/\\]lodash[\/\\]index\.js$/, + /[\/\\]node_modules[\/\\]vega-lib[\/\\]build[\/\\]vega\.js$/, ], rules: [ diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 28cf36dedba3f3..1b70cced4a5c9c 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -43933,30 +43933,29 @@ class CiStatsReporter { isEnabled() { return !!this.config; } - async metric(name, subName, value) { + async metrics(metrics) { var _a, _b, _c, _d; if (!this.config) { return; } let attempt = 0; const maxAttempts = 5; + const bodySummary = metrics + .map(({ group, id, value }) => `[${group}/${id}=${value}]`) + .join(' '); while (true) { attempt += 1; try { await axios_1.default.request({ method: 'POST', - url: '/metric', + url: '/v1/metrics', baseURL: this.config.apiUrl, - params: { - buildId: this.config.buildId, - }, headers: { Authorization: `token ${this.config.apiToken}`, }, data: { - name, - subName, - value, + buildId: this.config.buildId, + metrics, }, }); return; @@ -43968,11 +43967,11 @@ class CiStatsReporter { } if (((_b = error) === null || _b === void 0 ? void 0 : _b.response) && error.response.status !== 502) { // error response from service was received so warn the user and move on - this.log.warning(`error recording metric [status=${error.response.status}] [resp=${util_1.inspect(error.response.data)}] [${name}/${subName}=${value}]`); + this.log.warning(`error recording metric [status=${error.response.status}] [resp=${util_1.inspect(error.response.data)}] ${bodySummary}`); return; } if (attempt === maxAttempts) { - this.log.warning(`failed to reach kibana-ci-stats service too many times, unable to record metric [${name}/${subName}=${value}]`); + this.log.warning(`failed to reach kibana-ci-stats service too many times, unable to record metric ${bodySummary}`); return; } // we failed to reach the backend and we have remaining attempts, lets retry after a short delay diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 8442f1ecc6411a..fd496da26283c4 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -114,7 +114,9 @@ export class ApplicationService { context, http: { basePath }, injectedMetadata, - redirectTo = (path: string) => (window.location.href = path), + redirectTo = (path: string) => { + window.location.assign(path); + }, history, }: SetupDeps): InternalApplicationSetup { const basename = basePath.get(); @@ -210,7 +212,10 @@ export class ApplicationService { } const appBasePath = basePath.prepend(appRoute); - const mount: LegacyAppMounter = () => redirectTo(appBasePath); + const mount: LegacyAppMounter = ({ history: appHistory }) => { + redirectTo(appHistory.createHref(appHistory.location)); + window.location.reload(); + }; const { updater$, ...appProps } = app; this.apps.set(app.id, { diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx index 60c36d3e330e0b..e399fbc726977a 100644 --- a/src/core/public/application/integration_tests/application_service.test.tsx +++ b/src/core/public/application/integration_tests/application_service.test.tsx @@ -18,8 +18,10 @@ */ import { take } from 'rxjs/operators'; -import { createRenderer } from './utils'; +import { act } from 'react-dom/test-utils'; import { createMemoryHistory, MemoryHistory } from 'history'; + +import { createRenderer } from './utils'; import { ApplicationService } from '../application_service'; import { httpServiceMock } from '../../http/http_service.mock'; import { contextServiceMock } from '../../context/context_service.mock'; @@ -27,6 +29,9 @@ import { injectedMetadataServiceMock } from '../../injected_metadata/injected_me import { MockLifecycle } from '../test_types'; import { overlayServiceMock } from '../../overlays/overlay_service.mock'; import { AppMountParameters } from '../types'; +import { ScopedHistory } from '../scoped_history'; + +const flushPromises = () => new Promise(resolve => setImmediate(resolve)); describe('ApplicationService', () => { let setupDeps: MockLifecycle<'setup'>; @@ -83,7 +88,10 @@ describe('ApplicationService', () => { expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1'); - resolveMount!(); + await act(async () => { + resolveMount!(); + await flushPromises(); + }); expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1'); }); @@ -109,7 +117,7 @@ describe('ApplicationService', () => { const { navigateToApp, currentAppId$ } = await service.start(startDeps); - await navigateToApp('app1'); + await act(() => navigateToApp('app1')); expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1'); @@ -120,6 +128,46 @@ describe('ApplicationService', () => { }); }); + it('redirects to full path when navigating to legacy app', async () => { + const redirectTo = jest.fn(); + const reloadSpy = jest.spyOn(window.location, 'reload').mockImplementation(() => {}); + + // In the real application, we use a BrowserHistory instance configured with `basename`. However, in tests we must + // use MemoryHistory which does not support `basename`. In order to emulate this behavior, we will wrap this + // instance with a ScopedHistory configured with a basepath. + history.push(setupDeps.http.basePath.get()); // ScopedHistory constructor will fail if underlying history is not currently at basePath. + const { register, registerLegacyApp } = service.setup({ + ...setupDeps, + redirectTo, + history: new ScopedHistory(history, setupDeps.http.basePath.get()), + }); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: ({ onAppLeave }: AppMountParameters) => { + onAppLeave(actions => actions.default()); + return () => undefined; + }, + }); + registerLegacyApp({ + id: 'myLegacyTestApp', + appUrl: '/app/myLegacyTestApp', + title: 'My Legacy Test App', + }); + + const { navigateToApp, getComponent } = await service.start(startDeps); + + update = createRenderer(getComponent()); + + await navigate('/test/app/app1'); + await act(() => navigateToApp('myLegacyTestApp', { path: '#/some-path' })); + + expect(redirectTo).toHaveBeenCalledWith('/test/app/myLegacyTestApp#/some-path'); + expect(reloadSpy).toHaveBeenCalled(); + reloadSpy.mockRestore(); + }); + describe('leaving an application that registered an app leave handler', () => { it('navigates to the new app if action is default', async () => { startDeps.overlays.openConfirm.mockResolvedValue(true); @@ -146,8 +194,10 @@ describe('ApplicationService', () => { update = createRenderer(getComponent()); - await navigate('/app/app1'); - await navigateToApp('app2'); + await act(async () => { + await navigate('/app/app1'); + await navigateToApp('app2'); + }); expect(startDeps.overlays.openConfirm).not.toHaveBeenCalled(); expect(history.entries.length).toEqual(3); @@ -179,8 +229,10 @@ describe('ApplicationService', () => { update = createRenderer(getComponent()); - await navigate('/app/app1'); - await navigateToApp('app2'); + await act(async () => { + await navigate('/app/app1'); + await navigateToApp('app2'); + }); expect(startDeps.overlays.openConfirm).toHaveBeenCalledTimes(1); expect(startDeps.overlays.openConfirm).toHaveBeenCalledWith( @@ -216,8 +268,10 @@ describe('ApplicationService', () => { update = createRenderer(getComponent()); - await navigate('/app/app1'); - await navigateToApp('app2'); + await act(async () => { + await navigate('/app/app1'); + await navigateToApp('app2'); + }); expect(startDeps.overlays.openConfirm).toHaveBeenCalledTimes(1); expect(startDeps.overlays.openConfirm).toHaveBeenCalledWith( diff --git a/src/core/public/chrome/ui/_loading_indicator.scss b/src/core/public/chrome/ui/_loading_indicator.scss index 026c23b93b040f..ad934717b4b766 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/logging/README.md b/src/core/server/logging/README.md index ed64e7c4ce0b1c..553dc7c36e824c 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -167,7 +167,7 @@ logging: - context: plugins appenders: [custom] level: warn - - context: plugins.pid + - context: plugins.myPlugin level: info - context: server level: fatal @@ -180,14 +180,14 @@ logging: Here is what we get with the config above: -| Context | Appenders | Level | -| ------------- |:------------------------:| -----:| -| root | console, file | error | -| plugins | custom | warn | -| plugins.pid | custom | info | -| server | console, file | fatal | -| optimize | console | error | -| telemetry | json-file-appender | all | +| Context | Appenders | Level | +| ---------------- |:------------------------:| -----:| +| root | console, file | error | +| plugins | custom | warn | +| plugins.myPlugin | custom | info | +| server | console, file | fatal | +| optimize | console | error | +| telemetry | json-file-appender | all | The `root` logger has a dedicated configuration node since this context is special and should always exist. By @@ -259,7 +259,7 @@ define a custom one. ```yaml logging: loggers: - - context: your-plugin + - context: plugins.myPlugin appenders: [console] ``` Logs in a *file* if given file path. You should define a custom appender with `kind: file` @@ -273,7 +273,7 @@ logging: layout: kind: pattern loggers: - - context: your-plugin + - context: plugins.myPlugin appenders: [file] ``` #### logging.json @@ -282,10 +282,10 @@ the output format with [layouts](#layouts). #### logging.quiet Suppresses all logging output other than error messages. With new logging, config can be achieved -with adjusting minimum required [logging level](#log-level) +with adjusting minimum required [logging level](#log-level). ```yaml loggers: - - context: my-plugin + - context: plugins.myPlugin appenders: [console] level: error # or for all output diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 3ec478e3ca28db..bd10520ca1c571 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -293,7 +293,7 @@ describe('DocumentMigrator', () => { migrationVersion: { dog: '10.2.0' }, }) ).toThrow( - /Document "smelly" has property "dog" which belongs to a more recent version of Kibana \(10\.2\.0\)/i + /Document "smelly" has property "dog" which belongs to a more recent version of Kibana \[10\.2\.0\]\. The last known version is \[undefined\]/i ); }); @@ -315,7 +315,7 @@ describe('DocumentMigrator', () => { migrationVersion: { dawg: '1.2.4' }, }) ).toThrow( - /Document "fleabag" has property "dawg" which belongs to a more recent version of Kibana \(1\.2\.4\)/i + /Document "fleabag" has property "dawg" which belongs to a more recent version of Kibana \[1\.2\.4\]\. The last known version is \[1\.2\.3\]/i ); }); diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 4ddb2b070d3ac3..07c1da55861076 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -350,7 +350,7 @@ function nextUnmigratedProp(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMi if (docVersion && (!latestVersion || Semver.gt(docVersion, latestVersion))) { throw Boom.badData( `Document "${doc.id}" has property "${p}" which belongs to a more recent` + - ` version of Kibana (${docVersion}).`, + ` version of Kibana [${docVersion}]. The last known version is [${latestVersion}]`, doc ); } diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index c75fa68572c710..ef2a8870d78d0f 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -195,7 +195,7 @@ async function migrateSourceToDest(context: Context) { await Index.write( callCluster, dest.indexName, - migrateRawDocs(serializer, documentMigrator.migrate, docs) + migrateRawDocs(serializer, documentMigrator.migrate, docs, log) ); } } diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 89f3fde3848488..e55b72be2436d9 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -21,6 +21,7 @@ import _ from 'lodash'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsSerializer } from '../../serialization'; import { migrateRawDocs } from './migrate_raw_docs'; +import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { @@ -31,7 +32,8 @@ describe('migrateRawDocs', () => { [ { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ] + ], + createSavedObjectsMigrationLoggerMock() ); expect(result).toEqual([ @@ -48,7 +50,8 @@ describe('migrateRawDocs', () => { expect(transform).toHaveBeenCalled(); }); - test('passes invalid docs through untouched', async () => { + test('passes invalid docs through untouched and logs error', async () => { + const logger = createSavedObjectsMigrationLoggerMock(); const transform = jest.fn((doc: any) => _.set(_.cloneDeep(doc), 'attributes.name', 'TADA') ); @@ -58,7 +61,8 @@ describe('migrateRawDocs', () => { [ { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ] + ], + logger ); expect(result).toEqual([ @@ -82,5 +86,7 @@ describe('migrateRawDocs', () => { }, ], ]); + + expect(logger.error).toBeCalledTimes(1); }); }); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index 5fe15f40db8ec8..49acea82e1c8af 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -23,6 +23,7 @@ import { SavedObjectsRawDoc, SavedObjectsSerializer } from '../../serialization'; import { TransformFn } from './document_migrator'; +import { SavedObjectsMigrationLogger } from '.'; /** * Applies the specified migration function to every saved object document in the list @@ -35,7 +36,8 @@ import { TransformFn } from './document_migrator'; export function migrateRawDocs( serializer: SavedObjectsSerializer, migrateDoc: TransformFn, - rawDocs: SavedObjectsRawDoc[] + rawDocs: SavedObjectsRawDoc[], + log: SavedObjectsMigrationLogger ): SavedObjectsRawDoc[] { return rawDocs.map(raw => { if (serializer.isRawSavedObject(raw)) { @@ -47,6 +49,10 @@ export function migrateRawDocs( }); } + log.error( + `Error: Unable to migrate the corrupt Saved Object document ${raw._id}. To prevent Kibana from performing a migration on every restart, please delete or fix this document by ensuring that the namespace and type in the document's id matches the values in the namespace and type fields.`, + { rawDocument: raw } + ); return raw; }); } diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts index 800edaeaa58858..3f2c31a7c0e5cf 100644 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts @@ -19,14 +19,10 @@ import _ from 'lodash'; import { coordinateMigration } from './migration_coordinator'; +import { createSavedObjectsMigrationLoggerMock } from '../mocks'; describe('coordinateMigration', () => { - const log = { - debug: jest.fn(), - warning: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - }; + const log = createSavedObjectsMigrationLoggerMock(); test('waits for isMigrated, if there is an index conflict', async () => { const pollInterval = 1; diff --git a/src/core/server/saved_objects/migrations/core/migration_logger.ts b/src/core/server/saved_objects/migrations/core/migration_logger.ts index 9dfb3abc8e72da..00ed8bf0b73fc5 100644 --- a/src/core/server/saved_objects/migrations/core/migration_logger.ts +++ b/src/core/server/saved_objects/migrations/core/migration_logger.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Logger } from 'src/core/server/logging'; +import { Logger, LogMeta } from '../../../logging'; /* * This file provides a helper class for ensuring that all logging @@ -35,6 +35,7 @@ export interface SavedObjectsMigrationLogger { */ warning: (msg: string) => void; warn: (msg: string) => void; + error: (msg: string, meta: LogMeta) => void; } export class MigrationLogger implements SavedObjectsMigrationLogger { @@ -48,4 +49,5 @@ export class MigrationLogger implements SavedObjectsMigrationLogger { public debug = (msg: string) => this.logger.debug(msg); public warning = (msg: string) => this.logger.warn(msg); public warn = (msg: string) => this.logger.warn(msg); + public error = (msg: string, meta: LogMeta) => this.logger.error(msg, meta); } diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index dafd6c53411966..7d9ff9bed6d728 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -22,9 +22,9 @@ * (the shape of the mappings and documents in the index). */ -import { Logger } from 'src/core/server/logging'; import { KibanaConfigType } from 'src/core/server/kibana_config'; import { BehaviorSubject } from 'rxjs'; +import { Logger } from '../../../logging'; import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings'; import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; import { docValidator, PropertyValidators } from '../../validation'; diff --git a/src/core/server/saved_objects/migrations/mocks.ts b/src/core/server/saved_objects/migrations/mocks.ts index 76a890d26bfa0c..50a71913934720 100644 --- a/src/core/server/saved_objects/migrations/mocks.ts +++ b/src/core/server/saved_objects/migrations/mocks.ts @@ -20,12 +20,13 @@ import { SavedObjectMigrationContext } from './types'; import { SavedObjectsMigrationLogger } from './core'; -const createLoggerMock = (): jest.Mocked => { +export const createSavedObjectsMigrationLoggerMock = (): jest.Mocked => { const mock = { debug: jest.fn(), info: jest.fn(), warning: jest.fn(), warn: jest.fn(), + error: jest.fn(), }; return mock; @@ -33,7 +34,7 @@ const createLoggerMock = (): jest.Mocked => { const createContextMock = (): jest.Mocked => { const mock = { - log: createLoggerMock(), + log: createSavedObjectsMigrationLoggerMock(), }; return mock; }; diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 85f15b4c18b66a..5e55a34193a962 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/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 927171438ae996..c46fcfbc6dbd74 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -23,6 +23,7 @@ import { SavedObjectsErrorHelpers } from './errors'; import { SavedObjectsSerializer } from '../../serialization'; import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { DocumentMigrator } from '../../migrations/core/document_migrator'; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -115,6 +116,7 @@ describe('SavedObjectsRepository', () => { const createType = type => ({ name: type, mappings: { properties: mappings.properties[type].properties }, + migrations: { '1.1.1': doc => doc }, }); const registry = new SavedObjectTypeRegistry(); @@ -144,6 +146,13 @@ describe('SavedObjectsRepository', () => { namespaceType: 'agnostic', }); + const documentMigrator = new DocumentMigrator({ + typeRegistry: registry, + kibanaVersion: '2.0.0', + log: {}, + validateDoc: jest.fn(), + }); + const getMockGetResponse = ({ type, id, references, namespace }) => ({ // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these found: true, @@ -207,7 +216,7 @@ describe('SavedObjectsRepository', () => { beforeEach(() => { callAdminCluster = jest.fn(); migrator = { - migrateDocument: jest.fn(doc => doc), + migrateDocument: jest.fn().mockImplementation(documentMigrator.migrate), runMigrations: async () => ({ status: 'skipped' }), }; @@ -424,9 +433,17 @@ describe('SavedObjectsRepository', () => { const getMockBulkCreateResponse = (objects, namespace) => { return { - items: objects.map(({ type, id }) => ({ + items: objects.map(({ type, id, attributes, references, migrationVersion }) => ({ create: { _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`, + _source: { + [type]: attributes, + type, + namespace, + references, + ...mockTimestampFields, + migrationVersion: migrationVersion || { [type]: '1.1.1' }, + }, ...mockVersionProps, }, })), @@ -474,7 +491,7 @@ describe('SavedObjectsRepository', () => { const expectSuccessResult = obj => ({ ...obj, - migrationVersion: undefined, + migrationVersion: { [obj.type]: '1.1.1' }, version: mockVersion, ...mockTimestampFields, }); @@ -619,13 +636,16 @@ describe('SavedObjectsRepository', () => { }; const bulkCreateError = async (obj, esError, expectedError) => { - const objects = [obj1, obj, obj2]; - const response = getMockBulkCreateResponse(objects); + let response; if (esError) { + response = getMockBulkCreateResponse([obj1, obj, obj2]); response.items[1].create = { error: esError }; + } else { + response = getMockBulkCreateResponse([obj1, obj2]); } callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + const objects = [obj1, obj, obj2]; const result = await savedObjectsRepository.bulkCreate(objects); expectClusterCalls('bulk'); const objCall = esError ? expectObjArgs(obj) : []; @@ -781,7 +801,7 @@ describe('SavedObjectsRepository', () => { id: 'three', }; const objects = [obj1, obj, obj2]; - const response = getMockBulkCreateResponse(objects); + const response = getMockBulkCreateResponse([obj1, obj2]); callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) const result = await savedObjectsRepository.bulkCreate(objects); expect(callAdminCluster).toHaveBeenCalledTimes(1); @@ -789,6 +809,32 @@ describe('SavedObjectsRepository', () => { saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], }); }); + + it(`a deserialized saved object`, async () => { + // Test for fix to https://github.com/elastic/kibana/issues/65088 where + // we returned raw ID's when an object without an id was created. + const namespace = 'myspace'; + const response = getMockBulkCreateResponse([obj1, obj2], namespace); + callAdminCluster.mockResolvedValueOnce(response); // this._writeToCluster('bulk', ...) + + // Bulk create one object with id unspecified, and one with id specified + const result = await savedObjectsRepository.bulkCreate([{ ...obj1, id: undefined }, obj2], { + namespace, + }); + + // Assert that both raw docs from the ES response are deserialized + expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(1, { + ...response.items[0].create, + _id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/), + }); + expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(2, response.items[1].create); + + // Assert that ID's are deserialized to remove the type and namespace + expect(result.saved_objects[0].id).toEqual( + expect.stringMatching(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/) + ); + expect(result.saved_objects[1].id).toEqual(obj2.id); + }); }); }); @@ -1604,6 +1650,7 @@ describe('SavedObjectsRepository', () => { version: mockVersion, attributes, references, + migrationVersion: { [type]: '1.1.1' }, }); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index bc8ad2cdb00582..61027130e0eb73 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -18,6 +18,7 @@ */ import { omit } from 'lodash'; +import uuid from 'uuid'; import { retryCallCluster } from '../../../elasticsearch/retry_call_cluster'; import { APICaller } from '../../../elasticsearch/'; @@ -299,6 +300,8 @@ export class SavedObjectsRepository { const requiresNamespacesCheck = method === 'index' && this._registry.isMultiNamespace(object.type); + if (object.id == null) object.id = uuid.v1(); + return { tag: 'Right' as 'Right', value: { @@ -404,35 +407,25 @@ export class SavedObjectsRepository { } const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; - const response = bulkResponse.items[esRequestIndex]; - const { - error, - _id: responseId, - _seq_no: seqNo, - _primary_term: primaryTerm, - } = Object.values(response)[0] as any; - - const { - _source: { type, [type]: attributes, references = [], namespaces }, - } = rawMigratedDoc; - - const id = requestedId || responseId; + const { error, ...rawResponse } = Object.values( + bulkResponse.items[esRequestIndex] + )[0] as any; + if (error) { return { - id, - type, - error: getBulkOperationError(error, type, id), + id: requestedId, + type: rawMigratedDoc._source.type, + error: getBulkOperationError(error, rawMigratedDoc._source.type, requestedId), }; } - return { - id, - type, - ...(namespaces && { namespaces }), - updated_at: time, - version: encodeVersion(seqNo, primaryTerm), - attributes, - references, - }; + + // When method == 'index' the bulkResponse doesn't include the indexed + // _source so we return rawMigratedDoc but have to spread the latest + // _seq_no and _primary_term values from the rawResponse. + return this._serializer.rawToSavedObject({ + ...rawMigratedDoc, + ...{ _seq_no: rawResponse._seq_no, _primary_term: rawResponse._primary_term }, + }); }), }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 62d11ee7cf9a7a..e4234689c25e81 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -91,7 +91,6 @@ import { IngestGetPipelineParams } from 'elasticsearch'; import { IngestPutPipelineParams } from 'elasticsearch'; import { IngestSimulateParams } from 'elasticsearch'; import { KibanaConfigType } from 'src/core/server/kibana_config'; -import { Logger as Logger_2 } from 'src/core/server/logging'; import { MGetParams } from 'elasticsearch'; import { MGetResponse } from 'elasticsearch'; import { MSearchParams } from 'elasticsearch'; @@ -1735,7 +1734,7 @@ export type SavedObjectMigrationFn; } // @public @@ -2169,6 +2168,8 @@ export interface SavedObjectsMigrationLogger { // (undocumented) debug: (msg: string) => void; // (undocumented) + error: (msg: string, meta: LogMeta) => void; + // (undocumented) info: (msg: string) => void; // (undocumented) warn: (msg: string) => void; diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.js b/src/dev/build/tasks/build_kibana_platform_plugins.js index 28d6b49f9e89a8..153a3120f896f8 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.js +++ b/src/dev/build/tasks/build_kibana_platform_plugins.js @@ -39,11 +39,10 @@ export const BuildKibanaPlatformPluginsTask = { }); const reporter = CiStatsReporter.fromEnv(log); - const reportStatsName = build.isOss() ? 'oss distributable' : 'default distributable'; await runOptimizer(optimizerConfig) .pipe( - reportOptimizerStats(reporter, reportStatsName), + reportOptimizerStats(reporter, optimizerConfig), logOptimizerState(log, optimizerConfig) ) .toPromise(); diff --git a/src/dev/build/tasks/create_archives_task.js b/src/dev/build/tasks/create_archives_task.js index 06be1bd0bd14f3..541b9551dbc9ba 100644 --- a/src/dev/build/tasks/create_archives_task.js +++ b/src/dev/build/tasks/create_archives_task.js @@ -17,13 +17,22 @@ * under the License. */ -import path from 'path'; +import Path from 'path'; +import Fs from 'fs'; +import { promisify } from 'util'; + +import { CiStatsReporter } from '@kbn/dev-utils'; + import { mkdirp, compress } from '../lib'; +const asyncStat = promisify(Fs.stat); + export const CreateArchivesTask = { description: 'Creating the archives for each platform', async run(config, log, build) { + const archives = []; + // archive one at a time, parallel causes OOM sometimes for (const platform of config.getTargetPlatforms()) { const source = build.resolvePathForPlatform(platform, '.'); @@ -31,10 +40,15 @@ export const CreateArchivesTask = { log.info('archiving', source, 'to', destination); - await mkdirp(path.dirname(destination)); + await mkdirp(Path.dirname(destination)); - switch (path.extname(destination)) { + switch (Path.extname(destination)) { case '.zip': + archives.push({ + format: 'zip', + path: destination, + }); + await compress( 'zip', { @@ -51,6 +65,11 @@ export const CreateArchivesTask = { break; case '.gz': + archives.push({ + format: 'tar', + path: destination, + }); + await compress( 'tar', { @@ -71,5 +90,20 @@ export const CreateArchivesTask = { throw new Error(`Unexpected extension for archive destination: ${destination}`); } } + + const reporter = CiStatsReporter.fromEnv(log); + if (reporter.isEnabled()) { + await reporter.metrics( + await Promise.all( + archives.map(async ({ format, path }) => { + return { + group: `${build.isOss() ? 'oss ' : ''}distributable size`, + id: format, + value: (await asyncStat(path)).size, + }; + }) + ) + ); + } }, }; diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index a13f61af601733..5019c8bd223411 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 9f5f4b764f9b05..691318e32245b4 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 ad67a74121cc94..4e97d46ab1773e 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/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts index bdb1436c37efb3..83335a6fabfeb6 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts @@ -96,18 +96,21 @@ export function getTabs( tabs.push({ name: getTitle('indexed', filteredCount, totalCount), id: TAB_INDEXED_FIELDS, + 'data-test-subj': 'tab-indexedFields', }); if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { tabs.push({ name: getTitle('scripted', filteredCount, totalCount), id: TAB_SCRIPTED_FIELDS, + 'data-test-subj': 'tab-scriptedFields', }); } tabs.push({ name: getTitle('sourceFilters', filteredCount, totalCount), id: TAB_SOURCE_FILTERS, + 'data-test-subj': 'tab-sourceFilters', }); return tabs; diff --git a/src/legacy/core_plugins/region_map/index.ts b/src/legacy/core_plugins/region_map/index.ts deleted file mode 100644 index 8c059314786bcc..00000000000000 --- a/src/legacy/core_plugins/region_map/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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const regionMapPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'region_map', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars(server) { - const { regionmap } = server.config().get('map'); - - return { - regionmap, - }; - }, - }, - init: (server: Legacy.Server) => ({}), - 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 regionMapPluginInitializer; diff --git a/src/legacy/core_plugins/region_map/public/legacy.ts b/src/legacy/core_plugins/region_map/public/legacy.ts deleted file mode 100644 index 4bbd839331e56b..00000000000000 --- a/src/legacy/core_plugins/region_map/public/legacy.ts +++ /dev/null @@ -1,35 +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 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { RegionMapPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - mapsLegacy: npSetup.plugins.mapsLegacy, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core); 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 27f019318a82b4..00000000000000 --- 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/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index 1629aac588a617..21e7b559f71f5f 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -22,6 +22,7 @@ jest.mock('history'); import { setRootControllerMock, historyMock } from './new_platform.test.mocks'; import { legacyAppRegister, __reset__, __setup__, __start__ } from './new_platform'; import { coreMock } from '../../../../core/public/mocks'; +import { AppMount } from '../../../../core/public'; describe('ui/new_platform', () => { describe('legacyAppRegister', () => { @@ -33,7 +34,7 @@ describe('ui/new_platform', () => { const registerApp = () => { const unmountMock = jest.fn(); - const mountMock = jest.fn(() => unmountMock); + const mountMock = jest.fn, Parameters>(() => unmountMock); legacyAppRegister({ id: 'test', title: 'Test', @@ -62,13 +63,25 @@ describe('ui/new_platform', () => { controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith({ - element: elementMock[0], + element: expect.any(HTMLElement), appBasePath: '/test/base/path/app/test', onAppLeave: expect.any(Function), history: historyMock, }); }); + test('app is mounted in new div inside containing element', () => { + const { mountMock } = registerApp(); + const controller = setRootControllerMock.mock.calls[0][1]; + const scopeMock = { $on: jest.fn() }; + const elementMock = [document.createElement('div')]; + + controller(scopeMock, elementMock); + + const { element } = mountMock.mock.calls[0][0]; + expect(element.parentElement).toEqual(elementMock[0]); + }); + test('controller calls deprecated context app.mount when invoked', () => { const unmountMock = jest.fn(); // Two arguments changes how this is called. @@ -84,7 +97,7 @@ describe('ui/new_platform', () => { controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { - element: elementMock[0], + element: expect.any(HTMLElement), appBasePath: '/test/base/path/app/test', onAppLeave: expect.any(Function), history: historyMock, diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index a15c7cce5511d9..1eb46e1a438955 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -176,7 +176,8 @@ export const legacyAppRegister = (app: App) => { legacyAppRegistered = true; require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { - const element = $element[0]; + const element = document.createElement('div'); + $element[0].appendChild(element); // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { 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 1bc85fa110ca0b..698c124d2d8057 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 8bf205b8cb5070..955d5244ce1904 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 = (

; + interface SetupDependencies { data: DataPublicPluginSetup; embeddable: EmbeddableSetup; @@ -111,8 +117,10 @@ interface StartDependencies { } export type Setup = void; + export interface DashboardStart { getSavedDashboardLoader: () => SavedObjectLoader; + dashboardUrlGenerator?: DashboardUrlGenerator; } declare module '../../../plugins/ui_actions/public' { @@ -130,6 +138,8 @@ export class DashboardPlugin private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; + private dashboardUrlGenerator?: DashboardUrlGenerator; + public setup( core: CoreSetup, { share, uiActions, embeddable, home, kibanaLegacy, data, usageCollection }: SetupDependencies @@ -140,8 +150,8 @@ export class DashboardPlugin const startServices = core.getStartServices(); if (share) { - share.urlGenerators.registerUrlGenerator( - createDirectAccessDashboardLinkGenerator(async () => { + this.dashboardUrlGenerator = share.urlGenerators.registerUrlGenerator( + createDashboardUrlGenerator(async () => { const [coreStart, , selfStart] = await startServices; return { appBasePath: coreStart.application.getUrlForApp('dashboard'), @@ -325,6 +335,7 @@ export class DashboardPlugin }); return { getSavedDashboardLoader: () => savedDashboardLoader, + dashboardUrlGenerator: this.dashboardUrlGenerator, }; } diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 248a3f991d6cbf..68d447c4a13361 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createDirectAccessDashboardLinkGenerator } from './url_generator'; +import { createDashboardUrlGenerator } from './url_generator'; import { hashedItemStore } from '../../kibana_utils/public'; // eslint-disable-next-line import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; @@ -55,7 +55,7 @@ describe('dashboard url generator', () => { }); test('creates a link to a saved dashboard', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, @@ -67,7 +67,7 @@ describe('dashboard url generator', () => { }); test('creates a link with global time range set up', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, @@ -83,7 +83,7 @@ describe('dashboard url generator', () => { }); test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, @@ -123,7 +123,7 @@ describe('dashboard url generator', () => { }); test('if no useHash setting is given, uses the one was start services', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true, @@ -137,7 +137,7 @@ describe('dashboard url generator', () => { }); test('can override a false useHash ui setting', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, @@ -152,7 +152,7 @@ describe('dashboard url generator', () => { }); test('can override a true useHash ui setting', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true, @@ -195,7 +195,7 @@ describe('dashboard url generator', () => { }; test('attaches filters from destination dashboard', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, @@ -224,7 +224,7 @@ describe('dashboard url generator', () => { }); test("doesn't fail if can't retrieve filters from destination dashboard", async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, @@ -246,7 +246,7 @@ describe('dashboard url generator', () => { }); test('can enforce empty filters', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, @@ -270,7 +270,7 @@ describe('dashboard url generator', () => { }); test('no filters in result url if no filters applied', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, @@ -288,7 +288,7 @@ describe('dashboard url generator', () => { }); test('can turn off preserving filters', async () => { - const generator = createDirectAccessDashboardLinkGenerator(() => + const generator = createDashboardUrlGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false, diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 6f121ceb2d3731..9d66f2df65777b 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -75,7 +75,7 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{ preserveSavedFilters?: boolean; }>; -export const createDirectAccessDashboardLinkGenerator = ( +export const createDashboardUrlGenerator = ( getStartServices: () => Promise<{ appBasePath: string; useHashedUrl: boolean; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index d4433f3825feae..69dd97a8817976 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -230,7 +230,6 @@ import { validateIndexPattern, getFromSavedObject, flattenHitWrapper, - getRoutes, formatHitProvider, } from './index_patterns'; @@ -246,8 +245,6 @@ export const indexPatterns = { validate: validateIndexPattern, getFromSavedObject, flattenHitWrapper, - // TODO: exported only in stub_index_pattern test. Move into data plugin and remove export. - getRoutes, formatHitProvider, }; diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index e05db0e4d4cec0..58c2cae1de0f38 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -26,7 +26,6 @@ export { getFromSavedObject, isDefault, } from './lib'; -export { getRoutes } from './utils'; export { flattenHitWrapper, formatHitProvider } from './index_patterns'; export { getIndexPatternFieldListCreator, Field, IIndexPatternFieldList } from './fields'; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts index f39be78433710f..98ec4495cef29a 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts @@ -30,7 +30,7 @@ import { import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; -import { findByTitle, getRoutes } from '../utils'; +import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; import { Field, IIndexPatternFieldList, getIndexPatternFieldListCreator } from '../fields'; import { createFieldsFetcher } from './_fields_fetcher'; @@ -190,10 +190,6 @@ export class IndexPattern implements IIndexPattern { return this.indexFields(forceFieldRefresh); } - public get routes() { - return getRoutes(); - } - getComputedFields() { const scriptFields: any = {}; if (!this.fields) { diff --git a/src/plugins/data/public/index_patterns/utils.ts b/src/plugins/data/public/index_patterns/utils.ts index 0ecc87f3080fd9..c3f9af62f8c0e7 100644 --- a/src/plugins/data/public/index_patterns/utils.ts +++ b/src/plugins/data/public/index_patterns/utils.ts @@ -48,13 +48,3 @@ export async function findByTitle( (obj: SimpleSavedObject) => obj.get('title').toLowerCase() === title.toLowerCase() ); } - -export function getRoutes() { - return { - edit: '/management/kibana/index_patterns/{{id}}', - addField: '/management/kibana/index_patterns/{{id}}/create-field', - indexedFields: '/management/kibana/index_patterns/{{id}}?_a=(tab:indexedFields)', - scriptedFields: '/management/kibana/index_patterns/{{id}}?_a=(tab:scriptedFields)', - sourceFilters: '/management/kibana/index_patterns/{{id}}?_a=(tab:sourceFilters)', - }; -} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index cb1e1d2bd0efe8..ee56ad60441f40 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -912,14 +912,6 @@ export class IndexPattern implements IIndexPattern { // (undocumented) removeScriptedField(field: IFieldType): Promise; // (undocumented) - get routes(): { - edit: string; - addField: string; - indexedFields: string; - scriptedFields: string; - sourceFilters: string; - }; - // (undocumented) save(saveAttempts?: number): Promise; // (undocumented) timeFieldName: string | undefined; @@ -1021,7 +1013,6 @@ export const indexPatterns: { validate: typeof validateIndexPattern; getFromSavedObject: typeof getFromSavedObject; flattenHitWrapper: typeof flattenHitWrapper; - getRoutes: typeof getRoutes; formatHitProvider: typeof formatHitProvider; }; @@ -1812,27 +1803,26 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:379:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:374:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:374:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:374:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:374:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:376:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/legacy/fetch_soon.test.ts b/src/plugins/data/public/search/legacy/fetch_soon.test.ts index b2e17798ccc9fa..6c0467e3297e8a 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.test.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.test.ts @@ -58,7 +58,7 @@ describe('fetchSoon', () => { (callClient as jest.Mock).mockClear(); }); - test('should delay by 0ms if config is set to not batch searches', () => { + test('should execute asap if config is set to not batch searches', () => { const config = getConfigStub({ 'courier:batchSearches': false, }); @@ -67,8 +67,6 @@ describe('fetchSoon', () => { fetchSoon(request, options, { config } as FetchHandlers); - expect(callClient).not.toBeCalled(); - jest.advanceTimersByTime(0); expect(callClient).toBeCalled(); }); diff --git a/src/plugins/data/public/search/legacy/fetch_soon.ts b/src/plugins/data/public/search/legacy/fetch_soon.ts index 18fa410a5bef03..83617d394fe958 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.ts @@ -67,6 +67,10 @@ async function delayedFetch( fetchHandlers: FetchHandlers, ms: number ) { + if (ms === 0) { + return callClient([request], [options], fetchHandlers)[0]; + } + const i = requestsToFetch.length; requestsToFetch = [...requestsToFetch, request]; requestOptions = [...requestOptions, options]; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index df4ba23244b4dc..1f4076aa12bded 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -93,8 +93,7 @@ import { IngestGetPipelineParams } from 'elasticsearch'; import { IngestPutPipelineParams } from 'elasticsearch'; import { IngestSimulateParams } from 'elasticsearch'; import { KibanaConfigType as KibanaConfigType_2 } from 'src/core/server/kibana_config'; -import { Logger as Logger_2 } from 'src/core/server/logging'; -import { Logger as Logger_3 } from 'kibana/server'; +import { Logger as Logger_2 } from 'kibana/server'; import { MGetParams } from 'elasticsearch'; import { MGetResponse } from 'elasticsearch'; import moment from 'moment'; diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss index 8eaa66cf586244..590dbebdf4cfee 100644 --- a/src/plugins/discover/public/application/_discover.scss +++ b/src/plugins/discover/public/application/_discover.scss @@ -38,17 +38,7 @@ discover-app { } .dscResultCount { - text-align: center; padding-top: $euiSizeXS; - padding-left: $euiSizeM; - - .dscResultHits { - padding-left: $euiSizeXS; - } - - > .kuiLink { - padding-left: $euiSizeM; - } } .dscTimechart__header { diff --git a/src/plugins/discover/public/application/angular/_index.scss b/src/plugins/discover/public/application/angular/_index.scss index 9e00ade3d41f6d..b0e5b6e3edf7b3 100644 --- a/src/plugins/discover/public/application/angular/_index.scss +++ b/src/plugins/discover/public/application/angular/_index.scss @@ -1,3 +1,2 @@ @import 'directives/index'; -@import 'doc_table/index'; @import 'context/index'; diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html index b4db89b9275b46..a0f98ea38ef783 100644 --- a/src/plugins/discover/public/application/angular/discover.html +++ b/src/plugins/discover/public/application/angular/discover.html @@ -89,24 +89,12 @@

{{screenTitle}}

-
- {{(hits || 0) | number:0}} - - -
+ +
; + + beforeAll(() => { + props = { + onResetQuery: jest.fn(), + showResetButton: true, + hits: 2, + }; + }); + + it('HitsCounter renders a button by providing the showResetButton property', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'resetSavedSearch').length).toBe(1); + }); + + it('HitsCounter not renders a button when the showResetButton property is false', () => { + component = mountWithIntl( + + ); + expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0); + }); + + it('expect to render the number of hits', function() { + component = mountWithIntl(); + const hits = findTestSubject(component, 'discoverQueryHits'); + expect(hits.text()).toBe('2'); + }); + + it('expect to render 1,899 hits if 1899 hits given', function() { + component = mountWithIntl( + + ); + const hits = findTestSubject(component, 'discoverQueryHits'); + expect(hits.text()).toBe('1,899'); + }); + + it('should reset query', function() { + component = mountWithIntl(); + findTestSubject(component, 'resetSavedSearch').simulate('click'); + expect(props.onResetQuery).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx new file mode 100644 index 00000000000000..1d2cd12877b1c0 --- /dev/null +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { formatNumWithCommas } from '../../helpers'; + +export interface HitsCounterProps { + /** + * the number of query hits + */ + hits: number; + /** + * displays the reset button + */ + showResetButton: boolean; + /** + * resets the query + */ + onResetQuery: () => void; +} + +export function HitsCounter({ hits, showResetButton, onResetQuery }: HitsCounterProps) { + return ( + + + + + {formatNumWithCommas(hits)}{' '} + + + + {showResetButton && ( + + + + + + )} + + + ); +} diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts b/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts new file mode 100644 index 00000000000000..8d45e28370cade --- /dev/null +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts @@ -0,0 +1,27 @@ +/* + * 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 { HitsCounter } from './hits_counter'; + +export function createHitsCounterDirective(reactDirective: any) { + return reactDirective(HitsCounter, [ + ['hits', { watchDepth: 'reference' }], + ['showResetButton', { watchDepth: 'reference' }], + ['onResetQuery', { watchDepth: 'reference' }], + ]); +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/initialize.ts b/src/plugins/discover/public/application/components/hits_counter/index.ts similarity index 87% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/initialize.ts rename to src/plugins/discover/public/application/components/hits_counter/index.ts index a4bc3cf17026c3..58e7a9eda7f51a 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/initialize.ts +++ b/src/plugins/discover/public/application/components/hits_counter/index.ts @@ -17,4 +17,5 @@ * under the License. */ -import './np_ready/public/legacy'; +export { HitsCounter } from './hits_counter'; +export { createHitsCounterDirective } from './hits_counter_directive'; diff --git a/src/plugins/discover/public/application/components/sidebar/_index.scss b/src/plugins/discover/public/application/components/sidebar/_index.scss deleted file mode 100644 index 17b0a6c9cfe4e6..00000000000000 --- a/src/plugins/discover/public/application/components/sidebar/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './_sidebar'; diff --git a/src/plugins/discover/public/application/components/sidebar/_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/_sidebar.scss rename to src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 74d1347b1694cb..56597dd31e572c 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiTitle } from '@elastic/eui'; diff --git a/src/plugins/discover/public/application/embeddable/_index.scss b/src/plugins/discover/public/application/embeddable/_index.scss deleted file mode 100644 index 6d64040e9e7a31..00000000000000 --- a/src/plugins/discover/public/application/embeddable/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ - -@import 'embeddables'; diff --git a/src/plugins/discover/public/application/embeddable/_embeddables.scss b/src/plugins/discover/public/application/embeddable/search_embeddable.scss similarity index 100% rename from src/plugins/discover/public/application/embeddable/_embeddables.scss rename to src/plugins/discover/public/application/embeddable/search_embeddable.scss diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index b650672ccaea71..2f8ac40bdf52c0 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import './search_embeddable.scss'; import angular from 'angular'; import _ from 'lodash'; import * as Rx from 'rxjs'; diff --git a/src/plugins/discover/public/application/helpers/format_number_with_commas.ts b/src/plugins/discover/public/application/helpers/format_number_with_commas.ts new file mode 100644 index 00000000000000..01a010d823d5f3 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/format_number_with_commas.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const COMMA_SEPARATOR_RE = /(\d)(?=(\d{3})+(?!\d))/g; + +/** + * Converts a number to a string and adds commas + * as thousands separators + */ +export const formatNumWithCommas = (input: number) => + String(input).replace(COMMA_SEPARATOR_RE, '$1,'); diff --git a/src/plugins/discover/public/application/helpers/index.ts b/src/plugins/discover/public/application/helpers/index.ts index 7196c96989e97d..3555d24924e806 100644 --- a/src/plugins/discover/public/application/helpers/index.ts +++ b/src/plugins/discover/public/application/helpers/index.ts @@ -18,3 +18,4 @@ */ export { shortenDottedString } from './shorten_dotted_string'; +export { formatNumWithCommas } from './format_number_with_commas'; diff --git a/src/plugins/discover/public/application/index.scss b/src/plugins/discover/public/application/index.scss index 0de036b1e17074..aaec7ab387e966 100644 --- a/src/plugins/discover/public/application/index.scss +++ b/src/plugins/discover/public/application/index.scss @@ -10,6 +10,4 @@ // monChart__legend--small // monChart__legend-isLoading -@import 'components/index'; @import 'angular/index'; -@import 'embeddable/index'; diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index e7813c43383f93..8c3f4f030688ce 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -57,6 +57,7 @@ import { createTopNavHelper, } from '../../kibana_legacy/public'; import { createDiscoverSidebarDirective } from './application/components/sidebar'; +import { createHitsCounterDirective } from '././application/components/hits_counter'; import { DiscoverStartPlugins } from './plugin'; /** @@ -151,6 +152,7 @@ export function initializeInnerAngularModule( .directive('fixedScroll', FixedScrollProvider) .directive('renderComplete', createRenderCompleteDirective) .directive('discoverSidebar', createDiscoverSidebarDirective) + .directive('hitsCounter', createHitsCounterDirective) .service('debounce', ['$timeout', DebounceProviderTimeout]); } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 282b0f05891e02..3894d6fbed382c 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -18,6 +18,7 @@ */ import * as React from 'react'; +import { EuiFlyout } from '@elastic/eui'; import { AddPanelFlyout } from './add_panel_flyout'; import { ContactCardEmbeddableFactory, @@ -75,6 +76,9 @@ test('createNewEmbeddable() add embeddable to container', async () => { /> ) as ReactWrapper; + // https://github.com/elastic/kibana/issues/64789 + expect(component.exists(EuiFlyout)).toBe(false); + expect(Object.values(container.getInput().panels).length).toBe(0); component.instance().createNewEmbeddable(CONTACT_CARD_EMBEDDABLE); await new Promise(r => setTimeout(r, 1)); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 5bf3f69a95c303..4c23916675e8ff 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -21,13 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { ReactElement } from 'react'; import { CoreSetup } from 'src/core/public'; -import { - EuiContextMenuItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, -} from '@elastic/eui'; +import { EuiContextMenuItem, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { IContainer } from '../../../../containers'; @@ -152,7 +146,7 @@ export class AddPanelFlyout extends React.Component { ); return ( - + <>

@@ -161,7 +155,7 @@ export class AddPanelFlyout extends React.Component { {savedObjectsFinder} - + ); } } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index a452e07b515771..867092b78ef7a8 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -55,7 +55,8 @@ export async function openAddPanelFlyout(options: { /> ), { - 'data-test-subj': 'addPanelFlyout', + 'data-test-subj': 'dashboardAddPanel', + ownFocus: true, } ); } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 35a10ed848e838..bb2eb52f9df72a 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -62,16 +62,34 @@ function renderNotifications( notifications: Array>, embeddable: IEmbeddable ) { - return notifications.map(notification => ( - notification.execute({ embeddable })} - > - {notification.getDisplayName({ embeddable })} - - )); + return notifications.map(notification => { + const context = { embeddable }; + + let badge = ( + notification.execute(context)} + > + {notification.getDisplayName(context)} + + ); + + if (notification.getDisplayNameTooltip) { + const tooltip = notification.getDisplayNameTooltip(context); + + if (tooltip) { + badge = ( + + {badge} + + ); + } + } + + return badge; + }); } function renderTooltip(description: string) { diff --git a/src/plugins/home/public/application/components/sample_data/index.tsx b/src/plugins/home/public/application/components/sample_data/index.tsx index 381aa49c30d5a1..2a51b48b084696 100644 --- a/src/plugins/home/public/application/components/sample_data/index.tsx +++ b/src/plugins/home/public/application/components/sample_data/index.tsx @@ -42,7 +42,7 @@ interface Props { export function SampleDataCard({ urlBasePath, onDecline, onConfirm }: Props) { return ( } 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 3e16187c443432..b0cc2e2db3cc92 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 d63ea8f7fb4930..fc3cb6094b5eaa 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 bb6e2982f59a08..d8f205dff24e8b 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 00000000000000..67e46d22705835 --- /dev/null +++ b/src/plugins/maps_legacy/config.ts @@ -0,0 +1,43 @@ +/* + * 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'; +import { configSchema as regionmapSchema } from '../region_map/config'; + +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 d66be2b156bb92..cd503883164ace 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 83b5359362e4ce..1002a8e9eedc89 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 17cecab9f74596..a7f5427909334a 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,16 @@ import { // @ts-ignore import { mapTooltipProvider } from './tooltip_provider'; -export function plugin() { - return new MapsLegacyPlugin(); +export interface MapsLegacyConfigType { + regionmap: any; + emsTileLayerId: string; + includeElasticMapsService: boolean; + proxyElasticMapsServiceInMaps: boolean; + tilemap: any; +} + +export function plugin(initializerContext: PluginInitializerContext) { + return new MapsLegacyPlugin(initializerContext); } /** @public */ @@ -59,6 +70,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/test/plugin_functional/plugins/core_provider_plugin/index.ts b/src/plugins/maps_legacy/public/leaflet.js similarity index 59% rename from test/plugin_functional/plugins/core_provider_plugin/index.ts rename to src/plugins/maps_legacy/public/leaflet.js index 01f3a67c6b5541..e36da2c52b8c51 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/index.ts +++ b/src/plugins/maps_legacy/public/leaflet.js @@ -17,20 +17,20 @@ * under the License. */ -import { resolve } from 'path'; -import { Legacy } from '../../../../kibana'; +export let L; -// 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')], - }, - }; +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; - return new kibana.Plugin(config); + 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 c7cec1b14159aa..85dafc318db8d0 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 8e3a0648e99d4b..437b78a3c3472c 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 acc7655a5e2636..78c2498b9ee900 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 dd8d4c6e9311e6..18f58189fc6077 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/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/src/plugins/region_map/config.ts similarity index 51% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts rename to src/plugins/region_map/config.ts index a7cd313038d69c..a721a76ca0a829 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts +++ b/src/plugins/region_map/config.ts @@ -17,23 +17,30 @@ * under the License. */ -import { PluginInitializerContext } from 'src/core/public'; -import { npSetup, npStart } from 'ui/new_platform'; +import { schema, TypeOf } from '@kbn/config-schema'; -import { plugin } from './np_ready'; +export const configSchema = 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: [] } + ), +}); -// 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); +export type ConfigSchema = TypeOf; diff --git a/src/plugins/region_map/kibana.json b/src/plugins/region_map/kibana.json new file mode 100644 index 00000000000000..3a6f64e92bcba6 --- /dev/null +++ b/src/plugins/region_map/kibana.json @@ -0,0 +1,14 @@ +{ + "id": "regionMap", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["map", "regionmap"], + "ui": true, + "server": true, + "requiredPlugins": [ + "visualizations", + "expressions", + "mapsLegacy", + "data" + ] +} diff --git a/src/legacy/core_plugins/region_map/package.json b/src/plugins/region_map/package.json similarity index 100% rename from src/legacy/core_plugins/region_map/package.json rename to src/plugins/region_map/package.json diff --git a/src/legacy/core_plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap b/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap similarity index 100% rename from src/legacy/core_plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap rename to src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap diff --git a/src/legacy/core_plugins/region_map/public/__tests__/aftercolorchange.png b/src/plugins/region_map/public/__tests__/aftercolorchange.png similarity index 100% rename from src/legacy/core_plugins/region_map/public/__tests__/aftercolorchange.png rename to src/plugins/region_map/public/__tests__/aftercolorchange.png diff --git a/src/legacy/core_plugins/region_map/public/__tests__/afterdatachange.png b/src/plugins/region_map/public/__tests__/afterdatachange.png similarity index 100% rename from src/legacy/core_plugins/region_map/public/__tests__/afterdatachange.png rename to src/plugins/region_map/public/__tests__/afterdatachange.png diff --git a/src/legacy/core_plugins/region_map/public/__tests__/afterdatachangeandresize.png b/src/plugins/region_map/public/__tests__/afterdatachangeandresize.png similarity index 100% rename from src/legacy/core_plugins/region_map/public/__tests__/afterdatachangeandresize.png rename to src/plugins/region_map/public/__tests__/afterdatachangeandresize.png diff --git a/src/legacy/core_plugins/region_map/public/__tests__/afterresize.png b/src/plugins/region_map/public/__tests__/afterresize.png similarity index 100% rename from src/legacy/core_plugins/region_map/public/__tests__/afterresize.png rename to src/plugins/region_map/public/__tests__/afterresize.png diff --git a/src/legacy/core_plugins/region_map/public/__tests__/changestartup.png b/src/plugins/region_map/public/__tests__/changestartup.png similarity index 100% rename from src/legacy/core_plugins/region_map/public/__tests__/changestartup.png rename to src/plugins/region_map/public/__tests__/changestartup.png diff --git a/src/legacy/core_plugins/region_map/public/__tests__/initial.png b/src/plugins/region_map/public/__tests__/initial.png similarity index 100% rename from src/legacy/core_plugins/region_map/public/__tests__/initial.png rename to src/plugins/region_map/public/__tests__/initial.png diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/plugins/region_map/public/__tests__/region_map_visualization.js similarity index 88% rename from src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js rename to src/plugins/region_map/public/__tests__/region_map_visualization.js index 87592cf4e750e4..cefef98fae8148 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/plugins/region_map/public/__tests__/region_map_visualization.js @@ -20,21 +20,22 @@ 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'; // 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 initialPng from './initial.png'; import toiso3Png from './toiso3.png'; @@ -47,14 +48,14 @@ import changestartupPng from './changestartup.png'; import { createRegionMapVisualization } from '../region_map_visualization'; import { createRegionMapTypeDefinition } from '../region_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'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; +import { setInjectedVarFunc } from '../../../maps_legacy/public/kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; -import { getBaseMapsVis } from '../../../../../plugins/maps_legacy/public'; +import { ServiceSettings } from '../../../maps_legacy/public/map/service_settings'; +import { getBaseMapsVis } from '../../../maps_legacy/public'; const THRESHOLD = 0.45; const PIXEL_DIFF = 96; @@ -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/__tests__/toiso3.png b/src/plugins/region_map/public/__tests__/toiso3.png similarity index 100% rename from src/legacy/core_plugins/region_map/public/__tests__/toiso3.png rename to src/plugins/region_map/public/__tests__/toiso3.png diff --git a/src/legacy/core_plugins/region_map/public/__tests__/world.json b/src/plugins/region_map/public/__tests__/world.json similarity index 100% rename from src/legacy/core_plugins/region_map/public/__tests__/world.json rename to src/plugins/region_map/public/__tests__/world.json diff --git a/src/legacy/core_plugins/region_map/public/choropleth_layer.js b/src/plugins/region_map/public/choropleth_layer.js similarity index 97% rename from src/legacy/core_plugins/region_map/public/choropleth_layer.js rename to src/plugins/region_map/public/choropleth_layer.js index 4ea9cc1f7bfbf1..ddaf2db257fbab 100644 --- a/src/legacy/core_plugins/region_map/public/choropleth_layer.js +++ b/src/plugins/region_map/public/choropleth_layer.js @@ -18,14 +18,13 @@ */ import $ from 'jquery'; -import L from 'leaflet'; import _ from 'lodash'; import d3 from 'd3'; import { i18n } from '@kbn/i18n'; import * as topojson from 'topojson-client'; -import { toastNotifications } from 'ui/notify'; -import { colorUtil, KibanaMapLayer } from '../../../../plugins/maps_legacy/public'; -import { truncatedColorMaps } from '../../../../plugins/charts/public'; +import { getNotifications } from './kibana_services'; +import { colorUtil, KibanaMapLayer } from '../../maps_legacy/public'; +import { truncatedColorMaps } from '../../charts/public'; const EMPTY_STYLE = { weight: 1, @@ -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(); } @@ -181,7 +182,7 @@ CORS configuration of the server permits requests from the Kibana application on ); } - toastNotifications.addDanger({ + getNotifications().toasts.addDanger({ title: i18n.translate( 'regionMap.choroplethLayer.downloadingVectorDataErrorMessageTitle', { @@ -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/region_map/public/components/region_map_options.tsx b/src/plugins/region_map/public/components/region_map_options.tsx similarity index 95% rename from src/legacy/core_plugins/region_map/public/components/region_map_options.tsx rename to src/plugins/region_map/public/components/region_map_options.tsx index 5604067433f136..9a6987b9815392 100644 --- a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx +++ b/src/plugins/region_map/public/components/region_map_options.tsx @@ -22,17 +22,9 @@ import { EuiIcon, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elast import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { - FileLayerField, - VectorLayer, - IServiceSettings, -} from '../../../../../plugins/maps_legacy/public'; -import { - NumberInputOption, - SelectOption, - SwitchOption, -} from '../../../../../plugins/charts/public'; -import { RegionMapVisParams, WmsOptions } from '../../../../../plugins/maps_legacy/public'; +import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_legacy/public'; +import { NumberInputOption, SelectOption, SwitchOption } from '../../../charts/public'; +import { RegionMapVisParams, WmsOptions } from '../../../maps_legacy/public'; const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ text: name, diff --git a/src/legacy/core_plugins/region_map/public/index.ts b/src/plugins/region_map/public/index.ts similarity index 86% rename from src/legacy/core_plugins/region_map/public/index.ts rename to src/plugins/region_map/public/index.ts index a29f5aa2470269..3f920ad16683a7 100644 --- a/src/legacy/core_plugins/region_map/public/index.ts +++ b/src/plugins/region_map/public/index.ts @@ -17,9 +17,14 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializerContext } from 'kibana/public'; import { RegionMapPlugin as Plugin } from './plugin'; +export interface RegionMapsConfigType { + includeElasticMapsService: boolean; + layers: any[]; +} + export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } diff --git a/src/plugins/region_map/public/kibana_services.ts b/src/plugins/region_map/public/kibana_services.ts new file mode 100644 index 00000000000000..1ef58c69c5bef4 --- /dev/null +++ b/src/plugins/region_map/public/kibana_services.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { NotificationsStart } from 'kibana/public'; +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('data.fieldFormats'); + +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts similarity index 56% rename from src/legacy/core_plugins/region_map/public/plugin.ts rename to src/plugins/region_map/public/plugin.ts index 08a73517dc13b3..09a13fbe9774e7 100644 --- a/src/legacy/core_plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -22,18 +22,19 @@ import { Plugin, PluginInitializerContext, IUiSettingsClient, -} from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; + NotificationsStart, +} from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; // @ts-ignore import { createRegionMapFn } from './region_map_fn'; // @ts-ignore import { createRegionMapTypeDefinition } from './region_map_type'; -import { - getBaseMapsVis, - IServiceSettings, - MapsLegacyPluginSetup, -} from '../../../../plugins/maps_legacy/public'; +import { getBaseMapsVis, IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { setFormatService, setNotifications } from './kibana_services'; +import { DataPublicPluginStart } from '../../data/public'; +import { RegionMapsConfigType } from './index'; +import { ConfigSchema } from '../../maps_legacy/config'; /** @private */ interface RegionMapVisualizationDependencies { @@ -50,27 +51,46 @@ export interface RegionMapPluginSetupDependencies { mapsLegacy: MapsLegacyPluginSetup; } +/** @internal */ +export interface RegionMapPluginStartDependencies { + data: DataPublicPluginStart; + notifications: NotificationsStart; +} + /** @internal */ export interface RegionMapsConfig { includeElasticMapsService: boolean; layers: any[]; } +export interface RegionMapPluginSetup { + config: any; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RegionMapPluginStart {} + /** @internal */ -export class RegionMapPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; +export class RegionMapPlugin implements Plugin { + readonly _initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; + this._initializerContext = initializerContext; } public async setup( core: CoreSetup, { expressions, visualizations, mapsLegacy }: RegionMapPluginSetupDependencies ) { + const config = { + ...this._initializerContext.config.get(), + // The maps legacy plugin updates the regionmap config directly in service_settings, + // future work on how configurations across the different plugins are organized would + // ideally constrain regionmap config updates to occur only from this plugin + ...mapsLegacy.config.regionmap, + }; const visualizationDependencies: Readonly = { uiSettings: core.uiSettings, - regionmapsConfig: core.injectedMetadata.getInjectedVar('regionmap') as RegionMapsConfig, + regionmapsConfig: config as RegionMapsConfig, serviceSettings: mapsLegacy.serviceSettings, BaseMapsVisualization: getBaseMapsVis(core, mapsLegacy.serviceSettings), }; @@ -80,9 +100,15 @@ export class RegionMapPlugin implements Plugin, void> { visualizations.createBaseVisualization( createRegionMapTypeDefinition(visualizationDependencies) ); + + return { + config, + }; } - public start(core: CoreStart) { - // nothing to do here yet + // @ts-ignore + public start(core: CoreStart, { data }: RegionMapPluginStartDependencies) { + setFormatService(data.fieldFormats); + setNotifications(core.notifications); } } diff --git a/src/legacy/core_plugins/region_map/public/region_map_fn.js b/src/plugins/region_map/public/region_map_fn.js similarity index 100% rename from src/legacy/core_plugins/region_map/public/region_map_fn.js rename to src/plugins/region_map/public/region_map_fn.js diff --git a/src/legacy/core_plugins/region_map/public/region_map_fn.test.js b/src/plugins/region_map/public/region_map_fn.test.js similarity index 92% rename from src/legacy/core_plugins/region_map/public/region_map_fn.test.js rename to src/plugins/region_map/public/region_map_fn.test.js index 07b4e33b85e279..684cc5e897df4e 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_fn.test.js +++ b/src/plugins/region_map/public/region_map_fn.test.js @@ -18,11 +18,9 @@ */ // 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 { createRegionMapFn } from './region_map_fn'; -jest.mock('ui/new_platform'); - describe('interpreter/functions#regionmap', () => { const fn = functionWrapper(createRegionMapFn()); const context = { diff --git a/src/legacy/core_plugins/region_map/public/region_map_type.js b/src/plugins/region_map/public/region_map_type.js similarity index 95% rename from src/legacy/core_plugins/region_map/public/region_map_type.js rename to src/plugins/region_map/public/region_map_type.js index b7ed14ed3706eb..d29360a9589ab5 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_type.js +++ b/src/plugins/region_map/public/region_map_type.js @@ -21,9 +21,9 @@ import { i18n } from '@kbn/i18n'; import { mapToLayerWithId } from './util'; import { createRegionMapVisualization } from './region_map_visualization'; import { RegionMapOptions } from './components/region_map_options'; -import { truncatedColorSchemas } from '../../../../plugins/charts/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; -import { ORIGIN } from '../../../../plugins/maps_legacy/public'; +import { truncatedColorSchemas } from '../../charts/public'; +import { Schemas } from '../../vis_default_editor/public'; +import { ORIGIN } from '../../maps_legacy/public'; export function createRegionMapTypeDefinition(dependencies) { const { uiSettings, regionmapsConfig, serviceSettings } = dependencies; diff --git a/src/legacy/core_plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js similarity index 93% rename from src/legacy/core_plugins/region_map/public/region_map_visualization.js rename to src/plugins/region_map/public/region_map_visualization.js index 5dbc1ecad277f1..ed6a3ed2c10c8b 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -19,11 +19,10 @@ import { i18n } from '@kbn/i18n'; import ChoroplethLayer from './choropleth_layer'; -import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { toastNotifications } from 'ui/notify'; -import { truncatedColorMaps } from '../../../../plugins/charts/public'; +import { getFormatService, getNotifications } from './kibana_services'; +import { truncatedColorMaps } from '../../charts/public'; import { tooltipFormatter } from './tooltip_formatter'; -import { mapTooltipProvider } from '../../../../plugins/maps_legacy/public'; +import { mapTooltipProvider } from '../../maps_legacy/public'; export function createRegionMapVisualization({ serviceSettings, @@ -75,7 +74,7 @@ export function createRegionMapVisualization({ results ); - const metricFieldFormatter = getFormat(this._params.metric.format); + const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format); this._choroplethLayer.setMetrics(results, metricFieldFormatter, valueColumn.name); if (termColumn && valueColumn) { @@ -108,7 +107,7 @@ export function createRegionMapVisualization({ this._params.showAllShapes ); - const metricFieldFormatter = getFormat(this._params.metric.format); + const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format); this._choroplethLayer.setJoinField(visParams.selectedJoinField.name); this._choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema].value); @@ -177,7 +176,7 @@ export function createRegionMapVisualization({ const shouldShowWarning = this._params.isDisplayWarning && uiSettings.get('visualization:regionmap:showWarnings'); if (event.mismatches.length > 0 && shouldShowWarning) { - toastNotifications.addWarning({ + getNotifications().toasts.addWarning({ title: i18n.translate('regionMap.visualization.unableToShowMismatchesWarningTitle', { defaultMessage: 'Unable to show {mismatchesLength} {oneMismatch, plural, one {result} other {results}} on map', diff --git a/src/legacy/core_plugins/region_map/public/tooltip_formatter.js b/src/plugins/region_map/public/tooltip_formatter.js similarity index 100% rename from src/legacy/core_plugins/region_map/public/tooltip_formatter.js rename to src/plugins/region_map/public/tooltip_formatter.js diff --git a/src/legacy/core_plugins/region_map/public/util.ts b/src/plugins/region_map/public/util.ts similarity index 86% rename from src/legacy/core_plugins/region_map/public/util.ts rename to src/plugins/region_map/public/util.ts index b4e0dcd5f35102..0160a32e815226 100644 --- a/src/legacy/core_plugins/region_map/public/util.ts +++ b/src/plugins/region_map/public/util.ts @@ -17,8 +17,8 @@ * under the License. */ -import { FileLayer, VectorLayer } from '../../../../plugins/maps_legacy/public'; -import { ORIGIN } from '../../../../plugins/maps_legacy/public'; +import { FileLayer, VectorLayer } from '../../maps_legacy/public'; +import { ORIGIN } from '../../maps_legacy/public'; export const mapToLayerWithId = (prefix: string, layer: FileLayer): VectorLayer => ({ ...layer, diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts b/src/plugins/region_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/region_map/server/index.ts index d7a764b581c01d..e2c544d2d0ba65 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts +++ b/src/plugins/region_map/server/index.ts @@ -17,12 +17,18 @@ * 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: { + includeElasticMapsService: true, + layers: true, + }, + schema: configSchema, }; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/share/public/url_generators/url_generator_service.test.ts b/src/plugins/share/public/url_generators/url_generator_service.test.ts index 4a377db033762f..d256dcf5f7aa0a 100644 --- a/src/plugins/share/public/url_generators/url_generator_service.test.ts +++ b/src/plugins/share/public/url_generators/url_generator_service.test.ts @@ -30,11 +30,11 @@ test('Asking for a generator that does not exist throws an error', () => { }); test('Registering and retrieving a generator', async () => { - setup.registerUrlGenerator({ + const generator = setup.registerUrlGenerator({ id: 'TEST_GENERATOR', createUrl: () => Promise.resolve('myurl'), }); - const generator = start.getUrlGenerator('TEST_GENERATOR'); + expect(generator).toMatchInlineSnapshot(` Object { "createUrl": [Function], @@ -47,6 +47,20 @@ test('Registering and retrieving a generator', async () => { new Error('You cannot call migrate on a non-deprecated generator.') ); expect(await generator.createUrl({})).toBe('myurl'); + + const retrievedGenerator = start.getUrlGenerator('TEST_GENERATOR'); + expect(retrievedGenerator).toMatchInlineSnapshot(` + Object { + "createUrl": [Function], + "id": "TEST_GENERATOR", + "isDeprecated": false, + "migrate": [Function], + } + `); + await expect(generator.migrate({})).rejects.toEqual( + new Error('You cannot call migrate on a non-deprecated generator.') + ); + expect(await generator.createUrl({})).toBe('myurl'); }); test('Registering a generator with a createUrl function that is deprecated throws an error', () => { diff --git a/src/plugins/share/public/url_generators/url_generator_service.ts b/src/plugins/share/public/url_generators/url_generator_service.ts index 332750671cee39..13c1b94acdd07e 100644 --- a/src/plugins/share/public/url_generators/url_generator_service.ts +++ b/src/plugins/share/public/url_generators/url_generator_service.ts @@ -28,7 +28,9 @@ export interface UrlGeneratorsStart { } export interface UrlGeneratorsSetup { - registerUrlGenerator: (generator: UrlGeneratorsDefinition) => void; + registerUrlGenerator: ( + generator: UrlGeneratorsDefinition + ) => UrlGeneratorContract; } export class UrlGeneratorsService implements Plugin { @@ -43,10 +45,9 @@ export class UrlGeneratorsService implements Plugin( generatorOptions: UrlGeneratorsDefinition ) => { - this.urlGenerators.set( - generatorOptions.id, - new UrlGeneratorInternal(generatorOptions, this.getUrlGenerator) - ); + const generator = new UrlGeneratorInternal(generatorOptions, this.getUrlGenerator); + this.urlGenerators.set(generatorOptions.id, generator); + return generator.getPublicContract(); }, }; return setup; diff --git a/src/plugins/tile_map/config.ts b/src/plugins/tile_map/config.ts new file mode 100644 index 00000000000000..435e52103d156d --- /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 00000000000000..71ae0bb29d17f1 --- /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 bce2e157ebbc8d..303ce67be71021 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 bdf9cd806eb8b5..a288e78ef00c18 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 1efb0b2f884f85..f7fb4daff63f01 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 f0261483d302d2..dbe64871265b1c 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 767a71225a7d86..4ce500b2da4d22 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 3d0d970e4dc202..d2b9a15a6ad3c2 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 406a50ccde9665..0150f6d2c54c95 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 0ae26bfcf032b1..ed9dbccbfbcdeb 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 f39de6ca7d1797..028d3de515ae70 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 e21d753f7001ab..745d0422856c68 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 aa1460a7e2890a..e55f7189929dfc 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/src/plugins/tile_map/public/services.ts b/src/plugins/tile_map/public/services.ts new file mode 100644 index 00000000000000..fd075a041ac9bf --- /dev/null +++ b/src/plugins/tile_map/public/services.ts @@ -0,0 +1,29 @@ +/* + * 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 { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('vislib data.fieldFormats'); + +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 5ad4a2c33db256..5f43077bcb24b7 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 ca6a586d220080..aa0160f3f5a9d8 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 6a7bda5e188831..f96c7291b34cf2 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 6da37f4c5ef86c..8fa12c9f9dbbe6 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/src/plugins/tile_map/server/index.ts b/src/plugins/tile_map/server/index.ts new file mode 100644 index 00000000000000..3381553fe93644 --- /dev/null +++ b/src/plugins/tile_map/server/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { PluginConfigDescriptor } from 'kibana/server'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + url: true, + deprecated: true, + options: true, + }, + schema: configSchema, +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index e3504c7c5d3013..aba1e22fe09ee2 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -48,6 +48,11 @@ export class ActionInternal return this.definition.getDisplayName(context); } + public getDisplayNameTooltip(context: Context): string { + if (!this.definition.getDisplayNameTooltip) return ''; + return this.definition.getDisplayNameTooltip(context); + } + public async isCompatible(context: Context): Promise { if (!this.definition.isCompatible) return true; return await this.definition.isCompatible(context); diff --git a/src/plugins/ui_actions/public/util/presentable.ts b/src/plugins/ui_actions/public/util/presentable.ts index f43b776e746589..57070f7673f61c 100644 --- a/src/plugins/ui_actions/public/util/presentable.ts +++ b/src/plugins/ui_actions/public/util/presentable.ts @@ -50,6 +50,12 @@ export interface Presentable { */ getDisplayName(context: Context): string; + /** + * Returns tooltip text which should be displayed when user hovers this object. + * Should return empty string if tooltip should not be displayed. + */ + getDisplayNameTooltip(context: Context): string; + /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or diff --git a/src/plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx index 68348d5ef10604..837d4785359361 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -147,5 +147,6 @@ function TableOptions({ ); } - -export { TableOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TableOptions as default }; diff --git a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx new file mode 100644 index 00000000000000..ca273aa771ef17 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { TableVisParams } from '../types'; + +const TableOptionsComponent = lazy(() => import('./table_vis_options')); + +export const TableOptions = (props: VisOptionsProps) => ( + }> + + +); diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index 26e5ac8cfd71ab..c3bc72497007ea 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -24,7 +24,7 @@ import { Vis } from '../../visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; // @ts-ignore import tableVisTemplate from './table_vis.html'; -import { TableOptions } from './components/table_vis_options'; +import { TableOptions } from './components/table_vis_options_lazy'; import { getTableVisualizationControllerClass } from './vis_controller'; export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js index ed17fceeda5401..4aa8856836fc6e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js @@ -64,7 +64,7 @@ class MarkdownPanelConfigUi extends Component { const lessSrc = `#markdown-${model.id} { ${value} }`; - lessC.render(lessSrc, { compress: true }, (e, output) => { + lessC.render(lessSrc, { compress: true, javascriptEnabled: false }, (e, output) => { const parts = { markdown_less: value }; if (output) { parts.markdown_css = output.css; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js index 98890f7462917a..87e9734ea10540 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js @@ -21,7 +21,8 @@ import { buildProcessorFunction } from '../build_processor_function'; import { processors } from '../response_processors/table'; import { getLastValue } from '../../../../common/get_last_value'; import regression from 'regression'; -import { first, get, set } from 'lodash'; +import { first, get } from 'lodash'; +import { overwrite } from '../helpers'; import { getActiveSeries } from '../helpers/get_active_series'; export function processBucket(panel) { @@ -35,7 +36,7 @@ export function processBucket(panel) { const timeseries = { buckets: get(bucket, `${series.id}.buckets`), }; - set(bucket, series.id, { meta, timeseries }); + overwrite(bucket, series.id, { meta, timeseries }); } const processor = buildProcessorFunction(processors, bucket, panel, series); diff --git a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts index fa4427fbb8c12b..1d565e69a801cb 100644 --- a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts +++ b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts @@ -17,59 +17,65 @@ * under the License. */ -import Joi from 'joi'; -const stringOptionalNullable = Joi.string() - .allow('', null) - .optional(); -const stringRequired = Joi.string() - .allow('') - .required(); -const arrayNullable = Joi.array().allow(null); -const numberIntegerOptional = Joi.number() - .integer() - .optional(); -const numberIntegerRequired = Joi.number() - .integer() - .required(); -const numberOptional = Joi.number().optional(); -const queryObject = Joi.object({ - language: Joi.string().allow(''), - query: Joi.string().allow(''), +import { schema } from '@kbn/config-schema'; +import { TypeOptions } from '@kbn/config-schema/target/types/types'; + +const stringOptionalNullable = schema.maybe(schema.nullable(schema.string())); + +const stringRequired = schema.string(); + +const arrayNullable = schema.arrayOf(schema.nullable(schema.any())); + +const validateInteger: TypeOptions['validate'] = value => { + if (!Number.isInteger(value)) { + return `${value} is not an integer`; + } +}; +const numberIntegerOptional = schema.maybe(schema.number({ validate: validateInteger })); +const numberIntegerRequired = schema.number({ validate: validateInteger }); + +const numberOptional = schema.maybe(schema.number()); + +const queryObject = schema.object({ + language: schema.string(), + query: schema.string(), }); -const stringOrNumberOptionalNullable = Joi.alternatives([stringOptionalNullable, numberOptional]); -const numberOptionalOrEmptyString = Joi.alternatives(numberOptional, Joi.string().valid('')); +const stringOrNumberOptionalNullable = schema.nullable( + schema.oneOf([stringOptionalNullable, numberOptional]) +); +const numberOptionalOrEmptyString = schema.maybe( + schema.oneOf([numberOptional, schema.literal('')]) +); -const annotationsItems = Joi.object({ +const annotationsItems = schema.object({ color: stringOptionalNullable, fields: stringOptionalNullable, - hidden: Joi.boolean().optional(), + hidden: schema.maybe(schema.boolean()), icon: stringOptionalNullable, id: stringOptionalNullable, ignore_global_filters: numberIntegerOptional, ignore_panel_filters: numberIntegerOptional, index_pattern: stringOptionalNullable, - query_string: queryObject.optional(), + query_string: schema.maybe(queryObject), template: stringOptionalNullable, time_field: stringOptionalNullable, }); -const backgroundColorRulesItems = Joi.object({ - value: Joi.number() - .allow(null) - .optional(), +const backgroundColorRulesItems = schema.object({ + value: schema.maybe(schema.nullable(schema.number())), id: stringOptionalNullable, background_color: stringOptionalNullable, color: stringOptionalNullable, }); -const gaugeColorRulesItems = Joi.object({ +const gaugeColorRulesItems = schema.object({ gauge: stringOptionalNullable, text: stringOptionalNullable, id: stringOptionalNullable, operator: stringOptionalNullable, - value: Joi.number(), + value: schema.number(), }); -const metricsItems = Joi.object({ +const metricsItems = schema.object({ field: stringOptionalNullable, id: stringRequired, metric_agg: stringOptionalNullable, @@ -84,50 +90,49 @@ const metricsItems = Joi.object({ beta: numberOptional, gamma: numberOptional, period: numberOptional, - multiplicative: Joi.boolean(), + multiplicative: schema.maybe(schema.boolean()), window: numberOptional, function: stringOptionalNullable, script: stringOptionalNullable, - variables: Joi.array() - .items( - Joi.object({ + variables: schema.maybe( + schema.arrayOf( + schema.object({ field: stringOptionalNullable, id: stringRequired, name: stringOptionalNullable, }) ) - .optional(), - percentiles: Joi.array() - .items( - Joi.object({ + ), + percentiles: schema.maybe( + schema.arrayOf( + schema.object({ id: stringRequired, field: stringOptionalNullable, - mode: Joi.string().allow('line', 'band'), - shade: Joi.alternatives(numberOptional, stringOptionalNullable), - value: Joi.alternatives(numberOptional, stringOptionalNullable), + mode: schema.oneOf([schema.literal('line'), schema.literal('band')]), + shade: schema.oneOf([numberOptional, stringOptionalNullable]), + value: schema.oneOf([numberOptional, stringOptionalNullable]), percentile: stringOptionalNullable, }) ) - .optional(), + ), type: stringRequired, value: stringOptionalNullable, - values: Joi.array() - .items(Joi.string().allow('', null)) - .allow(null) - .optional(), + values: schema.maybe(schema.nullable(schema.arrayOf(schema.nullable(schema.string())))), }); -const splitFiltersItems = Joi.object({ +const splitFiltersItems = schema.object({ id: stringOptionalNullable, color: stringOptionalNullable, - filter: Joi.object({ - language: Joi.string().allow(''), - query: Joi.string().allow(''), - }).optional(), + filter: schema.maybe( + schema.object({ + language: schema.string(), + query: schema.string(), + }) + ), label: stringOptionalNullable, }); -const seriesItems = Joi.object({ +const seriesItems = schema.object({ aggregate_by: stringOptionalNullable, aggregate_function: stringOptionalNullable, axis_position: stringRequired, @@ -135,31 +140,33 @@ const seriesItems = Joi.object({ axis_min: stringOrNumberOptionalNullable, chart_type: stringRequired, color: stringRequired, - color_rules: Joi.array() - .items( - Joi.object({ + color_rules: schema.maybe( + schema.arrayOf( + schema.object({ value: numberOptional, id: stringRequired, text: stringOptionalNullable, operator: stringOptionalNullable, }) ) - .optional(), + ), fill: numberOptionalOrEmptyString, - filter: Joi.alternatives( - Joi.object({ - query: stringRequired, - language: stringOptionalNullable, - }).optional(), - Joi.string().valid('') + filter: schema.maybe( + schema.oneOf([ + schema.object({ + query: stringRequired, + language: stringOptionalNullable, + }), + schema.literal(''), + ]) ), formatter: stringRequired, hide_in_legend: numberIntegerOptional, - hidden: Joi.boolean().optional(), + hidden: schema.maybe(schema.boolean()), id: stringRequired, label: stringOptionalNullable, line_width: numberOptionalOrEmptyString, - metrics: Joi.array().items(metricsItems), + metrics: schema.arrayOf(metricsItems), offset_time: stringOptionalNullable, override_index_pattern: numberOptional, point_size: numberOptionalOrEmptyString, @@ -170,9 +177,7 @@ const seriesItems = Joi.object({ series_interval: stringOptionalNullable, series_drop_last_bucket: numberIntegerOptional, split_color_mode: stringOptionalNullable, - split_filters: Joi.array() - .items(splitFiltersItems) - .optional(), + split_filters: schema.maybe(schema.arrayOf(splitFiltersItems)), split_mode: stringRequired, stacked: stringRequired, steps: numberIntegerOptional, @@ -189,38 +194,34 @@ const seriesItems = Joi.object({ var_name: stringOptionalNullable, }); -export const visPayloadSchema = Joi.object({ +export const visPayloadSchema = schema.object({ filters: arrayNullable, - panels: Joi.array().items( - Joi.object({ - annotations: Joi.array() - .items(annotationsItems) - .optional(), + panels: schema.arrayOf( + schema.object({ + annotations: schema.maybe(schema.arrayOf(annotationsItems)), axis_formatter: stringRequired, axis_position: stringRequired, axis_scale: stringRequired, axis_min: stringOrNumberOptionalNullable, axis_max: stringOrNumberOptionalNullable, - bar_color_rules: arrayNullable.optional(), + bar_color_rules: schema.maybe(arrayNullable), background_color: stringOptionalNullable, - background_color_rules: Joi.array() - .items(backgroundColorRulesItems) - .optional(), + background_color_rules: schema.maybe(schema.arrayOf(backgroundColorRulesItems)), default_index_pattern: stringOptionalNullable, default_timefield: stringOptionalNullable, drilldown_url: stringOptionalNullable, drop_last_bucket: numberIntegerOptional, - filter: Joi.alternatives( - stringOptionalNullable, - Joi.object({ - language: stringOptionalNullable, - query: stringOptionalNullable, - }) + filter: schema.nullable( + schema.oneOf([ + stringOptionalNullable, + schema.object({ + language: stringOptionalNullable, + query: stringOptionalNullable, + }), + ]) ), - gauge_color_rules: Joi.array() - .items(gaugeColorRulesItems) - .optional(), - gauge_width: [stringOptionalNullable, numberOptional], + gauge_color_rules: schema.maybe(schema.arrayOf(gaugeColorRulesItems)), + gauge_width: schema.nullable(schema.oneOf([stringOptionalNullable, numberOptional])), gauge_inner_color: stringOptionalNullable, gauge_inner_width: stringOrNumberOptionalNullable, gauge_style: stringOptionalNullable, @@ -230,7 +231,7 @@ export const visPayloadSchema = Joi.object({ ignore_global_filter: numberOptional, index_pattern: stringRequired, interval: stringRequired, - isModelInvalid: Joi.boolean().optional(), + isModelInvalid: schema.maybe(schema.boolean()), legend_position: stringOptionalNullable, markdown: stringOptionalNullable, markdown_scrollbars: numberIntegerOptional, @@ -242,9 +243,7 @@ export const visPayloadSchema = Joi.object({ pivot_label: stringOptionalNullable, pivot_type: stringOptionalNullable, pivot_rows: stringOptionalNullable, - series: Joi.array() - .items(seriesItems) - .required(), + series: schema.arrayOf(seriesItems), show_grid: numberIntegerRequired, show_legend: numberIntegerRequired, time_field: stringOptionalNullable, @@ -253,22 +252,19 @@ export const visPayloadSchema = Joi.object({ }) ), // general - query: Joi.array() - .items(queryObject) - .allow(null) - .required(), - state: Joi.object({ - sort: Joi.object({ - column: stringRequired, - order: Joi.string() - .valid(['asc', 'desc']) - .required(), - }).optional(), - }).required(), - savedObjectId: Joi.string().optional(), - timerange: Joi.object({ + query: schema.nullable(schema.arrayOf(queryObject)), + state: schema.object({ + sort: schema.maybe( + schema.object({ + column: stringRequired, + order: schema.oneOf([schema.literal('asc'), schema.literal('desc')]), + }) + ), + }), + savedObjectId: schema.maybe(schema.string()), + timerange: schema.object({ timezone: stringRequired, min: stringRequired, max: stringRequired, - }).required(), + }), }); diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 9abbc4ad617dc2..744020b5838827 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -38,16 +38,18 @@ export const visDataRoutes = ( }, }, async (requestContext, request, response) => { - const { error: validationError } = visPayloadSchema.validate(request.body); - if (validationError) { + try { + visPayloadSchema.validate(request.body); + } catch (error) { logFailedValidation(); const savedObjectId = (typeof request.body === 'object' && (request.body as any).savedObjectId) || 'unavailable'; framework.logger.warn( - `Request validation error: ${validationError.message} (saved object id: ${savedObjectId}). This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/kibana/issues/new?template=Bug_report.md` + `Request validation error: ${error.message} (saved object id: ${savedObjectId}). This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/kibana/issues/new?template=Bug_report.md` ); } + try { const results = await getVisData( requestContext, diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index b52dcfbd914f9a..1bce7ac92e5640 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 f81f87d7ad2e17..f2fddb41cf72b2 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 8e4009eab84884..bc1cb4e4734c7d 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 895d496a896aa3..4cd3eea503cb03 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/src/test_utils/public/stub_index_pattern.js b/src/test_utils/public/stub_index_pattern.js index 98ada2471e1ec9..29fb4c20f692e7 100644 --- a/src/test_utils/public/stub_index_pattern.js +++ b/src/test_utils/public/stub_index_pattern.js @@ -65,7 +65,6 @@ export default function StubIndexPattern(pattern, getConfig, timeField, fields, this.getSourceFiltering = sinon.stub(); this.metaFields = ['_id', '_type', '_source']; this.fieldFormatMap = {}; - this.routes = indexPatterns.getRoutes(); this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); this.flattenHit = indexPatterns.flattenHitWrapper(this, this.metaFields); diff --git a/test/api_integration/apis/saved_objects/bulk_create.js b/test/api_integration/apis/saved_objects/bulk_create.js index 2d77fdf266793a..a0d2717555150f 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.js +++ b/test/api_integration/apis/saved_objects/bulk_create.js @@ -72,11 +72,26 @@ export default function({ getService }) { attributes: { title: 'A great new dashboard', }, + migrationVersion: { + dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, + }, references: [], }, ], }); })); + + it('should not return raw id when object id is unspecified', async () => + await supertest + .post(`/api/saved_objects/_bulk_create`) + // eslint-disable-next-line no-unused-vars + .send(BULK_REQUESTS.map(({ id, ...rest }) => rest)) + .expect(200) + .then(resp => { + resp.body.saved_objects.map(({ id }) => + expect(id).not.match(/visualization|dashboard/) + ); + })); }); describe('without kibana index', () => { @@ -106,6 +121,9 @@ export default function({ getService }) { title: 'An existing visualization', }, references: [], + migrationVersion: { + visualization: resp.body.saved_objects[0].migrationVersion.visualization, + }, }, { type: 'dashboard', @@ -116,6 +134,9 @@ export default function({ getService }) { title: 'A great new dashboard', }, references: [], + migrationVersion: { + dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, + }, }, ], }); diff --git a/test/common/services/elasticsearch.ts b/test/common/services/elasticsearch.ts index 63c4bfeeb4ce77..0436dc901292d5 100644 --- a/test/common/services/elasticsearch.ts +++ b/test/common/services/elasticsearch.ts @@ -18,8 +18,9 @@ */ import { format as formatUrl } from 'url'; - +import fs from 'fs'; import { Client } from '@elastic/elasticsearch'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -27,6 +28,9 @@ export function ElasticsearchProvider({ getService }: FtrProviderContext) { const config = getService('config'); return new Client({ + ssl: { + ca: fs.readFileSync(CA_CERT_PATH, 'utf-8'), + }, nodes: [formatUrl(config.get('servers.elasticsearch'))], requestTimeout: config.get('timeouts.esRequestTimeout'), }); diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index 20e69ef8345c6f..0f63510dce431c 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -35,7 +35,7 @@ export default function({ getService, getPageObjects }) { describe('discover histogram', function describeIndexTests() { before(async function() { log.debug('load kibana index with default index pattern'); - await PageObjects.common.navigateToApp('home'); + await PageObjects.common.navigateToApp('settings'); await security.testUser.setRoles([ 'kibana_admin', 'test_logstash_reader', diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 3a3d6b93e166bf..b0a572d9a54f99 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -58,6 +58,7 @@ export default function({ getService, getPageObjects }) { }); it('should create shakespeare index pattern', async function() { + await PageObjects.common.navigateToApp('settings'); log.debug('Create shakespeare index pattern'); await PageObjects.settings.createIndexPattern('shakespeare', null); const patternName = await PageObjects.settings.getIndexPageHeading(); diff --git a/test/functional/apps/timelion/index.js b/test/functional/apps/timelion/index.js index 3b5167addf4e6f..021fa243978506 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/common_page.ts b/test/functional/page_objects/common_page.ts index 93debdcc37f0ab..4a7570049ded73 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -111,7 +111,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo await browser.get(appUrl); } else { log.debug(`navigateToUrl ${appUrl}`); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); // accept alert if it pops up const alert = await browser.getAlert(); await alert?.accept(); @@ -242,7 +242,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo let lastUrl = await retry.try(async () => { // since we're using hash URLs, always reload first to force re-render log.debug('navigate to: ' + appUrl); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); // accept alert if it pops up const alert = await browser.getAlert(); await alert?.accept(); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 81d22838d1e8b7..b8069b31257d30 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(); @@ -206,17 +206,15 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async getFieldsTabCount() { return retry.try(async () => { - const indexedFieldsTab = await find.byCssSelector('#indexedFields .euiTab__content'); - const text = await indexedFieldsTab.getVisibleText(); - return text.split(/[()]/)[1]; + const text = await testSubjects.getVisibleText('tab-indexedFields'); + return text.split(' ')[1].replace(/\((.*)\)/, '$1'); }); } async getScriptedFieldsTabCount() { return await retry.try(async () => { - const scriptedFieldsTab = await find.byCssSelector('#scriptedFields .euiTab__content'); - const text = await scriptedFieldsTab.getVisibleText(); - return text.split(/[()]/)[1]; + const text = await testSubjects.getVisibleText('tab-scriptedFields'); + return text.split(' ')[2].replace(/\((.*)\)/, '$1'); }); } @@ -324,7 +322,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider isStandardIndexPattern = true ) { await retry.try(async () => { - await this.navigateTo(); await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickKibanaIndexPatterns(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -432,17 +429,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickFieldsTab() { log.debug('click Fields tab'); - await find.clickByCssSelector('#indexedFields'); + await testSubjects.click('tab-indexedFields'); } async clickScriptedFieldsTab() { log.debug('click Scripted Fields tab'); - await find.clickByCssSelector('#scriptedFields'); + await testSubjects.click('tab-scriptedFields'); } async clickSourceFiltersTab() { log.debug('click Source Filters tab'); - await find.clickByCssSelector('#sourceFilters'); + await testSubjects.click('tab-sourceFilters'); } async editScriptedField(name: string) { diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 92f0d090ff5ee3..4606d93ac27f56 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -65,7 +65,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo * Sets commonly used time * @param option 'Today' | 'This_week' | 'Last_15 minutes' | 'Last_24 hours' ... */ - async setCommonlyUsedTime(option: CommonlyUsed) { + async setCommonlyUsedTime(option: CommonlyUsed | string) { await testSubjects.click('superDatePickerToggleQuickMenuButton'); await testSubjects.click(`superDatePickerCommonlyUsed_${option}`); } diff --git a/test/functional/services/find.ts b/test/functional/services/find.ts index 312668b718dc0c..bdcc5ba95e9fbd 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 1b7ef2c1855d0b..df79db50b8683e 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 0fe7df4d507154..d3cfcea9823e9c 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 1d5564ec06e4ef..00000000000000 --- 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 00000000000000..f0c1c3a34fbc09 --- /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 338e85038922de..ebc74be937ef06 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 a50248a5b6fa32..ace2af2b4f0cfe 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 c4cc7175d61570..d7a764b581c01d 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/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 a700727d87299d..4972911d5894f0 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 00000000000000..5fcaeafbb0d852 --- /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/helpers.ts b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts index 00693845bb2662..2486fb0e1fbd04 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 00000000000000..1d5c5824d6b970 --- /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 c74928203db56f..2f271fe5ef65b3 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 c29959197958df..baedb5f2f621bf 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 bf19578c37baab..cae3b604ecd959 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/plugins/kbn_tp_embeddable_explorer/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts deleted file mode 100644 index 99f54277be5d2e..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.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 { Legacy } from 'kibana'; - -// eslint-disable-next-line import/no-default-export -export default function(kibana: any) { - return new kibana.Plugin({ - require: ['kibana'], - uiExports: { - app: { - title: 'Embeddable Explorer', - order: 1, - main: 'plugins/kbn_tp_embeddable_explorer/np_ready/public/legacy', - }, - }, - init(server: Legacy.Server) { - server.injectUiAppVars('kbn_tp_embeddable_explorer', async () => - server.getInjectedUiAppVars('kibana') - ); - }, - }); -} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/kibana.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/kibana.json new file mode 100644 index 00000000000000..6c8d51ccb8651f --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "kbn_tp_embeddable_explorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "requiredPlugins": [ + "visTypeMarkdown", + "visTypeVislib", + "data", + "embeddable", + "uiActions", + "inspector", + "discover" + ], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx similarity index 100% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx similarity index 98% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx index 16c2840d6a32e8..e56b82378ddf7c 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx @@ -24,7 +24,7 @@ import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, DashboardContainerInput, -} from '../../../../../../../../src/plugins/dashboard/public'; +} from '../../../../../../src/plugins/dashboard/public'; import { dashboardInput } from './dashboard_input'; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts similarity index 96% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts index 37ef8cad948cb7..6f4e1f052f5e09 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts @@ -18,7 +18,7 @@ */ import { ViewMode, CONTACT_CARD_EMBEDDABLE, HELLO_WORLD_EMBEDDABLE } from '../embeddable_api'; -import { DashboardContainerInput } from '../../../../../../../../src/plugins/dashboard/public'; +import { DashboardContainerInput } from '../../../../../../src/plugins/dashboard/public'; export const dashboardInput: DashboardContainerInput = { panels: { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/index.ts similarity index 100% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/index.ts rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/index.ts diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddable_api.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddable_api.ts similarity index 74% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddable_api.ts rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddable_api.ts index dd25bebf899200..9f6597fefa1e40 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddable_api.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddable_api.ts @@ -17,6 +17,9 @@ * under the License. */ -export * from '../../../../../../../src/plugins/embeddable/public'; -export * from '../../../../../../../src/plugins/embeddable/public/lib/test_samples'; -export { HELLO_WORLD_EMBEDDABLE } from '../../../../../../../examples/embeddable_examples/public'; +export * from '../../../../../src/plugins/embeddable/public'; +export * from '../../../../../src/plugins/embeddable/public/lib/test_samples'; +export { + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, +} from '../../../../../examples/embeddable_examples/public'; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.ts similarity index 100% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.ts rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.ts diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/kibana.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/kibana.json deleted file mode 100644 index d0d0784eae8d33..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "kbn_tp_embeddable_explorer", - "version": "kibana", - "requiredPlugins": [ - "embeddable", - "inspector" - ], - "server": false, - "ui": true -} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.html b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.html deleted file mode 100644 index a242631e1638f3..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
ANGULAR STUFF!
- diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts deleted file mode 100644 index 6d125bc3002e00..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts +++ /dev/null @@ -1,90 +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. - */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import 'ui/autoload/all'; - -import 'uiExports/interpreter'; -import 'uiExports/embeddableFactories'; -import 'uiExports/embeddableActions'; -import 'uiExports/contextMenuActions'; -import 'uiExports/devTools'; -import 'uiExports/docViews'; -import 'uiExports/embeddableActions'; -import 'uiExports/fieldFormatEditors'; -import 'uiExports/fieldFormats'; -import 'uiExports/home'; -import 'uiExports/indexManagement'; -import 'uiExports/inspectorViews'; -import 'uiExports/savedObjectTypes'; -import 'uiExports/search'; -import 'uiExports/shareContextMenuExtensions'; -import 'uiExports/visTypes'; -import 'uiExports/visualize'; - -import { npSetup, npStart } from 'ui/new_platform'; -import { ExitFullScreenButton } from 'ui/exit_full_screen'; -import uiRoutes from 'ui/routes'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -/* eslint-enable @kbn/eslint/no-restricted-paths */ - -import template from './index.html'; - -import { plugin } from '.'; - -const pluginInstance = plugin({} as any); - -export const setup = pluginInstance.setup(npSetup.core, { - embeddable: npSetup.plugins.embeddable, - inspector: npSetup.plugins.inspector, - __LEGACY: { - ExitFullScreenButton, - }, -}); - -let rendered = false; -const onRenderCompleteListeners: Array<() => void> = []; - -uiRoutes.enable(); -uiRoutes.defaults(/\embeddable_explorer/, {}); -uiRoutes.when('/', { - template, - controller($scope) { - $scope.$$postDigest(() => { - rendered = true; - onRenderCompleteListeners.forEach(listener => listener()); - }); - }, -}); - -export const start = pluginInstance.start(npStart.core, { - embeddable: npStart.plugins.embeddable, - inspector: npStart.plugins.inspector, - uiActions: npStart.plugins.uiActions, - __LEGACY: { - ExitFullScreenButton, - onRenderComplete: (renderCompleteListener: () => void) => { - if (rendered) { - renderCompleteListener(); - } else { - onRenderCompleteListeners.push(renderCompleteListener); - } - }, - }, -}); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx deleted file mode 100644 index b47e84216dd16e..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ /dev/null @@ -1,84 +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 React from 'react'; -import ReactDOM from 'react-dom'; -import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; -import { createHelloWorldAction } from '../../../../../../../src/plugins/ui_actions/public/tests/test_samples'; - -import { - Start as InspectorStartContract, - Setup as InspectorSetupContract, -} from '../../../../../../../src/plugins/inspector/public'; - -import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; - -const REACT_ROOT_ID = 'embeddableExplorerRoot'; - -import { SayHelloAction, createSendMessageAction } from './embeddable_api'; -import { App } from './app'; -import { - EmbeddableStart, - EmbeddableSetup, -} from '.../../../../../../../src/plugins/embeddable/public'; - -export interface SetupDependencies { - embeddable: EmbeddableSetup; - inspector: InspectorSetupContract; - __LEGACY: { - ExitFullScreenButton: React.ComponentType; - }; -} - -interface StartDependencies { - embeddable: EmbeddableStart; - uiActions: UiActionsStart; - inspector: InspectorStartContract; - __LEGACY: { - ExitFullScreenButton: React.ComponentType; - onRenderComplete: (onRenderComplete: () => void) => void; - }; -} - -export type EmbeddableExplorerSetup = void; -export type EmbeddableExplorerStart = void; - -export class EmbeddableExplorerPublicPlugin - implements - Plugin { - public setup(core: CoreSetup, setupDeps: SetupDependencies): EmbeddableExplorerSetup {} - - public start(core: CoreStart, plugins: StartDependencies): EmbeddableExplorerStart { - const helloWorldAction = createHelloWorldAction(core.overlays); - const sayHelloAction = new SayHelloAction(alert); - const sendMessageAction = createSendMessageAction(core.overlays); - - plugins.uiActions.registerAction(sayHelloAction); - plugins.uiActions.registerAction(sendMessageAction); - - plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, helloWorldAction); - - plugins.__LEGACY.onRenderComplete(() => { - const root = document.getElementById(REACT_ROOT_ID); - ReactDOM.render(, root); - }); - } - - public stop() {} -} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx new file mode 100644 index 00000000000000..f99d89ca630bbc --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { CoreSetup, Plugin, AppMountParameters } from 'kibana/public'; +import { UiActionsStart, UiActionsSetup } from '../../../../../src/plugins/ui_actions/public'; +import { createHelloWorldAction } from '../../../../../src/plugins/ui_actions/public/tests/test_samples'; + +import { + Start as InspectorStartContract, + Setup as InspectorSetupContract, +} from '../../../../../src/plugins/inspector/public'; + +import { App } from './app'; +import { + CONTEXT_MENU_TRIGGER, + CONTACT_CARD_EMBEDDABLE, + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, + ContactCardEmbeddableFactory, + SayHelloAction, + createSendMessageAction, +} from './embeddable_api'; +import { + EmbeddableStart, + EmbeddableSetup, +} from '.../../../../../../../src/plugins/embeddable/public'; + +export interface SetupDependencies { + embeddable: EmbeddableSetup; + inspector: InspectorSetupContract; + uiActions: UiActionsSetup; +} + +interface StartDependencies { + embeddable: EmbeddableStart; + uiActions: UiActionsStart; + inspector: InspectorStartContract; +} + +export type EmbeddableExplorerSetup = void; +export type EmbeddableExplorerStart = void; + +export class EmbeddableExplorerPublicPlugin + implements + Plugin { + public setup(core: CoreSetup, setupDeps: SetupDependencies): EmbeddableExplorerSetup { + const helloWorldAction = createHelloWorldAction({} as any); + const sayHelloAction = new SayHelloAction(alert); + const sendMessageAction = createSendMessageAction({} as any); + + setupDeps.uiActions.registerAction(helloWorldAction); + setupDeps.uiActions.registerAction(sayHelloAction); + setupDeps.uiActions.registerAction(sendMessageAction); + + setupDeps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction.id); + + setupDeps.embeddable.registerEmbeddableFactory( + HELLO_WORLD_EMBEDDABLE, + new HelloWorldEmbeddableFactory() + ); + + setupDeps.embeddable.registerEmbeddableFactory( + CONTACT_CARD_EMBEDDABLE, + new ContactCardEmbeddableFactory((() => null) as any, {} as any) + ); + + core.application.register({ + id: 'EmbeddableExplorer', + title: 'Embeddable Explorer', + async mount(params: AppMountParameters) { + const startPlugins = (await core.getStartServices())[1] as StartDependencies; + render(, params.element); + + return () => unmountComponentAtNode(params.element); + }, + }); + } + + public start() {} + public stop() {} +} 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 0781cf8a4f5bdf..b2c0413c5024b7 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 097833750bc800..b8e26b8e6ffcbd 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 8ddd0ff96ba8f5..b2393443989f94 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 1f6e09fad19e95..e3f46e7a6ada41 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 8dc41639fa9468..c962b962b1e5e6 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 5a3716e620fed8..a270144bd49fea 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 c35076e1295339..00000000000000 --- 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/.i18nrc.json b/x-pack/.i18nrc.json index a033515fef8b06..7c464d44d57610 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -14,6 +14,7 @@ "xpack.dashboardMode": "legacy/plugins/dashboard_mode", "xpack.data": "plugins/data_enhanced", "xpack.drilldowns": "plugins/drilldowns", + "xpack.embeddableEnhanced": "plugins/embeddable_enhanced", "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", 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 532c49803e7b0b..746fa693e435eb 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 40123040764b73..a46cdfe35e32dc 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 79f3dcf76b82eb..d2d5309606cde1 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 d49f9827e3ea04..6b83f4026f1db9 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/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts index 2f93765165e50d..3999393600e481 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts @@ -6,16 +6,17 @@ import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; +import { LevelLogger as Logger, startTrace } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; -import { AttributesMap, ElementsPositionAndAttribute } from './types'; -import { Logger } from '../../../../types'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; +import { AttributesMap, ElementsPositionAndAttribute } from './types'; export const getElementPositionAndAttributes = async ( browser: HeadlessBrowser, layout: LayoutInstance, logger: Logger ): Promise => { + const endTrace = startTrace('get_element_position_data', 'read'); const { screenshot: screenshotSelector } = layout.selectors; // data-shared-items-container let elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; try { @@ -69,5 +70,7 @@ export const getElementPositionAndAttributes = async ( elementsPositionAndAttributes = null; } + endTrace(); + return elementsPositionAndAttributes; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts index 57d025890d3e22..d0c1a2a3ce6722 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; +import { LevelLogger, startTrace } from '../../../../server/lib'; import { CaptureConfig } from '../../../../server/types'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; @@ -17,6 +17,7 @@ export const getNumberOfItems = async ( layout: LayoutInstance, logger: LevelLogger ): Promise => { + const endTrace = startTrace('get_number_of_items', 'read'); const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors; let itemsCount: number; @@ -70,5 +71,7 @@ export const getNumberOfItems = async ( itemsCount = 1; } + endTrace(); + return itemsCount; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts index d50ac64743f078..bc9e17854b27d9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts @@ -6,26 +6,9 @@ import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; +import { LevelLogger, startTrace } from '../../../../server/lib'; import { Screenshot, ElementsPositionAndAttribute } from './types'; -const getAsyncDurationLogger = (logger: LevelLogger) => { - return async (description: string, promise: Promise) => { - const start = Date.now(); - const result = await promise; - logger.debug( - i18n.translate('xpack.reporting.screencapture.asyncTook', { - defaultMessage: '{description} took {took}ms', - values: { - description, - took: Date.now() - start, - }, - }) - ); - return result; - }; -}; - export const getScreenshots = async ( browser: HeadlessBrowser, elementsPositionAndAttributes: ElementsPositionAndAttribute[], @@ -37,21 +20,20 @@ export const getScreenshots = async ( }) ); - const asyncDurationLogger = getAsyncDurationLogger(logger); const screenshots: Screenshot[] = []; for (let i = 0; i < elementsPositionAndAttributes.length; i++) { + const endTrace = startTrace('get_screenshots', 'read'); const item = elementsPositionAndAttributes[i]; - const base64EncodedData = await asyncDurationLogger( - `screenshot #${i + 1}`, - browser.screenshot(item.position) - ); + const base64EncodedData = await browser.screenshot(item.position); screenshots.push({ base64EncodedData, title: item.attributes.title, description: item.attributes.description, }); + + endTrace(); } logger.info( diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts index c1c43ed4525941..bcd4cf9000df45 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts @@ -5,7 +5,7 @@ */ import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; +import { LevelLogger, startTrace } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_GETTIMERANGE } from './constants'; import { TimeRange } from './types'; @@ -15,6 +15,7 @@ export const getTimeRange = async ( layout: LayoutInstance, logger: LevelLogger ): Promise => { + const endTrace = startTrace('get_time_range', 'read'); logger.debug('getting timeRange'); const timeRange: TimeRange | null = await browser.evaluate( @@ -45,5 +46,7 @@ export const getTimeRange = async ( logger.debug('no timeRange'); } + endTrace(); + return timeRange; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts index cb2673e85186ba..40bb84870b16de 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts @@ -7,8 +7,8 @@ import { i18n } from '@kbn/i18n'; import fs from 'fs'; import { promisify } from 'util'; -import { LevelLogger } from '../../../../server/lib'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; +import { LevelLogger, startTrace } from '../../../../server/lib'; import { Layout } from '../../layouts/layout'; import { CONTEXT_INJECTCSS } from './constants'; @@ -19,6 +19,7 @@ export const injectCustomCss = async ( layout: Layout, logger: LevelLogger ): Promise => { + const endTrace = startTrace('inject_css', 'correction'); logger.debug( i18n.translate('xpack.reporting.screencapture.injectingCss', { defaultMessage: 'injecting custom css', @@ -49,4 +50,6 @@ export const injectCustomCss = async ( }) ); } + + endTrace(); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts index eb96753f0ce183..282490a28d591e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -4,8 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; -import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators'; +import { + catchError, + concatMap, + first, + mergeMap, + take, + takeUntil, + tap, + toArray, +} from 'rxjs/operators'; import { CaptureConfig } from '../../../../server/types'; import { DEFAULT_PAGELOAD_SELECTOR } from '../../constants'; import { HeadlessChromiumDriverFactory } from '../../../../types'; @@ -41,6 +51,9 @@ export function screenshotsObservableFactory( layout, browserTimezone, }: ScreenshotObservableOpts): Rx.Observable { + const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); + + const apmCreatePage = apmTrans?.startSpan('create_page', 'wait'); const create$ = browserDriverFactory.createPage( { viewport: layout.getBrowserViewport(), browserTimezone }, logger @@ -48,6 +61,7 @@ export function screenshotsObservableFactory( return create$.pipe( mergeMap(({ driver, exit$ }) => { + if (apmCreatePage) apmCreatePage.end(); return Rx.from(urls).pipe( concatMap((url, index) => { const setup$: Rx.Observable = Rx.of(1).pipe( @@ -81,10 +95,12 @@ export function screenshotsObservableFactory( // allows for them to be displayed properly in many cases await injectCustomCss(driver, layout, logger); + const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); if (layout.positionElements) { // position panel elements for print layout await layout.positionElements(driver, logger); } + if (apmPositionElements) apmPositionElements.end(); await waitForRenderComplete(captureConfig, driver, layout, logger); }), @@ -125,7 +141,10 @@ export function screenshotsObservableFactory( toArray() ); }), - first() + first(), + tap(() => { + if (apmTrans) apmTrans.end(); + }) ); }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts index 92a58aded5f66c..a0708b7dba36bc 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; +import { LevelLogger, startTrace } from '../../../../server/lib'; import { CaptureConfig } from '../../../../server/types'; import { ConditionalHeaders } from '../../../../types'; @@ -18,6 +18,7 @@ export const openUrl = async ( conditionalHeaders: ConditionalHeaders, logger: LevelLogger ): Promise => { + const endTrace = startTrace('open_url', 'wait'); try { await browser.open( url, @@ -32,11 +33,10 @@ export const openUrl = async ( throw new Error( i18n.translate('xpack.reporting.screencapture.couldntLoadKibana', { defaultMessage: `An error occurred when trying to open the Kibana URL. You may need to increase '{configKey}'. {error}`, - values: { - configKey: 'xpack.reporting.capture.timeouts.openUrl', - error: err, - }, + values: { configKey: 'xpack.reporting.capture.timeouts.openUrl', error: err }, }) ); } + + endTrace(); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts index e113a5d228cd79..13ddf5eb74fcff 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts @@ -30,7 +30,7 @@ export interface ElementsPositionAndAttribute { } export interface Screenshot { - base64EncodedData: Buffer; + base64EncodedData: string; title: string; description: string; } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts index 069896c8d9e90c..fe92fbc9077e65 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; +import { LevelLogger, startTrace } from '../../../../server/lib'; import { CaptureConfig } from '../../../../server/types'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_WAITFORRENDER } from './constants'; @@ -17,6 +17,8 @@ export const waitForRenderComplete = async ( layout: LayoutInstance, logger: LevelLogger ) => { + const endTrace = startTrace('wait_for_render', 'wait'); + logger.debug( i18n.translate('xpack.reporting.screencapture.waitingForRenderComplete', { defaultMessage: 'waiting for rendering to complete', @@ -76,5 +78,7 @@ export const waitForRenderComplete = async ( defaultMessage: 'rendering is complete', }) ); + + endTrace(); }); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts index 7960e1552e5590..d456c4089ecee7 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; +import { LevelLogger, startTrace } from '../../../../server/lib'; import { CaptureConfig } from '../../../../server/types'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; @@ -29,6 +29,7 @@ export const waitForVisualizations = async ( layout: LayoutInstance, logger: LevelLogger ): Promise => { + const endTrace = startTrace('wait_for_visualizations', 'wait'); const { renderComplete: renderCompleteSelector } = layout.selectors; logger.debug( @@ -63,4 +64,6 @@ export const waitForVisualizations = async ( }) ); } + + endTrace(); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts index 9d3deda5d98bec..fd879f09872323 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts @@ -126,7 +126,7 @@ test(`returns content_type of application/png`, async () => { const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); + generatePngObservable.mockReturnValue(Rx.of('foo')); const { content_type: contentType } = await executeJob( 'pngJobId', @@ -137,10 +137,10 @@ test(`returns content_type of application/png`, async () => { }); test(`returns content of generatePng getBuffer base64 encoded`, async () => { - const testContent = 'test content'; + const testContent = 'raw string from get_screenhots'; const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; - generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); + generatePngObservable.mockReturnValue(Rx.of({ base64: testContent })); const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); @@ -150,5 +150,5 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { cancellationToken ); - expect(content).toEqual(Buffer.from(testContent).toString('base64')); + expect(content).toEqual(testContent); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 0ffd42d0b52f9c..88c2d8a9fe4bbe 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; @@ -29,6 +30,10 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); return async function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) { + const apmTrans = apm.startTransaction('reporting execute_job png', 'reporting'); + const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); + let apmGeneratePng: { end: () => void } | null | undefined; + const generatePngObservable = await generatePngObservableFactory(reporting); const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( @@ -38,6 +43,9 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut mergeMap(conditionalHeaders => { const urls = getFullUrls({ config, job }); const hashUrl = urls[0]; + if (apmGetAssets) apmGetAssets.end(); + + apmGeneratePng = apmTrans?.startSpan('generate_png_pipeline', 'execute'); return generatePngObservable( jobLogger, hashUrl, @@ -46,11 +54,14 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut job.layout ); }), - map(({ buffer, warnings }) => { + map(({ base64, warnings }) => { + if (apmGeneratePng) apmGeneratePng.end(); + return { content_type: 'image/png', - content: buffer.toString('base64'), - size: buffer.byteLength, + content: base64, + size: (base64 && base64.length) || 0, + warnings, }; }), diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index c03ea170f76eee..c79aa281870526 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; import { ReportingCore } from '../../../../server'; @@ -22,12 +23,16 @@ export async function generatePngObservableFactory(reporting: ReportingCore) { browserTimezone: string, conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams - ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { + ): Rx.Observable<{ base64: string | null; warnings: string[] }> { + const apmTrans = apm.startTransaction('reporting generate_png', 'reporting'); + const apmLayout = apmTrans?.startSpan('create_layout', 'setup'); if (!layoutParams || !layoutParams.dimensions) { throw new Error(`LayoutParams.Dimensions is undefined.`); } - const layout = new PreserveLayout(layoutParams.dimensions); + if (apmLayout) apmLayout.end(); + + const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup'); const screenshots$ = getScreenshots({ logger, urls: [url], @@ -36,8 +41,11 @@ export async function generatePngObservableFactory(reporting: ReportingCore) { browserTimezone, }).pipe( map((results: ScreenshotResults[]) => { + if (apmScreenshots) apmScreenshots.end(); + if (apmTrans) apmTrans.end(); + return { - buffer: results[0].screenshots[0].base64EncodedData, + base64: results[0].screenshots[0].base64EncodedData, warnings: results.reduce((found, current) => { if (current.error) { found.push(current.error.message); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index 3d69042b6c7abe..5aad66c53a998f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PDF_JOB_TYPE } from '../../../../common/constants'; @@ -31,6 +32,10 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut const logger = parentLogger.clone([PDF_JOB_TYPE, 'execute']); return async function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) { + const apmTrans = apm.startTransaction('reporting execute_job pdf', 'reporting'); + const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); + let apmGeneratePdf: { end: () => void } | null | undefined; + const generatePdfObservable = await generatePdfObservableFactory(reporting); const jobLogger = logger.clone([jobId]); @@ -43,6 +48,9 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut const urls = getFullUrls({ config, job }); const { browserTimezone, layout, title } = job; + if (apmGetAssets) apmGetAssets.end(); + + apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute'); return generatePdfObservable( jobLogger, title, @@ -53,12 +61,20 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut logo ); }), - map(({ buffer, warnings }) => ({ - content_type: 'application/pdf', - content: buffer.toString('base64'), - size: buffer.byteLength, - warnings, - })), + map(({ buffer, warnings }) => { + if (apmGeneratePdf) apmGeneratePdf.end(); + + const apmEncode = apmTrans?.startSpan('encode_pdf', 'output'); + const content = buffer?.toString('base64') || null; + if (apmEncode) apmEncode.end(); + + return { + content_type: 'application/pdf', + content, + size: buffer?.byteLength || 0, + warnings, + }; + }), catchError(err => { jobLogger.error(err); return Rx.throwError(err); @@ -66,6 +82,8 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut ); const stop$ = Rx.fromEventPattern(cancellationToken.on); + + if (apmTrans) apmTrans.end(); return process$.pipe(takeUntil(stop$)).toPromise(); }; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index c882ef682f9529..238accba8b1dc0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -13,6 +13,7 @@ import { ConditionalHeaders } from '../../../../types'; import { createLayout } from '../../../common/layouts'; import { LayoutInstance, LayoutParams } from '../../../common/layouts/layout'; import { ScreenshotResults } from '../../../common/lib/screenshots/types'; +import { getTracker } from './tracker'; // @ts-ignore untyped module import { pdf } from './pdf'; @@ -39,8 +40,14 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams, logo?: string - ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { + ): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { + const tracker = getTracker(); + tracker.startLayout(); + const layout = createLayout(captureConfig, layoutParams) as LayoutInstance; + tracker.endLayout(); + + tracker.startScreenshots(); const screenshots$ = getScreenshots({ logger, urls, @@ -49,16 +56,22 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { browserTimezone, }).pipe( mergeMap(async (results: ScreenshotResults[]) => { - const pdfOutput = pdf.create(layout, logo); + tracker.endScreenshots(); + tracker.startSetup(); + const pdfOutput = pdf.create(layout, logo); if (title) { const timeRange = getTimeRange(results); title += timeRange ? ` - ${timeRange.duration}` : ''; pdfOutput.setTitle(title); } + tracker.endSetup(); results.forEach(r => { r.screenshots.forEach(screenshot => { + logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.base64EncodedData?.length || 0}`); // prettier-ignore + tracker.startAddImage(); + tracker.endAddImage(); pdfOutput.addImage(screenshot.base64EncodedData, { title: screenshot.title, description: screenshot.description, @@ -66,10 +79,26 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { }); }); - pdfOutput.generate(); + let buffer: Buffer | null = null; + try { + tracker.startCompile(); + logger.debug(`Compiling PDF...`); + pdfOutput.generate(); + tracker.endCompile(); + + tracker.startGetBuffer(); + logger.debug(`Generating PDF Buffer...`); + buffer = await pdfOutput.getBuffer(); + logger.debug(`PDF buffer byte length: ${buffer?.byteLength || 0}`); + tracker.endGetBuffer(); + } catch (err) { + logger.error(`Could not generate the PDF buffer! ${err}`); + } + + tracker.end(); return { - buffer: await pdfOutput.getBuffer(), + buffer, warnings: results.reduce((found, current) => { if (current.error) { found.push(current.error.message); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/tracker.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/tracker.ts new file mode 100644 index 00000000000000..b6fad243db7b12 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/tracker.ts @@ -0,0 +1,83 @@ +/* + * 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 apm from 'elastic-apm-node'; + +interface PdfTracker { + startLayout: () => void; + endLayout: () => void; + startScreenshots: () => void; + endScreenshots: () => void; + startSetup: () => void; + endSetup: () => void; + startAddImage: () => void; + endAddImage: () => void; + startCompile: () => void; + endCompile: () => void; + startGetBuffer: () => void; + endGetBuffer: () => void; + end: () => void; +} + +const SPANTYPE_SETUP = 'setup'; +const SPANTYPE_OUTPUT = 'output'; + +interface ApmSpan { + end: () => void; +} + +export function getTracker(): PdfTracker { + const apmTrans = apm.startTransaction('reporting generate_pdf', 'reporting'); + + let apmLayout: ApmSpan | null = null; + let apmScreenshots: ApmSpan | null = null; + let apmSetup: ApmSpan | null = null; + let apmAddImage: ApmSpan | null = null; + let apmCompilePdf: ApmSpan | null = null; + let apmGetBuffer: ApmSpan | null = null; + + return { + startLayout() { + apmLayout = apmTrans?.startSpan('create_layout', SPANTYPE_SETUP) || null; + }, + endLayout() { + if (apmLayout) apmLayout.end(); + }, + startScreenshots() { + apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', SPANTYPE_SETUP) || null; + }, + endScreenshots() { + if (apmScreenshots) apmScreenshots.end(); + }, + startSetup() { + apmSetup = apmTrans?.startSpan('setup_pdf', SPANTYPE_SETUP) || null; + }, + endSetup() { + if (apmSetup) apmSetup.end(); + }, + startAddImage() { + apmAddImage = apmTrans?.startSpan('add_pdf_image', SPANTYPE_OUTPUT) || null; + }, + endAddImage() { + if (apmAddImage) apmAddImage.end(); + }, + startCompile() { + apmCompilePdf = apmTrans?.startSpan('compile_pdf', SPANTYPE_OUTPUT) || null; + }, + endCompile() { + if (apmCompilePdf) apmCompilePdf.end(); + }, + startGetBuffer() { + apmGetBuffer = apmTrans?.startSpan('get_buffer', SPANTYPE_OUTPUT) || null; + }, + endGetBuffer() { + if (apmGetBuffer) apmGetBuffer.end(); + }, + end() { + if (apmTrans) apmTrans.end(); + }, + }; +} diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 16b8fbdb30fdd9..ad0f05c02a1f44 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -11,18 +11,13 @@ import { ESQueueInstance, ESQueueWorkerExecuteFn, ExportTypeDefinition, - ImmediateExecuteFn, - JobDocPayload, JobSource, Logger, - RequestFacade, } from '../../types'; // @ts-ignore untyped dependency import { events as esqueueEvents } from './esqueue'; export function createWorkerFactory(reporting: ReportingCore, logger: Logger) { - type JobDocPayloadType = JobDocPayload; - const config = reporting.getConfig(); const queueConfig = config.get('queue'); const kibanaName = config.kbnConfig.get('server', 'name'); @@ -31,48 +26,36 @@ export function createWorkerFactory(reporting: ReportingCore, log // Once more document types are added, this will need to be passed in return async function createWorker(queue: ESQueueInstance) { // export type / execute job map - const jobExecutors: Map< - string, - ImmediateExecuteFn | ESQueueWorkerExecuteFn - > = new Map(); + const jobExecutors: Map> = new Map(); for (const exportType of reporting.getExportTypesRegistry().getAll() as Array< - ExportTypeDefinition< - JobParamsType, - unknown, - unknown, - ImmediateExecuteFn | ESQueueWorkerExecuteFn - > + ExportTypeDefinition> >) { const jobExecutor = await exportType.executeJobFactory(reporting, logger); // FIXME: does not "need" to be async jobExecutors.set(exportType.jobType, jobExecutor); } - const workerFn = (jobSource: JobSource, ...workerRestArgs: any[]) => { + const workerFn = ( + jobSource: JobSource, + jobParams: ScheduledTaskParamsType, + cancellationToken: CancellationToken + ) => { const { _id: jobId, _source: { jobtype: jobType }, } = jobSource; + if (!jobId) { + throw new Error(`Claimed job is missing an ID!: ${JSON.stringify(jobSource)}`); + } + const jobTypeExecutor = jobExecutors.get(jobType); - // pass the work to the jobExecutor if (!jobTypeExecutor) { throw new Error(`Unable to find a job executor for the claimed job: [${jobId}]`); } - if (jobId) { - const jobExecutorWorker = jobTypeExecutor as ESQueueWorkerExecuteFn; - return jobExecutorWorker( - jobId, - ...(workerRestArgs as [JobDocPayloadType, CancellationToken]) - ); - } else { - const jobExecutorImmediate = jobExecutors.get(jobType) as ImmediateExecuteFn; - return jobExecutorImmediate( - null, - ...(workerRestArgs as [JobDocPayload, RequestFacade]) - ); - } + // pass the work to the jobExecutor + return jobTypeExecutor(jobId, jobParams, cancellationToken); }; const workerOptions = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 3e87337dc43550..8f33d9b73566cc 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -9,7 +9,6 @@ import { ConditionalHeaders, EnqueueJobFn, ESQueueCreateJobFn, - ImmediateCreateJobFn, Job, Logger, RequestFacade, @@ -40,7 +39,7 @@ export function enqueueJobFactory(reporting: ReportingCore, parentLogger: Logger headers: ConditionalHeaders['headers'], request: RequestFacade ): Promise { - type CreateJobFn = ESQueueCreateJobFn | ImmediateCreateJobFn; + type CreateJobFn = ESQueueCreateJobFn; const esqueue = await reporting.getEsqueue(); const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/legacy/plugins/reporting/server/lib/index.ts index f5ccbe493a91f0..2a8fa45b6fcefd 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +export { LevelLogger } from './level_logger'; export { checkLicenseFactory } from './check_license'; export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { enqueueJobFactory } from './enqueue_job'; export { getExportTypesRegistry } from './export_types_registry'; -export { LevelLogger } from './level_logger'; export { runValidations } from './validate'; +export { startTrace } from './trace'; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts b/x-pack/legacy/plugins/reporting/server/lib/trace.ts similarity index 53% rename from x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts rename to x-pack/legacy/plugins/reporting/server/lib/trace.ts index 22d2fec0beddc4..2d79d17715d0b4 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/trace.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller } from 'kibana/server'; -import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import apm from 'elastic-apm-node'; -export function validateCardinality(callAsCurrentUser: APICaller, job: CombinedJob): any[]; +export function startTrace(name: string, category: string) { + const span = apm.startSpan(name, category); + return () => { + if (span) span.end(); + }; +} diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 4c8cc3aa503e6e..54624b94e0de35 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -98,7 +98,7 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba | _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | | _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | | _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | -| _xpack.actions._**preconfigured** | A list of preconfigured actions. Default: `[]` | Array | +| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array | #### Whitelisting Built-in Action Types 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 3be2f265570791..a8f50ec3535e2c 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 723982b11e1ccd..73ae49a7e69c2f 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/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 161a6c31d4e599..e86f2d7832828e 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -14,7 +14,7 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], - "preconfigured": Array [], + "preconfigured": Object {}, "whitelistedHosts": Array [ "*", ], @@ -24,16 +24,15 @@ describe('config validation', () => { test('action with preconfigured actions', () => { const config: Record = { - preconfigured: [ - { - id: 'my-slack1', + preconfigured: { + mySlack1: { actionTypeId: '.slack', name: 'Slack #xyz', config: { webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, }, - ], + }, }; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { @@ -41,21 +40,57 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], - "preconfigured": Array [ - Object { + "preconfigured": Object { + "mySlack1": Object { "actionTypeId": ".slack", "config": Object { "webhookUrl": "https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz", }, - "id": "my-slack1", "name": "Slack #xyz", "secrets": Object {}, }, - ], + }, "whitelistedHosts": Array [ "*", ], } `); }); + + test('validates preconfigured action ids', () => { + expect(() => + configSchema.validate(preConfiguredActionConfig('')) + ).toThrowErrorMatchingInlineSnapshot( + `"[preconfigured]: invalid preconfigured action id \\"\\""` + ); + + expect(() => + configSchema.validate(preConfiguredActionConfig('constructor')) + ).toThrowErrorMatchingInlineSnapshot( + `"[preconfigured]: invalid preconfigured action id \\"constructor\\""` + ); + + expect(() => + configSchema.validate(preConfiguredActionConfig('__proto__')) + ).toThrowErrorMatchingInlineSnapshot( + `"[preconfigured]: invalid preconfigured action id \\"__proto__\\""` + ); + }); }); + +// object creator that ensures we can create a property named __proto__ on an +// object, via JSON.parse() +function preConfiguredActionConfig(id: string) { + return JSON.parse(`{ + "preconfigured": { + ${JSON.stringify(id)}: { + "actionTypeId": ".server-log", + "name": "server log 1" + }, + "serverLog": { + "actionTypeId": ".server-log", + "name": "server log 2" + } + } + }`); +} diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 1f04efd1941b4b..b2f3fa2680a9cc 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -7,6 +7,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { WhitelistedHosts, EnabledActionTypes } from './actions_config'; +const preconfiguredActionSchema = schema.object({ + name: schema.string({ minLength: 1 }), + actionTypeId: schema.string({ minLength: 1 }), + config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), +}); + export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), whitelistedHosts: schema.arrayOf( @@ -21,18 +28,26 @@ export const configSchema = schema.object({ defaultValue: [WhitelistedHosts.Any], } ), - preconfigured: schema.arrayOf( - schema.object({ - id: schema.string({ minLength: 1 }), - name: schema.string(), - actionTypeId: schema.string({ minLength: 1 }), - config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - }), - { - defaultValue: [], - } - ), + preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, { + defaultValue: {}, + validate: validatePreconfigured, + }), }); export type ActionsConfig = TypeOf; + +const invalidActionIds = new Set(['', '__proto__', 'constructor']); + +function validatePreconfigured(preconfigured: Record): string | undefined { + // check for ids that should not be used + for (const id of Object.keys(preconfigured)) { + if (invalidActionIds.has(id)) { + return `invalid preconfigured action id "${id}"`; + } + } + + // in case __proto__ was used as a preconfigured action id ... + if (Object.getPrototypeOf(preconfigured) !== Object.getPrototypeOf({})) { + return `invalid preconfigured action id "__proto__"`; + } +} diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 2b334953063d18..8673d992ada983 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -12,6 +12,7 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogMock } from '../../event_log/server/mocks'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ActionType } from './types'; +import { ActionsConfig } from './config'; import { ActionsPlugin, ActionsPluginsSetup, @@ -31,33 +32,11 @@ describe('Actions Plugin', () => { let pluginsSetup: jest.Mocked; beforeEach(() => { - context = coreMock.createPluginInitializerContext({ - preconfigured: [ - { - id: 'my-slack1', - actionTypeId: '.slack', - name: 'Slack #xyz', - description: 'Send a message to the #xyz channel', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, - }, - { - id: 'custom-system-abc-connector', - actionTypeId: 'system-abc-action-type', - description: 'Send a notification to system ABC', - name: 'System ABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, - secrets: { - xyzSecret1: 'credential1', - xyzSecret2: 'credential2', - }, - }, - ], + context = coreMock.createPluginInitializerContext({ + enabled: true, + enabledActionTypes: ['*'], + whitelistedHosts: ['*'], + preconfigured: {}, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -192,6 +171,7 @@ describe('Actions Plugin', () => { }); }); }); + describe('start()', () => { let plugin: ActionsPlugin; let coreSetup: ReturnType; @@ -200,8 +180,18 @@ describe('Actions Plugin', () => { let pluginsStart: jest.Mocked; beforeEach(() => { - const context = coreMock.createPluginInitializerContext({ - preconfigured: [], + const context = coreMock.createPluginInitializerContext({ + enabled: true, + enabledActionTypes: ['*'], + whitelistedHosts: ['*'], + preconfigured: { + preconfiguredServerLog: { + actionTypeId: '.server-log', + name: 'preconfigured-server-log', + config: {}, + secrets: {}, + }, + }, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -220,6 +210,15 @@ describe('Actions Plugin', () => { }); describe('getActionsClientWithRequest()', () => { + it('should handle preconfigured actions', async () => { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); + const pluginStart = plugin.start(coreStart, pluginsStart); + + expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); + }); + it('should not throw error when ESO plugin not using a generated key', async () => { // coreMock.createSetup doesn't support Plugin generics // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index f14df794bbf47b..bc7440c8bee4de 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -150,12 +150,14 @@ export class ActionsPlugin implements Plugin, Plugi const actionsConfig = (await this.config) as ActionsConfig; const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig); - this.preconfiguredActions.push( - ...actionsConfig.preconfigured.map( - preconfiguredAction => - ({ ...preconfiguredAction, isPreconfigured: true } as PreConfiguredAction) - ) - ); + for (const preconfiguredId of Object.keys(actionsConfig.preconfigured)) { + this.preconfiguredActions.push({ + ...actionsConfig.preconfigured[preconfiguredId], + id: preconfiguredId, + isPreconfigured: true, + }); + } + const actionTypeRegistry = new ActionTypeRegistry({ taskRunnerFactory, taskManager: plugins.taskManager, diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx index 867ead688d23d0..4d14226777a0b8 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx @@ -32,9 +32,9 @@ export interface ActionWizardProps { /** * Action factory selected changed - * null - means user click "change" and removed action factory selection + * empty - means user click "change" and removed action factory selection */ - onActionFactoryChange: (actionFactory: ActionFactory | null) => void; + onActionFactoryChange: (actionFactory?: ActionFactory) => void; /** * current config for currently selected action factory @@ -71,7 +71,7 @@ export const ActionWizard: React.FC = ({ actionFactory={currentActionFactory} showDeselect={actionFactories.length > 1} onDeselect={() => { - onActionFactoryChange(null); + onActionFactoryChange(undefined); }} context={context} config={config} diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx index c3e749f163c949..692e86b53f09d7 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx @@ -167,7 +167,7 @@ export function Demo({ actionFactories }: { actionFactories: Array({}); - function changeActionFactory(newActionFactory: ActionFactory | null) { + function changeActionFactory(newActionFactory?: ActionFactory) { if (!newActionFactory) { // removing action factory return setState({}); diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts index f1aef5deff49e0..262a5ef7d4561c 100644 --- a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts @@ -42,6 +42,10 @@ export class ActionFactory< return this.def.getDisplayName(context); } + public getDisplayNameTooltip(context: FactoryContext): string { + return ''; + } + public async isCompatible(context: FactoryContext): Promise { if (!this.def.isCompatible) return true; return await this.def.isCompatible(context); 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 f9df390242cd45..f556287703347c 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 55e39b6a817dbf..8bcb4d838ca1bb 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 2ff30a61499b6b..87db0005fb6566 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -5,22 +5,23 @@ */ import { i18n } from '@kbn/i18n'; +import cytoscape from 'cytoscape'; import { ILicense } from '../../licensing/public'; import { AGENT_NAME, SERVICE_ENVIRONMENT, SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, SPAN_SUBTYPE, - SPAN_TYPE, - SPAN_DESTINATION_SERVICE_RESOURCE + SPAN_TYPE } from './elasticsearch_fieldnames'; -export interface ServiceConnectionNode { +export interface ServiceConnectionNode extends cytoscape.NodeDataDefinition { [SERVICE_NAME]: string; [SERVICE_ENVIRONMENT]: string | null; [AGENT_NAME]: string; } -export interface ExternalConnectionNode { +export interface ExternalConnectionNode extends cytoscape.NodeDataDefinition { [SPAN_DESTINATION_SERVICE_RESOURCE]: string; [SPAN_TYPE]: string; [SPAN_SUBTYPE]: string; @@ -34,7 +35,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/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index 0fbf0a5c7a27d3..3de725dc58ea7f 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -6,7 +6,21 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` Array [ Object { "field": "groupId", - "name": "Group ID", + "name": + Group ID + + + , "render": [Function], "sortable": false, "width": "96px", @@ -141,9 +155,26 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > Group ID + + +
+
@@ -358,7 +389,21 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` Array [ Object { "field": "groupId", - "name": "Group ID", + "name": + Group ID + + + , "render": [Function], "sortable": false, "width": "96px", @@ -524,9 +569,26 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > Group ID + + +
+
@@ -689,6 +751,23 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > Group ID + + +
+
List should render with data 1`] = ` className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > Group ID + + +
+
List should render with data 1`] = ` className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > Group ID + + +
+
List should render with data 1`] = ` className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > Group ID + + +
+
= props => { if (!serviceName) { throw new Error('Service name is required'); } - const columns = useMemo( () => [ { - name: i18n.translate('xpack.apm.errorsTable.groupIdColumnLabel', { - defaultMessage: 'Group ID' - }), + name: ( + <> + {i18n.translate('xpack.apm.errorsTable.groupIdColumnLabel', { + defaultMessage: 'Group ID' + })}{' '} + + + ), field: 'groupId', sortable: false, width: px(unit * 6), 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 cb983cdffa0282..1e3a73acfab57f 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 8960af0f21fd29..b4a556c497c1bb 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 51bdb63874e633..e7f6cba59318a5 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/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx index cff190cd98a11a..6aa7815ad688c6 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx @@ -134,9 +134,11 @@ export function MachineLearningFlyoutView({

+ ), + serviceMapAnnotationText: ( + + {i18n.translate( + 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.serviceMapAnnotationText', + { + defaultMessage: 'service maps' } )} @@ -155,15 +167,15 @@ export function MachineLearningFlyoutView({

{i18n.translate( 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText', { - defaultMessage: 'Machine Learning jobs management page' + defaultMessage: 'Machine Learning Job Management page' } )} 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 7e15d0116b84d1..b5bfa63c1bdde0 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 e5962afd76eb8d..2edd36f0d13807 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 5c28fc0a5a7d0e..39d54dc5801d27 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 3a6f94b9758002..79a6370b4be46a 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/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 8ddce66f0b853d..e15cf2df38c5f5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -236,28 +236,33 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => } }); -/** - * Changes the parent_id of items based on the child.id property. - * Solves the problem of Inferred spans that are created as child of trace spans - * when it actually should be its parent. - * @param waterfallItems - */ -const reparentSpans = (waterfallItems: IWaterfallItem[]) => { +function reparentSpans(waterfallItems: IWaterfallItem[]) { + // find children that needs to be re-parented and map them to their correct parent id + const childIdToParentIdMapping = Object.fromEntries( + flatten( + waterfallItems.map(waterfallItem => { + if (waterfallItem.docType === 'span') { + const childIds = waterfallItem.doc.child?.id ?? []; + return childIds.map(id => [id, waterfallItem.id]); + } + return []; + }) + ) + ); + + // update parent id for children that needs it or return unchanged return waterfallItems.map(waterfallItem => { - if (waterfallItem.docType === 'span') { - const childId = waterfallItem.doc.child?.id; - if (childId) { - childId.forEach(id => { - const item = waterfallItems.find(_item => _item.id === id); - if (item) { - item.parentId = waterfallItem.id; - } - }); - } + const newParentId = childIdToParentIdMapping[waterfallItem.id]; + if (newParentId) { + return { + ...waterfallItem, + parentId: newParentId + }; } + return waterfallItem; }); -}; +} const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) => groupBy(waterfallItems, item => (item.parentId ? item.parentId : ROOT_ID)); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts index 2f28e37f73f624..6f4a0629c7bc51 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -2027,7 +2027,7 @@ export const inferredSpans = { id: '41226ae63af4f235', type: 'unknown' }, - child: { ids: ['8d80de06aa11a6fc'] } + child: { id: ['8d80de06aa11a6fc'] } }, { container: { diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx index e3b33f11d0805f..f8dcec14630a53 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; @@ -109,27 +109,26 @@ export function TransactionList({ items, isLoading }: Props) { { field: 'impact', name: ( - - <> - {i18n.translate('xpack.apm.transactionsTable.impactColumnLabel', { - defaultMessage: 'Impact' - })}{' '} - - - + <> + {i18n.translate('xpack.apm.transactionsTable.impactColumnLabel', { + defaultMessage: 'Impact' + })}{' '} + + ), sortable: true, dataType: 'number', diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 4092e0148286e5..6d9a917af659f4 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -6,7 +6,8 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { FunctionComponent, useMemo, useState } from 'react'; +import React, { FunctionComponent, useMemo, useState, MouseEvent } from 'react'; +import url from 'url'; import { Filter } from '../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { @@ -82,7 +83,39 @@ export const TransactionActionMenu: FunctionComponent = ({ basePath: core.http.basePath, location, urlParams - }); + }).map(sectionList => + sectionList.map(section => ({ + ...section, + actions: section.actions.map(action => { + const { href } = action; + + // use navigateToApp as a temporary workaround for faster navigation between observability apps. + // see https://github.com/elastic/kibana/issues/65682 + + return { + ...action, + onClick: (event: MouseEvent) => { + const parsed = url.parse(href); + + const appPathname = core.http.basePath.remove( + parsed.pathname ?? '' + ); + + const [, , app, ...rest] = appPathname.split('/'); + + if (app === 'uptime' || app === 'metrics' || app === 'logs') { + event.preventDefault(); + core.application.navigateToApp(app, { + path: `${rest.join('/')}${ + parsed.search ? `&${parsed.search}` : '' + }` + }); + } + } + }; + }) + })) + ); const closePopover = () => { setIsActionPopoverOpen(false); @@ -151,6 +184,7 @@ export const TransactionActionMenu: FunctionComponent = ({ key={action.key} label={action.label} href={action.href} + onClick={action.onClick} /> ))} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 7d5f0a75d2208c..8fb44b70bc0812 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -9,16 +9,15 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; +import { getMlIndex } from '../../../common/ml_job_constants'; import { getServicesProjection } from '../../../common/projections/services'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; +import { rangeFilter } from '../helpers/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { dedupeConnections } from './dedupe_connections'; +import { transformServiceMapResponses } from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; -import { addAnomaliesToServicesData } from './ml_helpers'; -import { getMlIndex } from '../../../common/ml_job_constants'; -import { rangeFilter } from '../helpers/range_filter'; export interface IEnvOptions { setup: Setup & SetupTimeRange; @@ -179,13 +178,9 @@ export async function getServiceMap(options: IEnvOptions) { getAnomaliesData(options) ]); - const servicesDataWithAnomalies = addAnomaliesToServicesData( - servicesData, - anomaliesData - ); - - return dedupeConnections({ + return transformServiceMapResponses({ ...connectionData, - services: servicesDataWithAnomalies + anomalies: anomaliesData, + services: servicesData }); } 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 d7e28828572d55..7e8dccb8aff060 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/apm/server/lib/service_map/ml_helpers.test.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts index c80ba8dba01eaa..908dbe6df4636b 100644 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts @@ -5,11 +5,11 @@ */ import { AnomaliesResponse } from './get_service_map'; -import { addAnomaliesToServicesData } from './ml_helpers'; +import { addAnomaliesDataToNodes } from './ml_helpers'; -describe('addAnomaliesToServicesData', () => { - it('adds anomalies to services data', () => { - const servicesData = [ +describe('addAnomaliesDataToNodes', () => { + it('adds anomalies to nodes', () => { + const nodes = [ { 'service.name': 'opbeans-ruby', 'agent.name': 'ruby', @@ -89,8 +89,8 @@ describe('addAnomaliesToServicesData', () => { ]; expect( - addAnomaliesToServicesData( - servicesData, + addAnomaliesDataToNodes( + nodes, (anomaliesResponse as unknown) as AnomaliesResponse ) ).toEqual(result); diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts index 9789911660bd03..fae9e7d4cb1c61 100644 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts +++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts @@ -9,10 +9,11 @@ import { getMlJobServiceName, getSeverity } from '../../../common/ml_job_constants'; -import { AnomaliesResponse, ServicesResponse } from './get_service_map'; +import { ConnectionNode } from '../../../common/service_map'; +import { AnomaliesResponse } from './get_service_map'; -export function addAnomaliesToServicesData( - servicesData: ServicesResponse, +export function addAnomaliesDataToNodes( + nodes: ConnectionNode[], anomaliesResponse: AnomaliesResponse ) { const anomaliesMap = ( @@ -52,7 +53,7 @@ export function addAnomaliesToServicesData( }; }, {}); - const servicesDataWithAnomalies = servicesData.map(service => { + const servicesDataWithAnomalies = nodes.map(service => { const serviceAnomalies = anomaliesMap[service[SERVICE_NAME]]; if (serviceAnomalies) { const maxScore = serviceAnomalies.max_score; diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts similarity index 83% rename from x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts rename to x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 4af8a541392042..45b64c1ad03a42 100644 --- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -4,16 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ServiceMapResponse } from './'; import { - SPAN_DESTINATION_SERVICE_RESOURCE, - SERVICE_NAME, - SERVICE_ENVIRONMENT, AGENT_NAME, - SPAN_TYPE, - SPAN_SUBTYPE -} from '../../../../common/elasticsearch_fieldnames'; -import { dedupeConnections } from './'; + SERVICE_ENVIRONMENT, + SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_SUBTYPE, + SPAN_TYPE +} from '../../../common/elasticsearch_fieldnames'; +import { AnomaliesResponse } from './get_service_map'; +import { + transformServiceMapResponses, + ServiceMapResponse +} from './transform_service_map_responses'; const nodejsService = { [SERVICE_NAME]: 'opbeans-node', @@ -33,9 +36,14 @@ const javaService = { [AGENT_NAME]: 'java' }; -describe('dedupeConnections', () => { +const anomalies = ({ + aggregations: { jobs: { buckets: [] } } +} as unknown) as AnomaliesResponse; + +describe('transformServiceMapResponses', () => { it('maps external destinations to internal services', () => { const response: ServiceMapResponse = { + anomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -51,7 +59,7 @@ describe('dedupeConnections', () => { ] }; - const { elements } = dedupeConnections(response); + const { elements } = transformServiceMapResponses(response); const connection = elements.find( element => 'source' in element.data && 'target' in element.data @@ -67,6 +75,7 @@ describe('dedupeConnections', () => { it('collapses external destinations based on span.destination.resource.name', () => { const response: ServiceMapResponse = { + anomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -89,7 +98,7 @@ describe('dedupeConnections', () => { ] }; - const { elements } = dedupeConnections(response); + const { elements } = transformServiceMapResponses(response); const connections = elements.filter(element => 'source' in element.data); @@ -102,6 +111,7 @@ describe('dedupeConnections', () => { it('picks the first span.type/subtype in an alphabetically sorted list', () => { const response: ServiceMapResponse = { + anomalies, services: [javaService], discoveredServices: [], connections: [ @@ -126,7 +136,7 @@ describe('dedupeConnections', () => { ] }; - const { elements } = dedupeConnections(response); + const { elements } = transformServiceMapResponses(response); const nodes = elements.filter(element => !('source' in element.data)); @@ -140,6 +150,7 @@ describe('dedupeConnections', () => { it('processes connections without a matching "service" aggregation', () => { const response: ServiceMapResponse = { + anomalies, services: [javaService], discoveredServices: [], connections: [ @@ -150,7 +161,7 @@ describe('dedupeConnections', () => { ] }; - const { elements } = dedupeConnections(response); + const { elements } = transformServiceMapResponses(response); expect(elements.length).toBe(3); }); diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts similarity index 76% rename from x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts rename to x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index e5d7c0b2de10cb..8b91bb98b5200d 100644 --- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -10,14 +10,19 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, SPAN_TYPE, SPAN_SUBTYPE -} from '../../../../common/elasticsearch_fieldnames'; +} from '../../../common/elasticsearch_fieldnames'; import { Connection, ConnectionNode, ServiceConnectionNode, ExternalConnectionNode -} from '../../../../common/service_map'; -import { ConnectionsResponse, ServicesResponse } from '../get_service_map'; +} from '../../../common/service_map'; +import { + ConnectionsResponse, + ServicesResponse, + AnomaliesResponse +} from './get_service_map'; +import { addAnomaliesDataToNodes } from './ml_helpers'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -34,13 +39,16 @@ function getConnectionId(connection: Connection) { } export type ServiceMapResponse = ConnectionsResponse & { + anomalies: AnomaliesResponse; services: ServicesResponse; }; -export function dedupeConnections(response: ServiceMapResponse) { - const { discoveredServices, services, connections } = response; +export function transformServiceMapResponses(response: ServiceMapResponse) { + const { anomalies, discoveredServices, services, connections } = response; - const allNodes = connections + // Derive the rest of the map nodes from the connections and add the services + // from the services data query + const allNodes: ConnectionNode[] = connections .flatMap(connection => [connection.source, connection.destination]) .map(node => ({ ...node, id: getConnectionNodeId(node) })) .concat( @@ -50,25 +58,21 @@ export function dedupeConnections(response: ServiceMapResponse) { })) ); - const serviceNodes = allNodes.filter(node => SERVICE_NAME in node) as Array< - ServiceConnectionNode & { - id: string; - } - >; + // List of nodes that are services + const serviceNodes = allNodes.filter( + node => SERVICE_NAME in node + ) as ServiceConnectionNode[]; + // List of nodes that are externals const externalNodes = allNodes.filter( node => SPAN_DESTINATION_SERVICE_RESOURCE in node - ) as Array< - ExternalConnectionNode & { - id: string; - } - >; + ) as ExternalConnectionNode[]; - // 1. maps external nodes to internal services - // 2. collapses external nodes into one node based on span.destination.service.resource - // 3. picks the first available span.type/span.subtype in an alphabetically sorted list + // 1. Map external nodes to internal services + // 2. Collapse external nodes into one node based on span.destination.service.resource + // 3. Pick the first available span.type/span.subtype in an alphabetically sorted list const nodeMap = allNodes.reduce((map, node) => { - if (map[node.id]) { + if (!node.id || map[node.id]) { return map; } @@ -119,14 +123,14 @@ export function dedupeConnections(response: ServiceMapResponse) { .sort()[0] } }; - }, {} as Record); + }, {} as Record); - // maps destination.address to service.name if possible + // Map destination.address to service.name if possible function getConnectionNode(node: ConnectionNode) { return nodeMap[getConnectionNodeId(node)]; } - // build connections with mapped nodes + // Build connections with mapped nodes const mappedConnections = connections .map(connection => { const sourceData = getConnectionNode(connection.source); @@ -166,7 +170,7 @@ export function dedupeConnections(response: ServiceMapResponse) { {} as Record ); - // instead of adding connections in two directions, + // Instead of adding connections in two directions, // we add a `bidirectional` flag to use in styling const dedupedConnections = (sortBy( Object.values(connectionsById), @@ -192,10 +196,18 @@ export function dedupeConnections(response: ServiceMapResponse) { return prev.concat(connection); }, []); + // Add anomlies data + const dedupedNodesWithAnomliesData = addAnomaliesDataToNodes( + dedupedNodes, + anomalies + ); + // Put everything together in elements, with everything in the "data" property - const elements = [...dedupedConnections, ...dedupedNodes].map(element => ({ - data: element - })); + const elements = [...dedupedConnections, ...dedupedNodesWithAnomliesData].map( + element => ({ + data: element + }) + ); return { elements }; } diff --git a/x-pack/plugins/canvas/common/lib/constants.ts b/x-pack/plugins/canvas/common/lib/constants.ts index a37dc3fd6a7b3d..f2155d9202939c 100644 --- a/x-pack/plugins/canvas/common/lib/constants.ts +++ b/x-pack/plugins/canvas/common/lib/constants.ts @@ -18,7 +18,7 @@ export const API_ROUTE_WORKPAD_STRUCTURES = `${API_ROUTE}/workpad-structures`; export const API_ROUTE_CUSTOM_ELEMENT = `${API_ROUTE}/custom-element`; export const LOCALSTORAGE_PREFIX = `kibana.canvas`; export const LOCALSTORAGE_CLIPBOARD = `${LOCALSTORAGE_PREFIX}.clipboard`; -export const LOCALSTORAGE_LASTPAGE = 'canvas:lastpage'; +export const SESSIONSTORAGE_LASTPATH = 'lastPath:canvas'; export const FETCH_TIMEOUT = 30000; // 30 seconds export const CANVAS_USAGE_TYPE = 'canvas'; export const DEFAULT_WORKPAD_CSS = '.canvasPage {\n\n}'; diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 284023e74d137b..9c2aa821be2d53 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -10,8 +10,9 @@ import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Provider } from 'react-redux'; +import { BehaviorSubject } from 'rxjs'; -import { AppMountParameters, CoreStart, CoreSetup } from 'kibana/public'; +import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from 'kibana/public'; import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; // @ts-ignore Untyped local @@ -88,9 +89,10 @@ export const initializeCanvas = async ( coreStart: CoreStart, setupPlugins: CanvasSetupDeps, startPlugins: CanvasStartDeps, - registries: SetupRegistries + registries: SetupRegistries, + appUpdater: BehaviorSubject ) => { - startServices(coreSetup, coreStart, setupPlugins, startPlugins); + startServices(coreSetup, coreStart, setupPlugins, startPlugins, appUpdater); // Create Store const canvasStore = await createStore(coreSetup, setupPlugins); diff --git a/x-pack/plugins/canvas/public/components/app/index.js b/x-pack/plugins/canvas/public/components/app/index.js index de0d4c190eae61..750132dadb97da 100644 --- a/x-pack/plugins/canvas/public/components/app/index.js +++ b/x-pack/plugins/canvas/public/components/app/index.js @@ -8,6 +8,7 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; import { getAppReady, getBasePath } from '../../state/selectors/app'; import { appReady, appError } from '../../state/actions/app'; +import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; import { App as Component } from './app'; @@ -44,7 +45,8 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { export const App = compose( connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ - onRouteChange: () => undefined, + withKibana, + withProps(props => ({ + onRouteChange: props.kibana.services.canvas.navLink.updatePath, })) )(Component); diff --git a/x-pack/plugins/canvas/public/lib/clipboard.ts b/x-pack/plugins/canvas/public/lib/clipboard.ts index 11755807aa5336..cb940fd064a47b 100644 --- a/x-pack/plugins/canvas/public/lib/clipboard.ts +++ b/x-pack/plugins/canvas/public/lib/clipboard.ts @@ -4,22 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LOCALSTORAGE_CLIPBOARD } from '../../common/lib/constants'; -import { getWindow } from './get_window'; - -let storage: Storage; - -const getStorage = (): Storage => { - if (!storage) { - storage = new Storage(getWindow().localStorage); - } - - return storage; -}; +import { getLocalStorage } from './storage'; export const setClipboardData = (data: any) => { - getStorage().set(LOCALSTORAGE_CLIPBOARD, JSON.stringify(data)); + getLocalStorage().set(LOCALSTORAGE_CLIPBOARD, JSON.stringify(data)); }; -export const getClipboardData = () => getStorage().get(LOCALSTORAGE_CLIPBOARD); +export const getClipboardData = () => getLocalStorage().get(LOCALSTORAGE_CLIPBOARD); diff --git a/x-pack/plugins/canvas/public/lib/get_window.ts b/x-pack/plugins/canvas/public/lib/get_window.ts index 42c632f4a514f5..c8fb035d4d33f6 100644 --- a/x-pack/plugins/canvas/public/lib/get_window.ts +++ b/x-pack/plugins/canvas/public/lib/get_window.ts @@ -5,10 +5,18 @@ */ // return window if it exists, otherwise just return an object literal -const windowObj = { location: null, localStorage: {} as Window['localStorage'] }; +const windowObj = { + location: null, + localStorage: {} as Window['localStorage'], + sessionStorage: {} as Window['sessionStorage'], +}; export const getWindow = (): | Window - | { location: Location | null; localStorage: Window['localStorage'] } => { + | { + location: Location | null; + localStorage: Window['localStorage']; + sessionStorage: Window['sessionStorage']; + } => { return typeof window === 'undefined' ? windowObj : window; }; diff --git a/x-pack/plugins/canvas/public/lib/storage.ts b/x-pack/plugins/canvas/public/lib/storage.ts new file mode 100644 index 00000000000000..47c8cc741eaf3a --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/storage.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { getWindow } from './get_window'; + +export enum StorageType { + Local = 'localStorage', + Session = 'sessionStorage', +} + +const storages: { + [x in StorageType]: Storage | null; +} = { + [StorageType.Local]: null, + [StorageType.Session]: null, +}; + +const getStorage = (type: StorageType): Storage => { + const storage = storages[type] || new Storage(getWindow()[type]); + storages[type] = storage; + + return storage; +}; + +export const getLocalStorage = (): Storage => { + return getStorage(StorageType.Local); +}; + +export const getSessionStorage = (): Storage => { + return getStorage(StorageType.Session); +}; diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index bd39dcfb39fe22..c2192818e528bf 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { BehaviorSubject } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, AppMountParameters, + AppUpdater, DEFAULT_APP_CATEGORIES, } from '../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { initLoadingIndicator } from './lib/loading_indicator'; +import { getSessionStorage } from './lib/storage'; +import { SESSIONSTORAGE_LASTPATH } from '../common/lib/constants'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { ExpressionsSetup, ExpressionsStart } from '../../../../src/plugins/expressions/public'; import { DataPublicPluginSetup } from '../../../../src/plugins/data/public'; @@ -60,6 +64,7 @@ export type CanvasStart = void; /** @internal */ export class CanvasPlugin implements Plugin { + private appUpdater = new BehaviorSubject(() => ({})); // TODO: Do we want to completely move canvas_plugin_src into it's own plugin? private srcPlugin = new CanvasSrcPlugin(); @@ -68,12 +73,21 @@ export class CanvasPlugin this.srcPlugin.setup(core, { canvas: canvasApi }); + // Set the nav link to the last saved url if we have one in storage + const lastUrl = getSessionStorage().get(SESSIONSTORAGE_LASTPATH); + if (lastUrl) { + this.appUpdater.next(() => ({ + defaultPath: `#${lastUrl}`, + })); + } + core.application.register({ category: DEFAULT_APP_CATEGORIES.kibana, id: 'canvas', title: 'Canvas', euiIconType: 'canvasApp', - order: 0, // need to figure out if this is the proper order for us + order: 3000, + updater$: this.appUpdater, mount: async (params: AppMountParameters) => { // Load application bundle const { renderApp, initializeCanvas, teardownCanvas } = await import('./application'); @@ -81,7 +95,14 @@ export class CanvasPlugin // Get start services const [coreStart, depsStart] = await core.getStartServices(); - const canvasStore = await initializeCanvas(core, coreStart, plugins, depsStart, registries); + const canvasStore = await initializeCanvas( + core, + coreStart, + plugins, + depsStart, + registries, + this.appUpdater + ); const unmount = renderApp(coreStart, depsStart, params, canvasStore); diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index abc46beaa3e64e..42176f953c3318 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -4,16 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, CoreStart } from '../../../../../src/core/public'; +import { BehaviorSubject } from 'rxjs'; +import { CoreSetup, CoreStart, AppUpdater } from '../../../../../src/core/public'; import { CanvasSetupDeps, CanvasStartDeps } from '../plugin'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; +import { navLinkServiceFactory } from './nav_link'; export type CanvasServiceFactory = ( coreSetup: CoreSetup, coreStart: CoreStart, canvasSetupPlugins: CanvasSetupDeps, - canvasStartPlugins: CanvasStartDeps + canvasStartPlugins: CanvasStartDeps, + appUpdater: BehaviorSubject ) => Service; class CanvasServiceProvider { @@ -28,9 +31,16 @@ class CanvasServiceProvider { coreSetup: CoreSetup, coreStart: CoreStart, canvasSetupPlugins: CanvasSetupDeps, - canvasStartPlugins: CanvasStartDeps + canvasStartPlugins: CanvasStartDeps, + appUpdater: BehaviorSubject ) { - this.service = this.factory(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins); + this.service = this.factory( + coreSetup, + coreStart, + canvasSetupPlugins, + canvasStartPlugins, + appUpdater + ); } getService(): Service { @@ -51,20 +61,24 @@ export type ServiceFromProvider

= P extends CanvasServiceProvider ? export const services = { notify: new CanvasServiceProvider(notifyServiceFactory), platform: new CanvasServiceProvider(platformServiceFactory), + navLink: new CanvasServiceProvider(navLinkServiceFactory), }; export interface CanvasServices { notify: ServiceFromProvider; + platform: ServiceFromProvider; + navLink: ServiceFromProvider; } export const startServices = ( coreSetup: CoreSetup, coreStart: CoreStart, canvasSetupPlugins: CanvasSetupDeps, - canvasStartPlugins: CanvasStartDeps + canvasStartPlugins: CanvasStartDeps, + appUpdater: BehaviorSubject ) => { Object.entries(services).forEach(([key, provider]) => - provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins) + provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins, appUpdater) ); }; @@ -72,4 +86,8 @@ export const stopServices = () => { Object.entries(services).forEach(([key, provider]) => provider.stop()); }; -export const { notify: notifyService, platform: platformService } = services; +export const { + notify: notifyService, + platform: platformService, + navLink: navLinkService, +} = services; diff --git a/x-pack/plugins/canvas/public/services/nav_link.ts b/x-pack/plugins/canvas/public/services/nav_link.ts new file mode 100644 index 00000000000000..50614984580063 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/nav_link.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CanvasServiceFactory } from '.'; +import { SESSIONSTORAGE_LASTPATH } from '../../common/lib/constants'; +import { getSessionStorage } from '../lib/storage'; + +interface NavLinkService { + updatePath: (path: string) => void; +} + +export const navLinkServiceFactory: CanvasServiceFactory = ( + coreSetup, + coreStart, + setupPlugins, + startPlugins, + appUpdater +) => { + return { + updatePath: (path: string) => { + appUpdater.next(() => ({ + defaultPath: `#${path}`, + })); + + getSessionStorage().set(SESSIONSTORAGE_LASTPATH, path); + }, + }; +}; diff --git a/x-pack/plugins/dashboard_enhanced/public/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts index 772e032289bcee..c258a4148f84a0 100644 --- a/x-pack/plugins/dashboard_enhanced/public/plugin.ts +++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts @@ -11,6 +11,7 @@ import { DashboardDrilldownsService } from './services'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../advanced_ui_actions/public'; import { DrilldownsSetup, DrilldownsStart } from '../../drilldowns/public'; +import { DashboardStart } from '../../../../src/plugins/dashboard/public'; export interface SetupDependencies { advancedUiActions: AdvancedUiActionsSetup; @@ -25,6 +26,7 @@ export interface StartDependencies { drilldowns: DrilldownsStart; embeddable: EmbeddableStart; share: SharePluginStart; + dashboard: DashboardStart; } // eslint-disable-next-line diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index 0161836b2c5b97..f5926cd6961c2d 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -44,6 +44,12 @@ export class DashboardDrilldownsService { { advancedUiActions: uiActions }: SetupDependencies ) { const start = createStartServicesGetter(core.getStartServices); + const getDashboardUrlGenerator = () => { + const urlGenerator = start().plugins.dashboard.dashboardUrlGenerator; + if (!urlGenerator) + throw new Error('dashboardUrlGenerator is required for dashboard to dashboard drilldown'); + return urlGenerator; + }; const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ start }); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown); @@ -51,7 +57,10 @@ export class DashboardDrilldownsService { const actionFlyoutEditDrilldown = new FlyoutEditDrilldownAction({ start }); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutEditDrilldown); - const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({ start }); + const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({ + start, + getDashboardUrlGenerator, + }); uiActions.registerDrilldown(dashboardToDashboardDrilldown); } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index 18ee95cb57b3bc..d8465562f9302f 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -5,8 +5,7 @@ */ import { DashboardToDashboardDrilldown } from './drilldown'; -import { UrlGeneratorContract } from '../../../../../../../src/plugins/share/public'; -import { savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks'; +import { savedObjectsServiceMock, coreMock } from '../../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ActionContext, Config } from './types'; import { @@ -19,15 +18,16 @@ import { import { esFilters } from '../../../../../../../src/plugins/data/public'; // convenient to use real implementation here. -import { createDirectAccessDashboardLinkGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator'; +import { createDashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator'; +import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/public/url_generators'; import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; import { RangeSelectTriggerContext, ValueClickTriggerContext, } from '../../../../../../../src/plugins/embeddable/public'; +import { StartDependencies } from '../../../plugin'; import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public/core'; -import { StartDependencies } from '../../../plugin'; describe('.isConfigValid()', () => { const drilldown = new DashboardToDashboardDrilldown({} as any); @@ -105,23 +105,19 @@ describe('.execute() & getHref', () => { data: { actions: dataPluginActions, }, - share: { - urlGenerators: { - getUrlGenerator: () => - createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ - appBasePath: 'test', - useHashedUrl: false, - savedDashboardLoader: ({} as unknown) as SavedObjectLoader, - }) - ) as UrlGeneratorContract, - }, - }, }, self: {}, - })) as unknown) as StartServicesGetter< - Pick - >, + })) as unknown) as StartServicesGetter>, + getDashboardUrlGenerator: () => + new UrlGeneratorsService().setup(coreMock.createSetup()).registerUrlGenerator( + createDashboardUrlGenerator(() => + Promise.resolve({ + appBasePath: 'test', + useHashedUrl: false, + savedDashboardLoader: ({} as unknown) as SavedObjectLoader, + }) + ) + ), }); const selectRangeFiltersSpy = jest .spyOn(dataPluginActions, 'createFiltersFromRangeSelectAction') diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 848e77384f7f04..6d83b8443a828c 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; -import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; +import { DashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public'; import { ActionContext, Config } from './types'; import { CollectConfigContainer } from './components'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; @@ -22,7 +22,8 @@ import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_uti import { StartDependencies } from '../../../plugin'; export interface Params { - start: StartServicesGetter>; + start: StartServicesGetter>; + getDashboardUrlGenerator: () => DashboardUrlGenerator; } export class DashboardToDashboardDrilldown @@ -142,9 +143,7 @@ export class DashboardToDashboardDrilldown } } - const { plugins } = this.params.start(); - - return plugins.share.urlGenerators.getUrlGenerator(DASHBOARD_APP_URL_GENERATOR).createUrl({ + return this.params.getDashboardUrlGenerator().createUrl({ dashboardId: config.dashboardId, query: config.useCurrentFilters ? query : undefined, timeRange, diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index 6749b41e81fc70..52c53f32ff09b4 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -173,6 +173,42 @@ test('Create only mode', async () => { expect(await mockDynamicActionManager.state.get().events.length).toBe(1); }); +test('After switching between action factories state is restored', async () => { + const screen = render( + + ); + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + fireEvent.change(screen.getByLabelText(/name/i), { + target: { value: 'test' }, + }); + fireEvent.click(screen.getByText(/Go to URL/i)); + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: 'https://elastic.co' }, + }); + + // change to dashboard + fireEvent.click(screen.getByText(/change/i)); + fireEvent.click(screen.getByText(/Go to Dashboard/i)); + + // change back to url + fireEvent.click(screen.getByText(/change/i)); + fireEvent.click(screen.getByText(/Go to URL/i)); + + expect(screen.getByLabelText(/url/i)).toHaveValue('https://elastic.co'); + expect(screen.getByLabelText(/name/i)).toHaveValue('test'); + + fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); + await wait(() => expect(notifications.toasts.addSuccess).toBeCalled()); + expect(await (mockDynamicActionManager.state.get().events[0].action.config as any).url).toBe( + 'https://elastic.co' + ); +}); + test.todo("Error when can't fetch drilldown list"); test("Error when can't save drilldown changes", async () => { diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 0d4a67e325e4dd..5ebda079a15bf1 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -289,8 +289,8 @@ function useDrilldownsStateManager( await run(async () => { await actionManager.createEvent(action, selectedTriggers); notifications.toasts.addSuccess({ - title: toastDrilldownCreated.title, - text: toastDrilldownCreated.text(action.name), + title: toastDrilldownCreated.title(action.name), + text: toastDrilldownCreated.text, }); }); } @@ -303,8 +303,8 @@ function useDrilldownsStateManager( await run(async () => { await actionManager.updateEvent(drilldownId, action, selectedTriggers); notifications.toasts.addSuccess({ - title: toastDrilldownEdited.title, - text: toastDrilldownEdited.text(action.name), + title: toastDrilldownEdited.title(action.name), + text: toastDrilldownEdited.text, }); }); } @@ -320,8 +320,8 @@ function useDrilldownsStateManager( text: toastDrilldownDeleted.text, } : { - title: toastDrilldownsDeleted.title, - text: toastDrilldownsDeleted.text(drilldownIds.length), + title: toastDrilldownsDeleted.title(drilldownIds.length), + text: toastDrilldownsDeleted.text, } ); }); diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts index 31384860786efe..851439eccbe7ea 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts @@ -7,35 +7,41 @@ import { i18n } from '@kbn/i18n'; export const toastDrilldownCreated = { - title: i18n.translate( - 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle', + title: (drilldownName: string) => + i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle', + { + defaultMessage: 'Drilldown "{drilldownName}" created', + values: { + drilldownName, + }, + } + ), + text: i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText', { - defaultMessage: 'Drilldown created', + // TODO: remove `Save your dashboard before testing.` part + // when drilldowns are used not only in dashboard + // or after https://github.com/elastic/kibana/issues/65179 implemented + defaultMessage: 'Save your dashboard before testing.', } ), - text: (drilldownName: string) => - i18n.translate('xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText', { - defaultMessage: 'You created "{drilldownName}". Save dashboard before testing.', - values: { - drilldownName, - }, - }), }; export const toastDrilldownEdited = { - title: i18n.translate( - 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle', - { - defaultMessage: 'Drilldown edited', - } - ), - text: (drilldownName: string) => - i18n.translate('xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText', { - defaultMessage: 'You edited "{drilldownName}". Save dashboard before testing.', + title: (drilldownName: string) => + i18n.translate('xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle', { + defaultMessage: 'Drilldown "{drilldownName}" updated', values: { drilldownName, }, }), + text: i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText', + { + defaultMessage: 'Save your dashboard before testing.', + } + ), }; export const toastDrilldownDeleted = { @@ -48,28 +54,26 @@ export const toastDrilldownDeleted = { text: i18n.translate( 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText', { - defaultMessage: 'You deleted a drilldown.', + defaultMessage: 'Save your dashboard before testing.', } ), }; export const toastDrilldownsDeleted = { - title: i18n.translate( - 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle', - { - defaultMessage: 'Drilldowns deleted', - } - ), - text: (n: number) => + title: (n: number) => i18n.translate( - 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText', + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle', { - defaultMessage: 'You deleted {n} drilldowns', - values: { - n, - }, + defaultMessage: '{n} drilldowns deleted', + values: { n }, } ), + text: i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText', + { + defaultMessage: 'Save your dashboard before testing.', + } + ), }; export const toastDrilldownsCRUDError = i18n.translate( @@ -79,10 +83,3 @@ export const toastDrilldownsCRUDError = i18n.translate( description: 'Title for generic error toast when persisting drilldown updates failed', } ); - -export const toastDrilldownsFetchError = i18n.translate( - 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsFetchErrorTitle', - { - defaultMessage: 'Error fetching drilldowns', - } -); diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/i18n.ts b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/i18n.ts index 63dc95dabc0fbf..622376c5b40ad1 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/i18n.ts +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/i18n.ts @@ -10,7 +10,7 @@ export const txtHelpText = i18n.translate( 'xpack.drilldowns.components.DrilldownHelloBar.helpText', { defaultMessage: - 'Drilldowns provide the ability to define a new behavior when interacting with a panel. You can add multiple options or simply override the default filtering behavior.', + 'Drilldowns enable you to define new behaviors for interacting with panels. You can add multiple actions and override the default filter.', } ); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index 8541aae06ff0c7..1f775a5ff103f1 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -41,6 +41,72 @@ export interface FlyoutDrilldownWizardProps void; + setActionConfig: (actionConfig: object) => void; + setActionFactory: (actionFactory?: ActionFactory) => void; + } +] { + const [wizardConfig, setWizardConfig] = useState( + () => + initialDrilldownWizardConfig ?? { + name: '', + } + ); + const [actionConfigCache, setActionConfigCache] = useState>( + initialDrilldownWizardConfig?.actionFactory + ? { + [initialDrilldownWizardConfig.actionFactory + .id]: initialDrilldownWizardConfig.actionConfig!, + } + : {} + ); + + return [ + wizardConfig, + { + setName: (name: string) => { + setWizardConfig({ + ...wizardConfig, + name, + }); + }, + setActionConfig: (actionConfig: object) => { + setWizardConfig({ + ...wizardConfig, + actionConfig, + }); + }, + setActionFactory: (actionFactory?: ActionFactory) => { + if (actionFactory) { + setWizardConfig({ + ...wizardConfig, + actionFactory, + actionConfig: actionConfigCache[actionFactory.id] ?? actionFactory.createConfig(), + }); + } else { + if (wizardConfig.actionFactory?.id) { + setActionConfigCache({ + ...actionConfigCache, + [wizardConfig.actionFactory.id]: wizardConfig.actionConfig!, + }); + } + + setWizardConfig({ + ...wizardConfig, + actionFactory: undefined, + actionConfig: undefined, + }); + } + }, + }, + ]; +} + export function FlyoutDrilldownWizard({ onClose, onBack, @@ -53,11 +119,8 @@ export function FlyoutDrilldownWizard) { - const [wizardConfig, setWizardConfig] = useState( - () => - initialDrilldownWizardConfig ?? { - name: '', - } + const [wizardConfig, { setActionFactory, setActionConfig, setName }] = useWizardConfigState( + initialDrilldownWizardConfig ); const isActionValid = ( @@ -95,35 +158,11 @@ export function FlyoutDrilldownWizard { - setWizardConfig({ - ...wizardConfig, - name: newName, - }); - }} + onNameChange={setName} actionConfig={wizardConfig.actionConfig} - onActionConfigChange={newActionConfig => { - setWizardConfig({ - ...wizardConfig, - actionConfig: newActionConfig, - }); - }} + onActionConfigChange={setActionConfig} currentActionFactory={wizardConfig.actionFactory} - onActionFactoryChange={actionFactory => { - if (!actionFactory) { - setWizardConfig({ - ...wizardConfig, - actionFactory: undefined, - actionConfig: undefined, - }); - } else { - setWizardConfig({ - ...wizardConfig, - actionFactory, - actionConfig: actionFactory.createConfig(), - }); - } - }} + onActionFactoryChange={setActionFactory} actionFactories={drilldownActionFactories} actionFactoryContext={actionFactoryContext!} /> diff --git a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx index 93b3710bf6cc66..3bed81a9719216 100644 --- a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -19,7 +19,7 @@ export interface FormDrilldownWizardProps { onNameChange?: (name: string) => void; currentActionFactory?: ActionFactory; - onActionFactoryChange?: (actionFactory: ActionFactory | null) => void; + onActionFactoryChange?: (actionFactory?: ActionFactory) => void; actionFactoryContext: object; actionConfig?: object; diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts index 839379387e0943..158641cd97695e 100644 --- a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts +++ b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts @@ -47,6 +47,40 @@ describe('PanelNotificationsAction', () => { }); }); + describe('getDisplayNameTooltip', () => { + test('returns empty string if embeddable has no event', async () => { + const context = createContext(); + const action = new PanelNotificationsAction(); + + const name = await action.getDisplayNameTooltip(context); + expect(name).toBe(''); + }); + + test('returns "1 drilldown" if embeddable has one event', async () => { + const context = createContext([{}]); + const action = new PanelNotificationsAction(); + + const name = await action.getDisplayNameTooltip(context); + expect(name).toBe('Panel has 1 drilldown'); + }); + + test('returns "2 drilldowns" if embeddable has two events', async () => { + const context = createContext([{}, {}]); + const action = new PanelNotificationsAction(); + + const name = await action.getDisplayNameTooltip(context); + expect(name).toBe('Panel has 2 drilldowns'); + }); + + test('returns "3 drilldowns" if embeddable has three events', async () => { + const context = createContext([{}, {}, {}]); + const action = new PanelNotificationsAction(); + + const name = await action.getDisplayNameTooltip(context); + expect(name).toBe('Panel has 3 drilldowns'); + }); + }); + describe('isCompatible', () => { test('returns false if not in "edit" mode', async () => { const context = createContext([{}]); diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts index 19e0ac2a5a6d8e..165ce24c13ea3d 100644 --- a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts +++ b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts @@ -4,10 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; import { ViewMode } from '../../../../../src/plugins/embeddable/public'; import { EnhancedEmbeddableContext, EnhancedEmbeddable } from '../types'; +export const txtOneDrilldown = i18n.translate( + 'xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown', + { + defaultMessage: 'Panel has 1 drilldown', + } +); + +export const txtManyDrilldowns = (count: number) => + i18n.translate('xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns', { + defaultMessage: 'Panel has {count} drilldowns', + values: { + count: String(count), + }, + }); + export const ACTION_PANEL_NOTIFICATIONS = 'ACTION_PANEL_NOTIFICATIONS'; /** @@ -25,6 +41,11 @@ export class PanelNotificationsAction implements ActionDefinition { + const count = this.getEventCount(embeddable); + return !count ? '' : count === 1 ? txtOneDrilldown : txtManyDrilldowns(count); + }; + public readonly isCompatible = async ({ embeddable }: EnhancedEmbeddableContext) => { if (embeddable.getInput().viewMode !== ViewMode.EDIT) return false; return this.getEventCount(embeddable) > 0; diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 9e7aedcc90bb5e..ff8add42a50851 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 comes configured successfully', + message: 'elasticsearch communications configured successfully', status: HostPolicyResponseActionStatus.success, }, configure_kernel: { diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 181b0e7ab38846..b39b2e89ee1508 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -644,6 +644,9 @@ export interface HostPolicyResponseActions { read_malware_config: HostPolicyResponseActionDetails; } +/** + * policy configurations returned by the endpoint in response to a user applying a policy + */ export type HostPolicyResponseConfiguration = HostPolicyResponse['endpoint']['policy']['applied']['response']['configurations']; interface HostPolicyResponseConfigurationStatus { 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 aa04f2fdff57f4..8714141364e7de 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 @@ -12,7 +12,6 @@ import { HostPolicyResponseActions, HostPolicyResponseConfiguration, Immutable, - ImmutableArray, } from '../../../../../../common/types'; import { formatResponse } from './policy_response_friendly_names'; import { POLICY_STATUS_TO_HEALTH_COLOR } from './host_constants'; @@ -51,7 +50,7 @@ const ResponseActions = memo( actions, actionStatus, }: { - actions: ImmutableArray; + actions: Immutable>; actionStatus: Partial; }) => { return ( 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 index 251b3e86bc3f9d..502aa66b244215 100644 --- 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 @@ -159,8 +159,7 @@ responseMap.set( ); /** - * Takes in the snake-cased response from the API and - * removes the underscores and capitalizes the string. + * Maps a server provided value to corresponding i18n'd string. */ export function formatResponse(responseString: string) { if (responseMap.has(responseString)) { 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 f7eafff137f519..39529e7c11ab1b 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 ( 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 15c3ef0b845624..84fbc04aa5a313 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 00000000000000..eac68770d3de23 --- /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 00000000000000..19bf6973472ff3 --- /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 00000000000000..2bfaa884a01329 --- /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 00000000000000..4af5f82d851e38 --- /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 fa6bee56349e95..afdc039ae77d2a 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 c8c8ef8bfe9b3d..58242ec35018c8 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 00000000000000..8989e85d9f188d --- /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 0cf5bf3f4453ff..f516dfdb372ce7 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 6b33d4450c3ae2..c84756cab8e886 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 cb9b464d270ce3..c1a2b195a3f576 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 68b76a1203ad52..7185016029e00e 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 7f434d6f834b2b..f06b292bc33c8e 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 f79741d9a1a9f1..4278598dfc7c16 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 a97e3b227311c7..569af5d21cdb09 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 a91231352c1684..a44fd2257f52b2 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 0cf22946bf60a6..f99aa4d1eca9a5 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 fec8e49a1991ca..3e91e97eef618a 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 03c774227924ea..2046675881c29f 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 489424a07e04d8..854270f313e59c 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 97a7d205c13553..1c079c8d5cf879 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 6df86d561a532d..c0d922e0d1d373 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 3c4d6b08ebe449..f4aa17bf6fed92 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 0431ea472643b7..4b610ff0b401df 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 cece26618ced87..306e0448df379b 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 316fee55526a3b..46dc1176f62b4b 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 a9d26b953b96e0..280ea5c3dd28ce 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 cf9b57dcbcb14d..d74dd435ecdae0 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/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index a600d59865cccd..77147d1b3b2b78 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -118,10 +118,7 @@ export const ExpressionChart: React.FC = ({ const series = { ...firstSeries, rows: firstSeries.rows.map(row => { - const newRow: MetricsExplorerRow = { - timestamp: row.timestamp, - metric_0: row.metric_0 || null, - }; + const newRow: MetricsExplorerRow = { ...row }; thresholds.forEach((thresholdValue, index) => { newRow[`metric_threshold_${index}`] = thresholdValue; }); @@ -224,7 +221,7 @@ export const ExpressionChart: React.FC = ({ /> ) : null} - {isAbove ? ( + {isAbove && first(expression.threshold) != null ? ( = props => { const { sourceStatus } = useLogSourceContext(); useMount(() => { - for (const [key, value] of Object.entries(DEFAULT_EXPRESSION)) { + for (const [key, value] of Object.entries({ ...DEFAULT_EXPRESSION, ...alertParams })) { setAlertParams(key, value); setHasSetDefaults(true); } 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 2abef7d71e65aa..6bbd67ce932c69 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 5ff5cd4db7168f..16751fabd6e964 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 08049183d0a186..018f89fbb23c43 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 88b1441f0ba7c1..363b1b76271041 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 712d625052140a..bc25d7c49b1297 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 9667272eb24179..88e6ea8be4325e 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 dbf71665ea869a..91362d9098e344 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 3a2c33d1c824c5..ebb8243369b3c4 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 597977d9d2735d..dcd1c1d949971d 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/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 559422584f579a..f773c843d12fdd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -46,6 +46,24 @@ export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptions field: derivativeId, }, ]; + } else if (metric.aggregation === 'p95' || metric.aggregation === 'p99') { + const percentileValue = metric.aggregation === 'p95' ? '95' : '99'; + return [ + { + id: uuid.v1(), + type: 'percentile', + field: metric.field, + percentiles: [ + { + id: uuid.v1(), + value: percentileValue, + mode: 'line', + percentile: '', + shade: 0.2, + }, + ], + }, + ]; } else { return [ { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx index 3b84fcbc34836b..223318da8cf46a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx @@ -20,6 +20,7 @@ import { MetricsExplorerOptionsMetric, MetricsExplorerChartType, } from '../hooks/use_metrics_explorer_options'; +import { getMetricId } from './helpers/get_metric_id'; type NumberOrString = string | number; @@ -45,10 +46,12 @@ export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opac colorTransformer(MetricsExplorerColor.color0); const yAccessors = Array.isArray(id) - ? id.map(i => `metric_${i}`).slice(id.length - 1, id.length) - : [`metric_${id}`]; + ? id.map(i => getMetricId(metric, i)).slice(id.length - 1, id.length) + : [getMetricId(metric, id)]; const y0Accessors = - Array.isArray(id) && id.length > 1 ? id.map(i => `metric_${i}`).slice(0, 1) : undefined; + Array.isArray(id) && id.length > 1 + ? id.map(i => getMetricId(metric, i)).slice(0, 1) + : undefined; const chartId = `series-${series.id}-${yAccessors.join('-')}`; const seriesAreaStyle: RecursivePartial = { @@ -85,8 +88,10 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => (metric.color && colorTransformer(metric.color)) || colorTransformer(MetricsExplorerColor.color0); - const yAccessor = `metric_${id}`; - const chartId = `series-${series.id}-${yAccessor}`; + const yAccessors = Array.isArray(id) + ? id.map(i => getMetricId(metric, i)).slice(id.length - 1, id.length) + : [getMetricId(metric, id)]; + const chartId = `series-${series.id}-${yAccessors.join('-')}`; const seriesBarStyle: RecursivePartial = { rectBorder: { @@ -100,13 +105,13 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => }; 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 9414eb7d3e5640..7d4f35b19da7de 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 00000000000000..995d415ef3c8f2 --- /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 00000000000000..449bc03a922cfb --- /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/infra/server/lib/alerting/metric_threshold/create_percentile_aggregation.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/create_percentile_aggregation.ts new file mode 100644 index 00000000000000..2c83f6ecfd7056 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/create_percentile_aggregation.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Aggregators } from './types'; +export const createPercentileAggregation = ( + type: Aggregators.P95 | Aggregators.P99, + field: string +) => { + const value = type === Aggregators.P95 ? 95 : 99; + return { + aggregatedValue: { + percentiles: { + field, + percents: [value], + keyed: false, + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 2531e939792af2..ed5efc1473953f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -233,6 +233,58 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); + describe('querying with the p99 aggregator', () => { + const instanceID = 'test-*'; + const execute = (comparator: Comparator, threshold: number[]) => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator, + threshold, + aggType: 'p99', + metric: 'test.metric.2', + }, + ], + }, + }); + test('alerts based on the p99 values', async () => { + await execute(Comparator.GT, [1]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute(Comparator.LT, [1]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); + }); + describe('querying with the p95 aggregator', () => { + const instanceID = 'test-*'; + const execute = (comparator: Comparator, threshold: number[]) => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator, + threshold, + aggType: 'p95', + metric: 'test.metric.1', + }, + ], + }, + }); + test('alerts based on the p95 values', async () => { + await execute(Comparator.GT, [0.25]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute(Comparator.LT, [0.95]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); + }); describe("querying a metric that hasn't reported data", () => { const instanceID = 'test-*'; const execute = (alertOnNoData: boolean) => diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index ec9389537835bb..71bee3209bf532 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.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 { mapValues } from 'lodash'; +import { mapValues, first } from 'lodash'; import { i18n } from '@kbn/i18n'; import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; @@ -21,12 +21,16 @@ import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/ser import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; import { InfraBackendLibs } from '../../infra_types'; +import { createPercentileAggregation } from './create_percentile_aggregation'; const TOTAL_BUCKETS = 5; interface Aggregation { aggregatedIntervals: { - buckets: Array<{ aggregatedValue: { value: number }; doc_count: number }>; + buckets: Array<{ + aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; + doc_count: number; + }>; }; } @@ -47,6 +51,12 @@ const getCurrentValueFromAggregations = ( if (aggType === Aggregators.COUNT) { return mostRecentBucket.doc_count; } + if (aggType === Aggregators.P95 || aggType === Aggregators.P99) { + const values = mostRecentBucket.aggregatedValue?.values || []; + const firstValue = first(values); + if (!firstValue) return null; + return firstValue.value; + } const { value } = mostRecentBucket.aggregatedValue; return value; } catch (e) { @@ -86,6 +96,8 @@ export const getElasticsearchMetricQuery = ( ? {} : aggType === Aggregators.RATE ? networkTraffic('aggregatedValue', metric) + : aggType === Aggregators.P95 || aggType === Aggregators.P99 + ? createPercentileAggregation(aggType, metric) : { aggregatedValue: { [aggType]: { @@ -275,7 +287,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s ); // Because each alert result has the same group definitions, just grap the groups from the first one. - const groups = Object.keys(alertResults[0]); + const groups = Object.keys(first(alertResults)); for (const group of groups) { const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index fa55f80e472de3..25b709d6afc51b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -7,22 +7,22 @@ const bucketsA = [ { doc_count: 2, - aggregatedValue: { value: 0.5 }, + aggregatedValue: { value: 0.5, values: [{ key: 95.0, value: 0.5 }] }, }, { doc_count: 3, - aggregatedValue: { value: 1.0 }, + aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] }, }, ]; const bucketsB = [ { doc_count: 4, - aggregatedValue: { value: 2.5 }, + aggregatedValue: { value: 2.5, values: [{ key: 99.0, value: 2.5 }] }, }, { doc_count: 5, - aggregatedValue: { value: 3.5 }, + aggregatedValue: { value: 3.5, values: [{ key: 99.0, value: 3.5 }] }, }, ]; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index 18f5503fe2c9e3..76ddd107bd728e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -23,6 +23,8 @@ export enum Aggregators { MAX = 'max', RATE = 'rate', CARDINALITY = 'cardinality', + P95 = 'p95', + P99 = 'p99', } export enum AlertStates { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx index 818b365d5be12c..2f06d1d8703c25 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx @@ -13,6 +13,7 @@ import { // @ts-ignore EuiSearchBar, EuiText, + Query, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -35,9 +36,28 @@ export function PackageListGrid({ list, showInstalledBadge, }: ListProps) { + const initialQuery = EuiSearchBar.Query.MATCH_ALL; + + const [query, setQuery] = useState(initialQuery); const [searchTerm, setSearchTerm] = useState(''); const localSearchRef = useLocalSearch(list); + const onQueryChange = ({ + // eslint-disable-next-line no-shadow + query, + queryText: userInput, + error, + }: { + query: Query | null; + queryText: string; + error: { message: string } | null; + }) => { + if (!error) { + setQuery(query); + setSearchTerm(userInput); + } + }; + const controlsContent = ; let gridContent: JSX.Element; @@ -59,16 +79,14 @@ export function PackageListGrid({ {controlsContent} { - setSearchTerm(userInput); - }} + onChange={onQueryChange} /> {gridContent} diff --git a/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/handler.ts index 9d3eb5360dbe35..94fc6de6096139 100644 --- a/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/handler.ts @@ -58,6 +58,12 @@ export const postEnrollmentApiKeyHandler: RequestHandler< return response.ok({ body }); } catch (e) { + if (e.isBoom) { + return response.customError({ + statusCode: e.output.statusCode, + body: { message: e.message }, + }); + } return response.customError({ statusCode: 500, body: { message: e.message }, diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index 1ac812c3380cd9..3b003f47eb6f92 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts @@ -5,6 +5,7 @@ */ import uuid from 'uuid'; +import Boom from 'boom'; import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; @@ -106,6 +107,9 @@ export async function generateEnrollmentAPIKey( ) { const id = uuid.v4(); const { name: providedKeyName } = data; + if (data.configId) { + await validateConfigId(soClient, data.configId); + } const configId = data.configId ?? (await agentConfigService.getDefaultAgentConfigId(soClient)); const name = providedKeyName ? `${providedKeyName} (${id})` : id; const key = await createAPIKey(soClient, name, { @@ -143,6 +147,17 @@ export async function generateEnrollmentAPIKey( return getEnrollmentAPIKey(soClient, so.id); } +async function validateConfigId(soClient: SavedObjectsClientContract, configId: string) { + try { + await agentConfigService.get(soClient, configId); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + throw Boom.badRequest(`Agent config ${configId} does not exist`); + } + throw e; + } +} + function savedObjectToEnrollmentApiKey({ error, attributes, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index f6db5dfe353ea5..6cdcb8782f38e6 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -372,12 +372,11 @@ export const ensureDefaultIndices = async (callCluster: CallESAsCurrentUser) => Promise.all( Object.keys(IndexPatternType).map(async indexPattern => { const defaultIndexPatternName = indexPattern + INDEX_PATTERN_PLACEHOLDER_SUFFIX; - const indexExists = await doesIndexExist(defaultIndexPatternName, callCluster); + const indexExists = await callCluster('indices.exists', { index: defaultIndexPatternName }); if (!indexExists) { try { - await callCluster('transport.request', { - method: 'PUT', - path: `/${defaultIndexPatternName}`, + await callCluster('indices.create', { + index: defaultIndexPatternName, body: { mappings: { properties: { @@ -387,20 +386,9 @@ export const ensureDefaultIndices = async (callCluster: CallESAsCurrentUser) => }, }); } catch (putErr) { - throw new Error(`${defaultIndexPatternName} could not be created`); + // throw new Error(`${defaultIndexPatternName} could not be created`); + throw new Error(putErr); } } }) ); - -export const doesIndexExist = async (indexName: string, callCluster: CallESAsCurrentUser) => { - try { - await callCluster('transport.request', { - method: 'HEAD', - path: indexName, - }); - return true; - } catch (err) { - return false; - } -}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts new file mode 100644 index 00000000000000..3a2ee7ef8b008a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon, { SinonFakeServer } from 'sinon'; + +import { API_BASE_PATH } from '../../../common/constants'; + +// Register helpers to mock HTTP Requests +const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const setLoadPipelinesResponse = (response?: any[], error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', API_BASE_PATH, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setLoadPipelineResponse = (response?: {}, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/:name`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setDeletePipelineResponse = (response?: object) => { + server.respondWith('DELETE', `${API_BASE_PATH}/:name`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + + const setCreatePipelineResponse = (response?: object, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', API_BASE_PATH, [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + + return { + setLoadPipelinesResponse, + setLoadPipelineResponse, + setDeletePipelineResponse, + setCreatePipelineResponse, + }; +}; + +export const init = () => { + const server = sinon.fakeServer.create(); + server.respondImmediately = true; + + // Define default response for unhandled requests. + // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, + // and we can mock them all with a 200 instead of mocking each one individually. + server.respondWith([200, {}, 'DefaultMockedResponse']); + + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + + return { + server, + httpRequestsMockHelpers, + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts new file mode 100644 index 00000000000000..6216119c5d1d12 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setup as pipelinesListSetup } from './pipelines_list.helpers'; +import { setup as pipelinesCreateSetup } from './pipelines_create.helpers'; +import { setup as pipelinesCloneSetup } from './pipelines_clone.helpers'; +import { setup as pipelinesEditSetup } from './pipelines_edit.helpers'; + +export { nextTick, getRandomString, findTestSubject } from '../../../../../test_utils'; + +export { setupEnvironment } from './setup_environment'; + +export const pageHelpers = { + pipelinesList: { setup: pipelinesListSetup }, + pipelinesCreate: { setup: pipelinesCreateSetup }, + pipelinesClone: { setup: pipelinesCloneSetup }, + pipelinesEdit: { setup: pipelinesEditSetup }, +}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts new file mode 100644 index 00000000000000..d56e92a2419c42 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TestBed } from '../../../../../test_utils'; + +export const getFormActions = (testBed: TestBed) => { + const { find, form } = testBed; + + // User actions + const clickSubmitButton = () => { + find('submitButton').simulate('click'); + }; + + const clickTestPipelineButton = () => { + find('testPipelineButton').simulate('click'); + }; + + const clickShowRequestLink = () => { + find('showRequestLink').simulate('click'); + }; + + const toggleVersionSwitch = () => { + form.toggleEuiSwitch('versionToggle'); + }; + + const toggleOnFailureSwitch = () => { + form.toggleEuiSwitch('onFailureToggle'); + }; + + return { + clickSubmitButton, + clickShowRequestLink, + toggleVersionSwitch, + toggleOnFailureSwitch, + clickTestPipelineButton, + }; +}; + +export type PipelineFormTestSubjects = + | 'submitButton' + | 'pageTitle' + | 'savePipelineError' + | 'pipelineForm' + | 'versionToggle' + | 'versionField' + | 'nameField.input' + | 'descriptionField.input' + | 'processorsField' + | 'onFailureToggle' + | 'onFailureEditor' + | 'testPipelineButton' + | 'showRequestLink' + | 'requestFlyout' + | 'requestFlyout.title' + | 'testPipelineFlyout' + | 'testPipelineFlyout.title' + | 'documentationLink'; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts new file mode 100644 index 00000000000000..2791ffc32c8580 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.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 { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils'; +import { BASE_PATH } from '../../../common/constants'; +import { PipelinesClone } from '../../../public/application/sections/pipelines_clone'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; +import { WithAppDependencies } from './setup_environment'; + +export type PipelinesCloneTestBed = TestBed & { + actions: ReturnType; +}; + +export const PIPELINE_TO_CLONE = { + name: 'my_pipeline', + description: 'pipeline description', + processors: [ + { + set: { + field: 'foo', + value: 'new', + }, + }, + ], +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}create/${PIPELINE_TO_CLONE.name}`], + componentRoutePath: `${BASE_PATH}create/:name`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(PipelinesClone), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts new file mode 100644 index 00000000000000..54a62a8357e524 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils'; +import { BASE_PATH } from '../../../common/constants'; +import { PipelinesCreate } from '../../../public/application/sections/pipelines_create'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; +import { WithAppDependencies } from './setup_environment'; + +export type PipelinesCreateTestBed = TestBed & { + actions: ReturnType; +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}/create`], + componentRoutePath: `${BASE_PATH}/create`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(PipelinesCreate), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts new file mode 100644 index 00000000000000..12320f034a8193 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.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 { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils'; +import { BASE_PATH } from '../../../common/constants'; +import { PipelinesEdit } from '../../../public/application/sections/pipelines_edit'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; +import { WithAppDependencies } from './setup_environment'; + +export type PipelinesEditTestBed = TestBed & { + actions: ReturnType; +}; + +export const PIPELINE_TO_EDIT = { + name: 'my_pipeline', + description: 'pipeline description', + processors: [ + { + set: { + field: 'foo', + value: 'new', + }, + }, + ], +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}edit/${PIPELINE_TO_EDIT.name}`], + componentRoutePath: `${BASE_PATH}edit/:name`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(PipelinesEdit), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts new file mode 100644 index 00000000000000..0f9745981c18bb --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts @@ -0,0 +1,105 @@ +/* + * 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 { BASE_PATH } from '../../../common/constants'; +import { + registerTestBed, + TestBed, + TestBedConfig, + findTestSubject, + nextTick, +} from '../../../../../test_utils'; +import { PipelinesList } from '../../../public/application/sections/pipelines_list'; +import { WithAppDependencies } from './setup_environment'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [BASE_PATH], + componentRoutePath: BASE_PATH, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(PipelinesList), testBedConfig); + +export type PipelineListTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const { find } = testBed; + + /** + * User Actions + */ + const clickReloadButton = () => { + find('reloadButton').simulate('click'); + }; + + const clickPipelineAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('pipelinesTable'); + const pipelineLink = findTestSubject(rows[index].reactWrapper, 'pipelineDetailsLink'); + + await act(async () => { + const { href } = pipelineLink.props(); + router.navigateTo(href!); + await nextTick(); + component.update(); + }); + }; + + const clickActionMenu = (pipelineName: string) => { + const { component } = testBed; + + // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions" + component.find(`div[id="${pipelineName}-actions"] button`).simulate('click'); + }; + + const clickPipelineAction = (pipelineName: string, action: 'edit' | 'clone' | 'delete') => { + const actions = ['edit', 'clone', 'delete']; + const { component } = testBed; + + clickActionMenu(pipelineName); + + component + .find('.euiContextMenuItem') + .at(actions.indexOf(action)) + .simulate('click'); + }; + + return { + clickReloadButton, + clickPipelineAt, + clickPipelineAction, + clickActionMenu, + }; +}; + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; + +export type PipelineListTestSubjects = + | 'appTitle' + | 'documentationLink' + | 'createPipelineButton' + | 'pipelinesTable' + | 'pipelineDetails' + | 'pipelineDetails.title' + | 'deletePipelinesConfirmation' + | 'emptyList' + | 'emptyList.title' + | 'sectionLoading' + | 'pipelineLoadError' + | 'reloadButton'; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx new file mode 100644 index 00000000000000..3243d665832f20 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import React from 'react'; + +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { + notificationServiceMock, + fatalErrorsServiceMock, + docLinksServiceMock, + injectedMetadataServiceMock, +} from '../../../../../../src/core/public/mocks'; + +import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/public/mocks'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { HttpService } from '../../../../../../src/core/public/http'; + +import { + breadcrumbService, + documentationService, + uiMetricService, + apiService, +} from '../../../public/application/services'; + +import { init as initHttpRequests } from './http_requests'; + +const httpServiceSetupMock = new HttpService().setup({ + injectedMetadata: injectedMetadataServiceMock.createSetupContract(), + fatalErrors: fatalErrorsServiceMock.createSetupContract(), +}); + +const appServices = { + breadcrumbs: breadcrumbService, + metric: uiMetricService, + documentation: documentationService, + api: apiService, + notifications: notificationServiceMock.createSetupContract(), +}; + +export const setupEnvironment = () => { + uiMetricService.setup(usageCollectionPluginMock.createSetupContract()); + apiService.setup(httpServiceSetupMock, uiMetricService); + documentationService.setup(docLinksServiceMock.createStartContract()); + breadcrumbService.setup(() => {}); + + const { server, httpRequestsMockHelpers } = initHttpRequests(); + + return { + server, + httpRequestsMockHelpers, + }; +}; + +export const WithAppDependencies = (Comp: any) => (props: any) => ( + + + +); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx new file mode 100644 index 00000000000000..29013678922132 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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 { setupEnvironment, pageHelpers } from './helpers'; +import { PIPELINE_TO_CLONE, PipelinesCloneTestBed } from './helpers/pipelines_clone.helpers'; + +const { setup } = pageHelpers.pipelinesClone; + +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), +})); + +describe('', () => { + let testBed: PipelinesCloneTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPipelineResponse(PIPELINE_TO_CLONE); + + await act(async () => { + testBed = await setup(); + await testBed.waitFor('pipelineForm'); + }); + }); + + test('should render the correct page header', () => { + const { exists, find } = testBed; + + // Verify page title + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual('Create pipeline'); + + // Verify documentation link + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Create pipeline docs'); + }); + + describe('form submission', () => { + it('should send the correct payload', async () => { + const { actions, waitFor } = testBed; + + await act(async () => { + actions.clickSubmitButton(); + await waitFor('pipelineForm', 0); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + ...PIPELINE_TO_CLONE, + name: `${PIPELINE_TO_CLONE.name}-copy`, + }; + + expect(JSON.parse(latestRequest.requestBody)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx new file mode 100644 index 00000000000000..6acb6369e2e907 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { PipelinesCreateTestBed } from './helpers/pipelines_create.helpers'; + +const { setup } = pageHelpers.pipelinesCreate; + +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), +})); + +describe('', () => { + let testBed: PipelinesCreateTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('on component mount', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + await testBed.waitFor('pipelineForm'); + }); + }); + + test('should render the correct page header', () => { + const { exists, find } = testBed; + + // Verify page title + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual('Create pipeline'); + + // Verify documentation link + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Create pipeline docs'); + }); + + test('should toggle the version field', async () => { + const { actions, component, exists } = testBed; + + // Version field should be hidden by default + expect(exists('versionField')).toBe(false); + + await act(async () => { + actions.toggleVersionSwitch(); + await nextTick(); + component.update(); + }); + + expect(exists('versionField')).toBe(true); + }); + + test('should toggle the on-failure processors editor', async () => { + const { actions, component, exists } = testBed; + + // On-failure editor should be hidden by default + expect(exists('onFailureEditor')).toBe(false); + + await act(async () => { + actions.toggleOnFailureSwitch(); + await nextTick(); + component.update(); + }); + + expect(exists('onFailureEditor')).toBe(true); + }); + + test('should show the request flyout', async () => { + const { actions, component, find, exists } = testBed; + + await act(async () => { + actions.clickShowRequestLink(); + await nextTick(); + component.update(); + }); + + // Verify request flyout opens + expect(exists('requestFlyout')).toBe(true); + expect(find('requestFlyout.title').text()).toBe('Request'); + }); + + describe('form validation', () => { + test('should prevent form submission if required fields are missing', async () => { + const { form, actions, component, find } = testBed; + + await act(async () => { + actions.clickSubmitButton(); + await nextTick(); + component.update(); + }); + + expect(form.getErrorsMessages()).toEqual(['Name is required.']); + expect(find('submitButton').props().disabled).toEqual(true); + + // Add required fields and verify button is enabled again + form.setInputValue('nameField.input', 'my_pipeline'); + + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(find('submitButton').props().disabled).toEqual(false); + }); + }); + + describe('form submission', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + + const { waitFor, form } = testBed; + + await waitFor('pipelineForm'); + + form.setInputValue('nameField.input', 'my_pipeline'); + form.setInputValue('descriptionField.input', 'pipeline description'); + }); + }); + + test('should send the correct payload', async () => { + const { actions, waitFor } = testBed; + + await act(async () => { + actions.clickSubmitButton(); + await waitFor('pipelineForm', 0); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + name: 'my_pipeline', + description: 'pipeline description', + processors: [], + }; + + expect(JSON.parse(latestRequest.requestBody)).toEqual(expected); + }); + + test('should surface API errors from the request', async () => { + const { actions, find, exists, waitFor } = testBed; + + const error = { + status: 409, + error: 'Conflict', + message: `There is already a pipeline with name 'my_pipeline'.`, + }; + + httpRequestsMockHelpers.setCreatePipelineResponse(undefined, { body: error }); + + await act(async () => { + actions.clickSubmitButton(); + await waitFor('savePipelineError'); + }); + + expect(exists('savePipelineError')).toBe(true); + expect(find('savePipelineError').text()).toContain(error.message); + }); + }); + + describe('test pipeline', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + + const { waitFor } = testBed; + + await waitFor('pipelineForm'); + }); + }); + + test('should open the test pipeline flyout', async () => { + const { actions, exists, find, waitFor } = testBed; + + await act(async () => { + actions.clickTestPipelineButton(); + await waitFor('testPipelineFlyout'); + }); + + // Verify test pipeline flyout opens + expect(exists('testPipelineFlyout')).toBe(true); + expect(find('testPipelineFlyout.title').text()).toBe('Test pipeline'); + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx new file mode 100644 index 00000000000000..477eec83f876d7 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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 { setupEnvironment, pageHelpers } from './helpers'; +import { PIPELINE_TO_EDIT, PipelinesEditTestBed } from './helpers/pipelines_edit.helpers'; + +const { setup } = pageHelpers.pipelinesEdit; + +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), +})); + +describe('', () => { + let testBed: PipelinesEditTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPipelineResponse(PIPELINE_TO_EDIT); + + await act(async () => { + testBed = await setup(); + await testBed.waitFor('pipelineForm'); + }); + }); + + test('should render the correct page header', () => { + const { exists, find } = testBed; + + // Verify page title + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual(`Edit pipeline '${PIPELINE_TO_EDIT.name}'`); + + // Verify documentation link + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Edit pipeline docs'); + }); + + it('should disable the name field', () => { + const { find } = testBed; + + const nameInput = find('nameField.input'); + expect(nameInput.props().disabled).toEqual(true); + }); + + describe('form submission', () => { + it('should send the correct payload with changed values', async () => { + const UPDATED_DESCRIPTION = 'updated pipeline description'; + const { actions, form, waitFor } = testBed; + + // Make change to description field + form.setInputValue('descriptionField.input', UPDATED_DESCRIPTION); + + await act(async () => { + actions.clickSubmitButton(); + await waitFor('pipelineForm', 0); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + const { name, ...pipelineDefinition } = PIPELINE_TO_EDIT; + + const expected = { + ...pipelineDefinition, + description: UPDATED_DESCRIPTION, + }; + + expect(JSON.parse(latestRequest.requestBody)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts new file mode 100644 index 00000000000000..3e0b78d4f2e9de --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts @@ -0,0 +1,186 @@ +/* + * 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 { API_BASE_PATH } from '../../common/constants'; + +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { PipelineListTestBed } from './helpers/pipelines_list.helpers'; + +const { setup } = pageHelpers.pipelinesList; + +jest.mock('ui/i18n', () => { + const I18nContext = ({ children }: any) => children; + return { I18nContext }; +}); + +describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: PipelineListTestBed; + + afterAll(() => { + server.restore(); + }); + + describe('With pipelines', () => { + const pipeline1 = { + name: 'test_pipeline1', + description: 'test_pipeline1 description', + processors: [], + }; + + const pipeline2 = { + name: 'test_pipeline2', + description: 'test_pipeline2 description', + processors: [], + }; + + const pipelines = [pipeline1, pipeline2]; + + httpRequestsMockHelpers.setLoadPipelinesResponse(pipelines); + + beforeEach(async () => { + testBed = await setup(); + + await act(async () => { + const { waitFor } = testBed; + + await waitFor('pipelinesTable'); + }); + }); + + test('should render the list view', async () => { + const { exists, find, table } = testBed; + + // Verify app title + expect(exists('appTitle')).toBe(true); + expect(find('appTitle').text()).toEqual('Ingest Node Pipelines'); + + // Verify documentation link + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Ingest Node Pipelines docs'); + + // Verify create button exists + expect(exists('createPipelineButton')).toBe(true); + + // Verify table content + const { tableCellsValues } = table.getMetaData('pipelinesTable'); + tableCellsValues.forEach((row, i) => { + const pipeline = pipelines[i]; + + expect(row).toEqual(['', pipeline.name, '']); + }); + }); + + test('should reload the pipeline data', async () => { + const { component, actions } = testBed; + const totalRequests = server.requests.length; + + await act(async () => { + actions.clickReloadButton(); + await nextTick(100); + component.update(); + }); + + expect(server.requests.length).toBe(totalRequests + 1); + expect(server.requests[server.requests.length - 1].url).toBe(API_BASE_PATH); + }); + + test('should show the details of a pipeline', async () => { + const { find, exists, actions } = testBed; + + await actions.clickPipelineAt(0); + + expect(exists('pipelinesTable')).toBe(true); + expect(exists('pipelineDetails')).toBe(true); + expect(find('pipelineDetails.title').text()).toBe(pipeline1.name); + }); + + test('should delete a pipeline', async () => { + const { actions, component } = testBed; + const { name: pipelineName } = pipeline1; + + httpRequestsMockHelpers.setDeletePipelineResponse({ + itemsDeleted: [pipelineName], + errors: [], + }); + + actions.clickPipelineAction(pipelineName, 'delete'); + + // We need to read the document "body" as the modal is added there and not inside + // the component DOM tree. + const modal = document.body.querySelector('[data-test-subj="deletePipelinesConfirmation"]'); + const confirmButton: HTMLButtonElement | null = modal!.querySelector( + '[data-test-subj="confirmModalConfirmButton"]' + ); + + expect(modal).not.toBe(null); + expect(modal!.textContent).toContain('Delete pipeline'); + + await act(async () => { + confirmButton!.click(); + await nextTick(); + component.update(); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + expect(latestRequest.method).toBe('DELETE'); + expect(latestRequest.url).toBe(`${API_BASE_PATH}/${pipelineName}`); + expect(latestRequest.status).toEqual(200); + }); + }); + + describe('No pipelines', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPipelinesResponse([]); + + testBed = await setup(); + + await act(async () => { + const { waitFor } = testBed; + + await waitFor('emptyList'); + }); + }); + + test('should display an empty prompt', async () => { + const { exists, find } = testBed; + + expect(exists('sectionLoading')).toBe(false); + expect(exists('emptyList')).toBe(true); + expect(find('emptyList.title').text()).toEqual('Start by creating a pipeline'); + }); + }); + + describe('Error handling', () => { + beforeEach(async () => { + const error = { + status: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadPipelinesResponse(undefined, { body: error }); + + testBed = await setup(); + + await act(async () => { + const { waitFor } = testBed; + + await waitFor('pipelineLoadError'); + }); + }); + + test('should render an error message if error fetching pipelines', async () => { + const { exists, find } = testBed; + + expect(exists('pipelineLoadError')).toBe(true); + expect(find('pipelineLoadError').text()).toContain('Unable to load pipelines.'); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 9082196a48b395..55523bfa7d1166 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -126,6 +126,7 @@ export const PipelineForm: React.FunctionComponent = ({ setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} > {isRequestVisible ? ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index b90683426887f0..8144228b1e9d5b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -140,7 +140,12 @@ export const PipelineFormFields: React.FunctionComponent = ({ - + = ({ path="processors" component={JsonEditorField} componentProps={{ - ['data-test-subj']: 'processorsField', euiCodeEditorProps: { + ['data-test-subj']: 'processorsField', height: '300px', 'aria-label': i18n.translate('xpack.ingestPipelines.form.processorsFieldAriaLabel', { defaultMessage: 'Processors JSON editor', @@ -211,8 +216,8 @@ export const PipelineFormFields: React.FunctionComponent = ({ path="on_failure" component={JsonEditorField} componentProps={{ - ['data-test-subj']: 'onFailureEditor', euiCodeEditorProps: { + ['data-test-subj']: 'onFailureEditor', height: '300px', 'aria-label': i18n.translate('xpack.ingestPipelines.form.onFailureFieldAriaLabel', { defaultMessage: 'Failure processors JSON editor', diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx index 7cfe887d68d527..2ab7e84b3bb2be 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx @@ -40,10 +40,10 @@ export const PipelineRequestFlyout: React.FunctionComponent = ({ uuid.current++; return ( - + -

      +

      {name ? ( + -

      +

      {pipeline.name ? ( - -

      + +

      - -

      + +

      = ({ - +

      {pipeline.name}

      @@ -116,14 +117,16 @@ export const PipelineDetailsFlyout: FunctionComponent = ({ {/* Pipeline description */} - - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { - defaultMessage: 'Description', - })} - - - {pipeline.description ?? ''} - + {pipeline.description && ( + <> + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { + defaultMessage: 'Description', + })} + + {pipeline.description} + + )} {/* Pipeline version */} {pipeline.version && ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index 318a9219b2010a..f6fe2f0cf65faa 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -18,8 +18,9 @@ export const EmptyList: FunctionComponent = () => { +

      {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', { defaultMessage: 'Start by creating a pipeline', })} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 23d105c807c8b9..948290b1691342 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -80,11 +80,15 @@ export const PipelinesList: React.FunctionComponent = ({ history.push(BASE_PATH); }; + if (data && data.length === 0) { + return ; + } + let content: React.ReactNode; if (isLoading) { content = ( - + = ({ pipelines={data} /> ); - } else { - return ; } const renderFlyout = (): React.ReactNode => { @@ -148,6 +150,7 @@ export const PipelinesList: React.FunctionComponent = ({ href={services.documentation.getIngestNodeUrl()} target="_blank" iconType="help" + data-test-subj="documentationLink" > = ({ = ({ const tableProps: EuiInMemoryTableProps = { itemId: 'name', isSelectable: true, + 'data-test-subj': 'pipelinesTable', sorting: { sort: { field: 'name', direction: 'asc' } }, selection: { onSelectionChange: setSelection, @@ -91,7 +92,11 @@ export const PipelineTable: FunctionComponent = ({ defaultMessage: 'Name', }), sortable: true, - render: (name: string) => {name}, + render: (name: string) => ( + + {name} + + ), }, { name: ( diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index 63637eaac765d8..803d34bf0042b2 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../types'; const bodySchema = schema.object({ name: schema.string(), - description: schema.string(), + description: schema.maybe(schema.string()), processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), version: schema.maybe(schema.number()), on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), diff --git a/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx b/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx index be6830c1158361..08f55850b119ec 100644 --- a/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx +++ b/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useMemo, memo, FunctionComponent } from 'react'; +import React, { useState, useMemo, useEffect, memo, FunctionComponent } from 'react'; import { debounce } from 'lodash'; /** @@ -17,7 +17,11 @@ export function debouncedComponent(component: FunctionComponent, return (props: TProps) => { const [cachedProps, setCachedProps] = useState(props); - const delayRender = useMemo(() => debounce(setCachedProps, delay), []); + const debouncePropsChange = debounce(setCachedProps, delay); + const delayRender = useMemo(() => debouncePropsChange, []); + + // cancel debounced prop change if component has been unmounted in the meantime + useEffect(() => () => debouncePropsChange.cancel(), []); delayRender(props); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index f7be82dd34ba39..81476e8fa37089 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -43,6 +43,12 @@ export function LayerPanel( } ) { const dragDropContext = useContext(DragContext); + const [popoverState, setPopoverState] = useState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; if (!datasourcePublicAPI) { @@ -74,12 +80,6 @@ export function LayerPanel( dateRange: props.framePublicAPI.dateRange, }; - const [popoverState, setPopoverState] = useState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); const isEmptyLayer = !groups.some(d => d.accessors.length > 0); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 5cd803e7cebbc9..6da9a947110818 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -61,6 +61,8 @@ export function EditorFrame(props: EditorFrameProps) { // Initialize current datasource and all active datasources useEffect(() => { + // prevents executing dispatch on unmounted component + let isUnmounted = false; if (!allLoaded) { Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => { if ( @@ -70,16 +72,21 @@ export function EditorFrame(props: EditorFrameProps) { datasource .initialize(state.datasourceStates[datasourceId].state || undefined) .then(datasourceState => { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: datasourceState, - datasourceId, - }); + if (!isUnmounted) { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: datasourceState, + datasourceId, + }); + } }) .catch(onError); } }); } + return () => { + isUnmounted = true; + }; }, [allLoaded]); const datasourceLayers: Record = {}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index 1f741ca37934fc..e246d8e27a7089 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx @@ -122,6 +122,16 @@ export function InnerWorkspacePanel({ framePublicAPI.filters, ]); + useEffect(() => { + // reset expression error if component attempts to run it again + if (expression && localState.expressionBuildError) { + setLocalState(s => ({ + ...s, + expressionBuildError: undefined, + })); + } + }, [expression]); + function onDrop() { if (suggestionForDraggedField) { trackUiEvent('drop_onto_workspace'); @@ -174,16 +184,6 @@ export function InnerWorkspacePanel({ } function renderVisualization() { - useEffect(() => { - // reset expression error if component attempts to run it again - if (expression && localState.expressionBuildError) { - setLocalState(s => ({ - ...s, - expressionBuildError: undefined, - })); - } - }, [expression]); - if (expression === null) { return renderEmptyWorkspace(); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index c396f0efee42ed..5e3b32f6961e68 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -258,7 +258,17 @@ describe('IndexPattern Data Panel', () => { it('should render a warning if there are no index patterns', () => { const wrapper = shallowWithIntl( - + {} }} + changeIndexPattern={jest.fn()} + /> ); expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 79dcdafd916b4c..b013f2b9d22a67 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -144,21 +144,49 @@ export function IndexPatternDataPanel({ indexPatternList.map(x => `${x.title}:${x.timeFieldName}`).join(','), ]} /> - + + {Object.keys(indexPatterns).length === 0 ? ( + + + +

      + +

      +
      +
      +
      + ) : ( + + )} ); } @@ -194,35 +222,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ onChangeIndexPattern: (newId: string) => void; existingFields: IndexPatternPrivateState['existingFields']; }) { - if (Object.keys(indexPatterns).length === 0) { - return ( - - - -

      - -

      -
      -
      -
      - ); - } - const [localState, setLocalState] = useState({ nameFilter: '', typeFilter: [], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx index 04e13fead6fca0..7e2af6a19b0413 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx @@ -127,7 +127,7 @@ export function BucketNestingEditor({ defaultMessage: 'Entire data set', }), }, - ...aggColumns, + ...aggColumns.map(({ value, text }) => ({ value, text })), ]} value={prevColumn} onChange={e => setColumns(nestColumn(layer.columnOrder, e.target.value, columnId))} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index c4d2a6f8780c6a..5f0fa95ad0022a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -251,22 +251,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - - if (props.isLoading) { - return ; - } else if ( - (!props.histogram || props.histogram.buckets.length === 0) && - (!props.topValues || props.topValues.buckets.length === 0) - ) { - return ( - - {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { - defaultMessage: 'No data to display.', - })} - - ); - } - let histogramDefault = !!props.histogram; const totalValuesCount = @@ -309,6 +293,21 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { let title = <>; + if (props.isLoading) { + return ; + } else if ( + (!props.histogram || props.histogram.buckets.length === 0) && + (!props.topValues || props.topValues.buckets.length === 0) + ) { + return ( + + {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { + defaultMessage: 'No data to display.', + })} + + ); + } + if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { title = ( (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 5a02b91efc7497..bb63ceceb2b1b1 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 7935d53f56845a..20b267caa9074a 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 e363cf922b356e..16c8fda3807db3 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/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index a15e2b3692d026..eba7865028645c 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -6,7 +6,35 @@ import { cloneDeep } from 'lodash'; import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; -import { SavedObjectMigrationFn } from 'src/core/server'; +import { SavedObjectMigrationMap, SavedObjectMigrationFn } from 'src/core/server'; + +interface LensDocShape { + id?: string; + type?: string; + visualizationType: string | null; + title: string; + expression: string | null; + state: { + datasourceMetaData: { + filterableIndexPatterns: Array<{ id: string; title: string }>; + }; + datasourceStates: { + // This is hardcoded as our only datasource + indexpattern: { + layers: Record< + string, + { + columnOrder: string[]; + columns: Record; + } + >; + }; + }; + visualization: VisualizationState; + query: unknown; + filters: unknown[]; + }; +} interface XYLayerPre77 { layerId: string; @@ -15,13 +43,23 @@ interface XYLayerPre77 { accessors: string[]; } +interface XYStatePre77 { + layers: XYLayerPre77[]; +} + +interface XYStatePost77 { + layers: Array>; +} + /** * Removes the `lens_auto_date` subexpression from a stored expression * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { - const expression: string = doc.attributes?.expression; +const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { + const expression = doc.attributes.expression; + if (!expression) { + return doc; + } try { const ast = fromExpression(expression); const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { @@ -74,9 +112,11 @@ const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { /** * Adds missing timeField arguments to esaggs in the Lens expression */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { - const expression: string = doc.attributes?.expression; +const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { + const expression = doc.attributes.expression; + if (!expression) { + return doc; + } try { const ast = fromExpression(expression); @@ -133,27 +173,32 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => } }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const migrations: Record> = { - '7.7.0': doc => { - const newDoc = cloneDeep(doc); - if (newDoc.attributes?.visualizationType === 'lnsXY') { - const datasourceState = newDoc.attributes.state?.datasourceStates?.indexpattern; - const datasourceLayers = datasourceState?.layers ?? {}; - const xyState = newDoc.attributes.state?.visualization; - newDoc.attributes.state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => { - const layerId = layer.layerId; - const datasource = datasourceLayers[layerId]; - return { - ...layer, - xAccessor: datasource?.columns[layer.xAccessor] ? layer.xAccessor : undefined, - splitAccessor: datasource?.columns[layer.splitAccessor] ? layer.splitAccessor : undefined, - accessors: layer.accessors.filter(accessor => !!datasource?.columns[accessor]), - }; - }) as typeof xyState.layers; - } - return newDoc; - }, +const removeInvalidAccessors: SavedObjectMigrationFn< + LensDocShape, + LensDocShape +> = doc => { + const newDoc = cloneDeep(doc); + if (newDoc.attributes.visualizationType === 'lnsXY') { + const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {}; + const xyState = newDoc.attributes.state.visualization; + (newDoc.attributes as LensDocShape< + XYStatePost77 + >).state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => { + const layerId = layer.layerId; + const datasource = datasourceLayers[layerId]; + return { + ...layer, + xAccessor: datasource?.columns[layer.xAccessor] ? layer.xAccessor : undefined, + splitAccessor: datasource?.columns[layer.splitAccessor] ? layer.splitAccessor : undefined, + accessors: layer.accessors.filter(accessor => !!datasource?.columns[accessor]), + }; + }); + } + return newDoc; +}; + +export const migrations: SavedObjectMigrationMap = { + '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). '7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context), 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 180ecbb7973392..f036605a6a1742 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 574e4afcb36f05..1e27e48aac3103 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 4e4d8d9c572e44..a6411ebce84b63 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 9f877c8168cca9..ba69bee9ccf776 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 9728139eab42ad..ca9c4e16c69396 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 00000000000000..f0d4af520bdbbe --- /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 00000000000000..8178d49690e399 --- /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 8168e5a9838f20..6cba81e47fbcc2 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 00000000000000..7e6d8bb5ad8034 --- /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 ba791a55d17eb8..c4456bf97865a9 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 353a4ecdafa0ce..7a6e2a707873cb 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 f4c1fb5c43eb0b..96f054b304962c 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 e5d189db8490be..bc0fb7c479c50f 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 00000000000000..278508305c6f0a --- /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 00000000000000..7914cc86328ede --- /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 00000000000000..1ffe2e2fc4ecc5 --- /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 00000000000000..6713083e6a49be --- /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 00000000000000..ac007a704b92d2 --- /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 b8467d141bdd89..c1745dda7afabe 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 00000000000000..69e4d2f8293c77 --- /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 00000000000000..7f7c6368a1c5e9 --- /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 0cf01db8617f0e..94299c93b29d8d 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 00000000000000..f5113bd55d44f2 --- /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 00000000000000..58c19e8f9cb4f2 --- /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 3e8198a5109b31..536931f715f3f6 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 00000000000000..70e02944a46de9 --- /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 00000000000000..3ab658014bbfaf --- /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 efcb81fc8be2ac..59d1a66a581a0e 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 00000000000000..51d5745b0364d5 --- /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 00000000000000..5c71c9820cc1e4 --- /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 9ea14a2a21ed8b..b69523b664fd70 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 00000000000000..bbe71488f59ded --- /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 00000000000000..a1ba2655dd723b --- /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 e1f88bae66e0f8..23701ff753bc0d 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 d51ed60c41b56f..8223a6a34b7716 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 00000000000000..905b73cabda979 --- /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 00000000000000..6e7fb158767b50 --- /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 55aaf587ac06ba..bf74db516e1a93 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 00000000000000..2551020e3b5a43 --- /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 00000000000000..9cb130ec0e8ada --- /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 bf2bf21d2c216e..4c7a1fdaf8d4b8 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 1a30282ddaebac..309aeaa477c667 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 00000000000000..fbffd1d3ef2459 --- /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 ea068d774c4edc..5016252bc564a0 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 00000000000000..a37207271c06eb --- /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 00000000000000..1539c5ae01ff50 --- /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 2498c36967a536..5facf981c098e6 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 a3b6a520a4ecfc..36cf9bac373eb8 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 48deb3ee86820d..50e690a3185a85 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 17e3ad2f8de083..919aab5831440e 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 abbb2701499557..721d459bd7cc62 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 fcdad66d652518..dd15d6f74a2ab8 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 94cc57b53b4e24..dbbb257f22d11f 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 271c185860b074..b62de4be9d24af 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 00fcefb2c379fc..ea338d9dd37917 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 f6859e72d71b35..4aec27031f71bc 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 c7c80638e4c374..bf1608334ef24b 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 31a421c2e31bfe..c39d6cdc00ee1b 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 96bc22ca7e6f27..bfa6b1c9380736 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 d30b3c795550f1..342984b4bc2ef3 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 f21f97dc8d15f2..fd5fa743832700 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 7f5fff4dc3147a..5cf8b9e9d6c09c 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 0555997941baa0..7ee8664b04d6b7 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 4ef4110bc0742a..95b99dc87bab61 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 00000000000000..3d9902e1d43dd0 --- /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 f064543f1ec937..71db6fa2cf62c9 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 b08e5fa688b4b0..2f04353e0989b6 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 c945818a83e8a8..34cdadd1e554f3 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 2cc58c02dbfcff..d66575e7a30dba 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 0ea6533fc122a8..f0fd023d018ae5 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 36284a70fb97df..ef610ece1acc98 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 8ec92dfa4ef775..fd2ab654b55f6b 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 62b5e7c7aec4a3..b9f1ec4d400be7 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 c997d5325296a6..94028565732889 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 fe6fc37eaf81e6..ff974b6e7352b5 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 09bf0ee69c981f..1c4fde40a777ad 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 d7541f3e09e6cd..00000000000000 --- 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 5e9fd8995c0eb6..00000000000000 --- 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 6a565437617ba8..00000000000000 --- 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 d6b7d70c1aa778..00000000000000 --- 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 c555ba322fa2bb..00000000000000 --- 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 3b6f58479a2f22..8240e2965755ef 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 3b9864be6df538..8b32f09400719a 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 e0e4495d47c341..d7c3208e556fae 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 97dbf58865a882..1eb325dcc1610b 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 438038d6c885e0..3588e1f6b2417d 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 91d1a39d3970cf..7fbed5b3b86029 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 d551f4fba61d2e..4db2838cb53549 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 a9a9fa17c41fc8..722fdd03ebc438 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 b8bad47327f223..077601204e3eeb 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 f02ded1704533f..09f66740af3721 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 4b5cad8d19260e..867025cd702134 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 161b3fefdb8f9e..76e27338bdcd43 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 953b711cef6c7c..00000000000000 --- 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 d0cdbe7243abe4..be362c2ae0422e 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 984ace4fd87082..782e5e878164ed 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 00000000000000..7c5dc3d8a4c007 --- /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 1a83f4a98bb6f0..10d3f6af633706 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 00000000000000..211276cda904a6 --- /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 3d346fe1acdd52..454ba6ededcbd5 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 431d7a3b339b7c..2f07c1c5d086de 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 c14886bc37bfb6..7ed508199e64a0 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 dccf413b489f1b..8ecaf4d903251f 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/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts index a59122d7d6309c..e2833d5abd0c22 100644 --- a/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { AggDescriptor, ColorDynamicOptions, - LabelDynamicOptions, LayerDescriptor, SizeDynamicOptions, StylePropertyField, @@ -80,10 +79,6 @@ function createLayerLabel( metricName = i18n.translate('xpack.maps.observability.durationMetricName', { defaultMessage: 'Duration', }); - } else if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) { - metricName = i18n.translate('xpack.maps.observability.slaPercentageMetricName', { - defaultMessage: '% Duration of SLA', - }); } else if (metric === OBSERVABILITY_METRIC_TYPE.COUNT) { metricName = i18n.translate('xpack.maps.observability.countMetricName', { defaultMessage: 'Total', @@ -103,11 +98,6 @@ function createAggDescriptor(metric: OBSERVABILITY_METRIC_TYPE): AggDescriptor { type: AGG_TYPE.AVG, field: 'transaction.duration.us', }; - } else if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) { - return { - type: AGG_TYPE.AVG, - field: 'duration_sla_pct', - }; } else if (metric === OBSERVABILITY_METRIC_TYPE.UNIQUE_COUNT) { return { type: AGG_TYPE.UNIQUE_COUNT, @@ -251,16 +241,6 @@ export function createLayerDescriptor({ }, }; - if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) { - styleProperties[VECTOR_STYLES.LABEL_TEXT] = { - type: STYLE_TYPE.DYNAMIC, - options: { - ...(defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT]!.options as LabelDynamicOptions), - field: metricStyleField, - }, - }; - } - return VectorLayer.createDescriptor({ label, query: apmSourceQuery, diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx b/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx index 8750034f746966..4a40b257cb5176 100644 --- a/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx +++ b/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx @@ -11,7 +11,6 @@ import { OBSERVABILITY_LAYER_TYPE } from './layer_select'; export enum OBSERVABILITY_METRIC_TYPE { TRANSACTION_DURATION = 'TRANSACTION_DURATION', - SLA_PERCENTAGE = 'SLA_PERCENTAGE', COUNT = 'COUNT', UNIQUE_COUNT = 'UNIQUE_COUNT', } @@ -23,12 +22,6 @@ const APM_RUM_PERFORMANCE_METRIC_OPTIONS = [ defaultMessage: 'Transaction duraction', }), }, - { - value: OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE, - text: i18n.translate('xpack.maps.observability.slaPercentageLabel', { - defaultMessage: 'SLA percentage', - }), - }, ]; const APM_RUM_TRAFFIC_METRIC_OPTIONS = [ 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 47a4879acb58cd..b66918f93f521c 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 a6e2e7f42657c3..cc7e04a7313ac9 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 37c843d4a90609..00000000000000 --- 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 93c9af98eb17f1..03e3b2a8f49414 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 5802a223e48464..5115da510cc5b7 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 e9bf592c6d2b7f..c1e6e0d76af1f6 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 b7687fec43272f..806213b667ba43 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 fc745edbabee87..391ab5691938db 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 3bed9b2c09570a..b20a3c80e0510b 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 bc50890a0f4a30..748016cf889e25 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 58e6e39aaa1f96..5f6061b38678c0 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 af934d7464f618..f53cf689fbfe54 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 804915dd73052a..2dd6bcd8581379 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 c3245e8e98db2e..77183e334cb116 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 d83f2adb35ef7f..c6cc9b53b93019 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 21bff95731580f..8fe16c0d99d76f 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/common/types/ml_server_info.ts b/x-pack/plugins/ml/common/types/ml_server_info.ts index 26dd1758827b4a..66142f53add3a8 100644 --- a/x-pack/plugins/ml/common/types/ml_server_info.ts +++ b/x-pack/plugins/ml/common/types/ml_server_info.ts @@ -18,6 +18,7 @@ export interface MlServerDefaults { export interface MlServerLimits { max_model_memory_limit?: string; + effective_max_model_memory_limit?: string; } export interface MlInfoResponse { diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js index a3c60a87636f96..1853c3d629c3e3 100644 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js +++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js @@ -9,8 +9,6 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { getMLJobTypeAriaLabel } from '../../util/field_types_utils'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/components/message_call_out/message_call_out.js b/x-pack/plugins/ml/public/application/components/message_call_out/message_call_out.js index 9a122a0eea7005..9a1260ecfdd45d 100644 --- a/x-pack/plugins/ml/public/application/components/message_call_out/message_call_out.js +++ b/x-pack/plugins/ml/public/application/components/message_call_out/message_call_out.js @@ -14,8 +14,6 @@ import PropTypes from 'prop-types'; import { EuiCallOut } from '@elastic/eui'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { MESSAGE_LEVEL } from '../../../../common/constants/message_levels'; function getCallOutAttributes(message, status) { diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index 98e027ec4f3656..6001d7cbf6f617 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -30,8 +30,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { getDocLinks } from '../../util/dependency_cache'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { VALIDATION_STATUS } from '../../../../common/constants/validation'; import { getMostSevereMessageStatus } from '../../../../common/util/validation_utils'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index fb3b2b35199477..7501fe3d82fc6e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -91,7 +91,7 @@ export interface FieldSelectionItem { } export interface DfAnalyticsExplainResponse { - field_selection: FieldSelectionItem[]; + field_selection?: FieldSelectionItem[]; memory_estimation: { expected_memory_without_disk: string; expected_memory_with_disk: string; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index 6f9dc694d81724..e664a1ddbdbcc7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -51,6 +51,10 @@ export const useExplorationResults = ( d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== ML__ID_COPY ); + useEffect(() => { + dataGrid.resetPagination(); + }, [JSON.stringify(searchQuery)]); + useEffect(() => { getIndexData(jobConfig, dataGrid, searchQuery); // custom comparison diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 0d06bc0d433079..75b2f6aa867dff 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -58,6 +58,10 @@ export const useOutlierData = ( d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== ML__ID_COPY ); + useEffect(() => { + dataGrid.resetPagination(); + }, [JSON.stringify(searchQuery)]); + // initialize sorting: reverse sort on outlier score column useEffect(() => { if (jobConfig !== undefined) { 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 2463da054d1406..9221f8c5003268 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 cc75ddbe08cfb9..cfb11856670c4b 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/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx index 92de5ad7be21e3..85cd70912b41f2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx @@ -53,7 +53,7 @@ describe('Data Frame Analytics: ', () => { ); const euiFormRows = wrapper.find('EuiFormRow'); - expect(euiFormRows.length).toBe(9); + expect(euiFormRows.length).toBe(10); const row1 = euiFormRows.at(0); expect(row1.find('label').text()).toBe('Job type'); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 199100d8b5ab00..11052b171845d0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -48,6 +48,13 @@ import { } from '../../../../common/analytics'; import { shouldAddAsDepVarOption, OMIT_FIELDS } from './form_options_validation'; +const requiredFieldsErrorText = i18n.translate( + 'xpack.ml.dataframe.analytics.create.requiredFieldsErrorMessage', + { + defaultMessage: 'At least one field must be included in the analysis.', + } +); + export const CreateAnalyticsForm: FC = ({ actions, state }) => { const { services: { docLinks }, @@ -96,6 +103,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta numTopFeatureImportanceValuesValid, previousJobType, previousSourceIndex, + requiredFieldsError, sourceIndex, sourceIndexNameEmpty, sourceIndexNameValid, @@ -158,6 +166,8 @@ export const CreateAnalyticsForm: FC = ({ actions, sta }; const debouncedGetExplainData = debounce(async () => { + const jobTypeOrIndexChanged = + previousSourceIndex !== sourceIndex || previousJobType !== jobType; const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit; const shouldUpdateEstimatedMml = !firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === ''; @@ -167,7 +177,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta } // Reset if sourceIndex or jobType changes (jobType requires dependent_variable to be set - // which won't be the case if switching from outlier detection) - if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) { + if (jobTypeOrIndexChanged) { setFormState({ loadingFieldOptions: true, }); @@ -186,8 +196,21 @@ export const CreateAnalyticsForm: FC = ({ actions, sta setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); } + const fieldSelection: FieldSelectionItem[] | undefined = resp.field_selection; + + let hasRequiredFields = false; + if (fieldSelection) { + for (let i = 0; i < fieldSelection.length; i++) { + const field = fieldSelection[i]; + if (field.is_included === true && field.is_required === false) { + hasRequiredFields = true; + break; + } + } + } + // If sourceIndex has changed load analysis field options again - if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) { + if (jobTypeOrIndexChanged) { const analyzedFieldsOptions: EuiComboBoxOptionOption[] = []; if (resp.field_selection) { @@ -204,21 +227,24 @@ export const CreateAnalyticsForm: FC = ({ actions, sta loadingFieldOptions: false, fieldOptionsFetchFail: false, maxDistinctValuesError: undefined, + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, }); } else { setFormState({ ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, }); } } catch (e) { let errorMessage; if ( jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && - e.message !== undefined && - e.message.includes('status_exception') && - e.message.includes('must have at most') + e.body && + e.body.message !== undefined && + e.body.message.includes('status_exception') && + e.body.message.includes('must have at most') ) { - errorMessage = e.message; + errorMessage = e.body.message; } const fallbackModelMemoryLimit = jobType !== undefined @@ -321,6 +347,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta excludesOptions: [], previousSourceIndex: sourceIndex, sourceIndex: selectedOptions[0].label || '', + requiredFieldsError: undefined, }); }; @@ -368,6 +395,9 @@ export const CreateAnalyticsForm: FC = ({ actions, sta forceInput.current.dispatchEvent(evt); }, []); + const noSupportetdAnalysisFields = + excludesOptions.length === 0 && fieldOptionsFetchFail === false && !sourceIndexNameEmpty; + return ( @@ -715,18 +745,31 @@ export const CreateAnalyticsForm: FC = ({ actions, sta )} + + + = ({ type, setFormState }) => { previousJobType: type, jobType: value, excludes: [], + requiredFieldsError: undefined, }); }} data-test-subj="mlAnalyticsCreateJobFlyoutJobTypeSelect" diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index d55eb14a20e290..1cab42d8ee12d1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -124,6 +124,7 @@ export const validateAdvancedEditor = (state: State): State => { createIndexPattern, excludes, maxDistinctValuesError, + requiredFieldsError, } = state.form; const { jobConfig } = state; @@ -330,6 +331,7 @@ export const validateAdvancedEditor = (state: State): State => { state.isValid = maxDistinctValuesError === undefined && + requiredFieldsError === undefined && excludesValid && trainingPercentValid && state.form.modelMemoryLimitUnitValid && @@ -397,6 +399,7 @@ const validateForm = (state: State): State => { maxDistinctValuesError, modelMemoryLimit, numTopFeatureImportanceValuesValid, + requiredFieldsError, } = state.form; const { estimatedModelMemoryLimit } = state; @@ -412,6 +415,7 @@ const validateForm = (state: State): State => { state.isValid = maxDistinctValuesError === undefined && + requiredFieldsError === undefined && !jobTypeEmpty && !mmlValidationResult && !jobIdEmpty && diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 70840a442f6f6e..8ca985a537b6e7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -76,6 +76,7 @@ export interface State { numTopFeatureImportanceValuesValid: boolean; previousJobType: null | AnalyticsJobType; previousSourceIndex: EsIndexName | undefined; + requiredFieldsError: string | undefined; sourceIndex: EsIndexName; sourceIndexNameEmpty: boolean; sourceIndexNameValid: boolean; @@ -133,6 +134,7 @@ export const getInitialState = (): State => ({ numTopFeatureImportanceValuesValid: true, previousJobType: null, previousSourceIndex: undefined, + requiredFieldsError: undefined, sourceIndex: '', sourceIndexNameEmpty: true, sourceIndexNameValid: false, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 7d966949624c1f..3b82a34b889b71 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -5,6 +5,7 @@ */ import { isEqual } from 'lodash'; +// @ts-ignore import numeral from '@elastic/numeral'; import { ml } from '../../../../services/ml_api_service'; import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer'; diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index cfcba081983c2c..a46f35cbd4d205 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 86d16776b68e2a..8fd24798178073 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/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 03426869b0ccfc..2b577c978eb139 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -17,8 +17,6 @@ import d3 from 'd3'; import $ from 'jquery'; import moment from 'moment'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { formatHumanReadableDateTime } from '../../util/date_utils'; import { formatValue } from '../../formatters/format_value'; import { getSeverityColor, getSeverityWithLow } from '../../../../common/util/anomaly_utils'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 82041af39ca15e..531a24493c9610 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -17,8 +17,6 @@ import d3 from 'd3'; import $ from 'jquery'; import moment from 'moment'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { formatHumanReadableDateTime } from '../../util/date_utils'; import { formatValue } from '../../formatters/format_value'; import { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx index bf1a3b424edb91..8a8a826e1831f3 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx @@ -14,8 +14,6 @@ import _ from 'lodash'; import d3 from 'd3'; import moment from 'moment'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { i18n } from '@kbn/i18n'; import { Subscription } from 'rxjs'; import { TooltipValue } from '@elastic/charts'; 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 657f1c6c7af2ed..cf65419e4bd801 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 383d07eb7a9f60..03e3273b808327 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/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js index 29e89022a55025..2a65ee06f2c2c1 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js @@ -157,6 +157,7 @@ class CreateWatchService { id, type: 'json', isNew: false, // Set to false, as we want to allow watches to be overwritten. + isActive: true, watch, }, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index f7b0e726ecc53d..fa36a0626d632e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -165,6 +165,15 @@ export function extractJobDetails(job) { items: filterObjects(job.model_size_stats).map(formatValues), }; + const jobTimingStats = { + id: 'jobTimingStats', + title: i18n.translate('xpack.ml.jobsList.jobDetails.jobTimingStatsTitle', { + defaultMessage: 'Job timing stats', + }), + position: 'left', + items: filterObjects(job.timing_stats).map(formatValues), + }; + const datafeedTimingStats = { id: 'datafeedTimingStats', title: i18n.translate('xpack.ml.jobsList.jobDetails.datafeedTimingStatsTitle', { @@ -192,6 +201,7 @@ export function extractJobDetails(job) { datafeed, counts, modelSizeStats, + jobTimingStats, datafeedTimingStats, }; } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index 9984f3be299ae8..246a476517acea 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -63,6 +63,12 @@ export function formatValues([key, value]) { // numbers rounded to 3 decimal places case 'average_search_time_per_bucket_ms': case 'exponential_average_search_time_per_hour_ms': + case 'total_bucket_processing_time_ms': + case 'minimum_bucket_processing_time_ms': + case 'maximum_bucket_processing_time_ms': + case 'average_bucket_processing_time_ms': + case 'exponential_average_bucket_processing_time_ms': + case 'exponential_average_bucket_processing_time_per_hour_ms': value = typeof value === 'number' ? roundToDecimalPlace(value, 3).toLocaleString() : value; break; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index e3f348ad32b0c1..0375997b86bb89 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -60,6 +60,7 @@ export class JobDetails extends Component { datafeed, counts, modelSizeStats, + jobTimingStats, datafeedTimingStats, } = extractJobDetails(job); @@ -102,7 +103,7 @@ export class JobDetails extends Component { content: ( ), }, diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index bbfec49ac1388f..fb75476c48fa3d 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -369,6 +369,8 @@ class JobService { delete tempJob.open_time; delete tempJob.established_model_memory; delete tempJob.calendars; + delete tempJob.timing_stats; + delete tempJob.forecasts_stats; delete tempJob.analysis_config.use_per_partition_normalization; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index e5026778fec1c7..df2e119f511e17 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -88,6 +88,7 @@ exports[`CalendarForm Renders calendar form 1`] = ` size="xl" /> {isGlobalCalendar === false && ( diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 64f20667931184..eded8460d2205f 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -15,8 +15,6 @@ import React, { Component } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../../common/constants/states'; import { MESSAGE_LEVEL } from '../../../../../common/constants/message_levels'; import { isJobVersionGte } from '../../../../../common/util/job_utils'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index 7dd06268f7f8dd..3208697073b8e4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -23,8 +23,6 @@ import { EuiToolTip, } from '@elastic/eui'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { JOB_STATE } from '../../../../../common/constants/states'; import { FORECAST_DURATION_MAX_DAYS } from './forecasting_modal'; import { ForecastProgress } from './forecast_progress'; diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index c23d042822816d..a9ffb1a5bf5792 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -13,7 +13,6 @@ import { MlSetupDependencies, MlStartDependencies, } from './plugin'; -import { getMetricChangeDescription } from './application/formatters/metric_change_description'; export const plugin: PluginInitializer< MlPluginSetup, @@ -22,4 +21,5 @@ export const plugin: PluginInitializer< MlStartDependencies > = () => new MlPlugin(); -export { MlPluginSetup, MlPluginStart, getMetricChangeDescription }; +export { MlPluginSetup, MlPluginStart }; +export * from './shared'; diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts new file mode 100644 index 00000000000000..6821cb7ef0f945 --- /dev/null +++ b/x-pack/plugins/ml/public/shared.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from '../common/constants/anomalies'; + +export * from '../common/types/data_recognizer'; +export * from '../common/types/capabilities'; +export * from '../common/types/anomalies'; +export * from '../common/types/modules'; +export * from '../common/types/audit_message'; + +export * from '../common/util/anomaly_utils'; +export * from '../common/util/errors'; +export * from '../common/util/validators'; + +export * from './application/formatters/metric_change_description'; + +export * from './application/components/data_grid'; +export * from './application/data_frame_analytics/common'; diff --git a/x-pack/plugins/ml/server/index.ts b/x-pack/plugins/ml/server/index.ts index 175c20bf49c947..4c27854ec719bd 100644 --- a/x-pack/plugins/ml/server/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -7,5 +7,6 @@ import { PluginInitializerContext } from 'kibana/server'; import { MlServerPlugin } from './plugin'; export { MlPluginSetup, MlPluginStart } from './plugin'; +export * from './shared'; export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts similarity index 56% rename from x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts index a0dacc38e58352..f5daadfe86be0c 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { estimateBucketSpanFactory } from '../bucket_span_estimator'; +import { APICaller } from 'kibana/server'; + +import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; + +import { estimateBucketSpanFactory, BucketSpanEstimatorData } from './bucket_span_estimator'; // Mock callWithRequest with the ability to simulate returning different // permission settings. On each call using `ml.privilegeCheck` we retrieve @@ -14,7 +17,7 @@ import { estimateBucketSpanFactory } from '../bucket_span_estimator'; // sufficient permissions should be returned, the second time insufficient // permissions. const permissions = [false, true]; -const callWithRequest = method => { +const callWithRequest: APICaller = (method: string) => { return new Promise(resolve => { if (method === 'ml.privilegeCheck') { resolve({ @@ -28,34 +31,19 @@ const callWithRequest = method => { return; } resolve({}); - }); + }) as Promise; }; -const callWithInternalUser = () => { +const callWithInternalUser: APICaller = () => { return new Promise(resolve => { resolve({}); - }); + }) as Promise; }; -// mock xpack_main plugin -function mockXpackMainPluginFactory(isEnabled = false, licenseType = 'platinum') { - return { - info: { - isAvailable: () => true, - feature: () => ({ - isEnabled: () => isEnabled, - }), - license: { - getType: () => licenseType, - }, - }, - }; -} - // mock configuration to be passed to the estimator -const formConfig = { - aggTypes: ['count'], - duration: {}, +const formConfig: BucketSpanEstimatorData = { + aggTypes: [ES_AGGREGATION.COUNT], + duration: { start: 0, end: 1 }, fields: [null], index: '', query: { @@ -64,13 +52,15 @@ const formConfig = { must_not: [], }, }, + splitField: undefined, + timeField: undefined, }; describe('ML - BucketSpanEstimator', () => { it('call factory', () => { expect(function() { - estimateBucketSpanFactory(callWithRequest, callWithInternalUser); - }).to.not.throwError('Not initialized.'); + estimateBucketSpanFactory(callWithRequest, callWithInternalUser, false); + }).not.toThrow('Not initialized.'); }); it('call factory and estimator with security disabled', done => { @@ -78,44 +68,29 @@ describe('ML - BucketSpanEstimator', () => { const estimateBucketSpan = estimateBucketSpanFactory( callWithRequest, callWithInternalUser, - mockXpackMainPluginFactory() + true ); estimateBucketSpan(formConfig).catch(catchData => { - expect(catchData).to.be('Unable to retrieve cluster setting search.max_buckets'); + expect(catchData).toBe('Unable to retrieve cluster setting search.max_buckets'); done(); }); - }).to.not.throwError('Not initialized.'); + }).not.toThrow('Not initialized.'); }); - it('call factory and estimator with security enabled and sufficient permissions.', done => { + it('call factory and estimator with security enabled.', done => { expect(function() { const estimateBucketSpan = estimateBucketSpanFactory( callWithRequest, callWithInternalUser, - mockXpackMainPluginFactory(true) + false ); estimateBucketSpan(formConfig).catch(catchData => { - expect(catchData).to.be('Unable to retrieve cluster setting search.max_buckets'); + expect(catchData).toBe('Unable to retrieve cluster setting search.max_buckets'); done(); }); - }).to.not.throwError('Not initialized.'); - }); - - it('call factory and estimator with security enabled and insufficient permissions.', done => { - expect(function() { - const estimateBucketSpan = estimateBucketSpanFactory( - callWithRequest, - callWithInternalUser, - mockXpackMainPluginFactory(true) - ); - - estimateBucketSpan(formConfig).catch(catchData => { - expect(catchData).to.be('Insufficient permissions to call bucket span estimation.'); - done(); - }); - }).to.not.throwError('Not initialized.'); + }).not.toThrow('Not initialized.'); }); }); diff --git a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts index cd61dd9eddcdd7..1cc2a07ddbc881 100644 --- a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts @@ -9,6 +9,7 @@ import { APICaller } from 'kibana/server'; import { MLCATEGORY } from '../../../common/constants/field_types'; import { AnalysisConfig } from '../../../common/types/anomaly_detection_jobs'; import { fieldsServiceProvider } from '../fields_service'; +import { MlInfoResponse } from '../../../common/types/ml_server_info'; interface ModelMemoryEstimationResult { /** @@ -139,15 +140,9 @@ export function calculateModelMemoryLimitProvider(callAsCurrentUser: APICaller) latestMs: number, allowMMLGreaterThanMax = false ): Promise { - let maxModelMemoryLimit; - try { - const resp = await callAsCurrentUser('ml.info'); - if (resp?.limits?.max_model_memory_limit !== undefined) { - maxModelMemoryLimit = resp.limits.max_model_memory_limit.toUpperCase(); - } - } catch (e) { - throw new Error('Unable to retrieve max model memory limit'); - } + const info = await callAsCurrentUser('ml.info'); + const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase(); + const effectiveMaxModelMemoryLimit = info.limits.effective_max_model_memory_limit?.toUpperCase(); const { overallCardinality, maxBucketCardinality } = await getCardinalities( analysisConfig, @@ -168,17 +163,32 @@ export function calculateModelMemoryLimitProvider(callAsCurrentUser: APICaller) }) ).model_memory_estimate.toUpperCase(); - let modelMemoryLimit: string = estimatedModelMemoryLimit; + let modelMemoryLimit = estimatedModelMemoryLimit; + let mmlCappedAtMax = false; // if max_model_memory_limit has been set, // make sure the estimated value is not greater than it. - if (!allowMMLGreaterThanMax && maxModelMemoryLimit !== undefined) { - // @ts-ignore - const maxBytes = numeral(maxModelMemoryLimit).value(); + if (allowMMLGreaterThanMax === false) { // @ts-ignore const mmlBytes = numeral(estimatedModelMemoryLimit).value(); - if (mmlBytes > maxBytes) { + if (maxModelMemoryLimit !== undefined) { + // @ts-ignore + const maxBytes = numeral(maxModelMemoryLimit).value(); + if (mmlBytes > maxBytes) { + // @ts-ignore + modelMemoryLimit = `${Math.floor(maxBytes / numeral('1MB').value())}MB`; + mmlCappedAtMax = true; + } + } + + // if we've not already capped the estimated mml at the hard max server setting + // ensure that the estimated mml isn't greater than the effective max mml + if (mmlCappedAtMax === false && effectiveMaxModelMemoryLimit !== undefined) { // @ts-ignore - modelMemoryLimit = `${Math.floor(maxBytes / numeral('1MB').value())}MB`; + const effectiveMaxMmlBytes = numeral(effectiveMaxModelMemoryLimit).value(); + if (mmlBytes > effectiveMaxMmlBytes) { + // @ts-ignore + modelMemoryLimit = `${Math.floor(effectiveMaxMmlBytes / numeral('1MB').value())}MB`; + } } } @@ -186,6 +196,7 @@ export function calculateModelMemoryLimitProvider(callAsCurrentUser: APICaller) estimatedModelMemoryLimit, modelMemoryLimit, ...(maxModelMemoryLimit ? { maxModelMemoryLimit } : {}), + ...(effectiveMaxModelMemoryLimit ? { effectiveMaxModelMemoryLimit } : {}), }; }; } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json index d8c970e1794164..c792b981df30a5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json @@ -30,7 +30,7 @@ { "url_name": "Process rate", "time_range": "1h", - "url_value": "kibana#/dashboard/ml_auditbeat_docker_process_event_rate_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:container.runtime,negate:!f,params:(query:docker),type:phrase,value:docker),query:(match:(container.runtime:(query:docker,type:phrase)))),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))" + "url_value": "kibana#/dashboard/ml_auditbeat_docker_process_event_rate_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:container.runtime,negate:!f,params:(query:docker),type:phrase,value:docker),query:(match:(container.runtime:(query:docker,type:phrase)))),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))" }, { "url_name": "Raw data", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json index 76e3c8026c6310..b3f02ae5a6bf88 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json @@ -30,7 +30,7 @@ { "url_name": "Process explorer", "time_range": "1h", - "url_value": "kibana#/dashboard/ml_auditbeat_docker_process_explorer_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:container.runtime,negate:!f,params:(query:docker),type:phrase,value:docker),query:(match:(container.runtime:(query:docker,type:phrase)))),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))" + "url_value": "kibana#/dashboard/ml_auditbeat_docker_process_explorer_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:container.runtime,negate:!f,params:(query:docker),type:phrase,value:docker),query:(match:(container.runtime:(query:docker,type:phrase)))),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))" }, { "url_name": "Raw data", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json index 487bee53118785..0e9336507b465b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json @@ -29,7 +29,7 @@ { "url_name": "Process rate", "time_range": "1h", - "url_value": "kibana#/dashboard/ml_auditbeat_hosts_process_event_rate_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),exists:(field:container.runtime),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:container.runtime,negate:!t,type:exists,value:exists)),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))" + "url_value": "kibana#/dashboard/ml_auditbeat_hosts_process_event_rate_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),exists:(field:container.runtime),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:container.runtime,negate:!t,type:exists,value:exists)),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))" }, { "url_name": "Raw data", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json index 9ba6859bfa1669..4dd1409b71c793 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json @@ -30,7 +30,7 @@ { "url_name": "Process explorer", "time_range": "1h", - "url_value": "kibana#/dashboard/ml_auditbeat_hosts_process_explorer_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),exists:(field:container.runtime),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:container.runtime,negate:!t,type:exists,value:exists)),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))" + "url_value": "kibana#/dashboard/ml_auditbeat_hosts_process_explorer_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),exists:(field:container.runtime),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:container.runtime,negate:!t,type:exists,value:exists)),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))" }, { "url_name": "Raw data", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json index e0230e2a063734..c3d401085f7ae3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json @@ -27,7 +27,7 @@ "custom_urls": [ { "url_name": "Raw data", - "url_value": "kibana#/discover?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:ff959d40-b880-11e8-a6d9-e546fe2bba5f,query:(language:kuery,query:\u0027customer_full_name.keyword:\u0022$customer_full_name.keyword$\u0022\u0027),sort:!('@timestamp',desc))" + "url_value": "kibana#/discover?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027ff959d40-b880-11e8-a6d9-e546fe2bba5f\u0027,query:(language:kuery,query:\u0027customer_full_name.keyword:\u0022$customer_full_name.keyword$\u0022\u0027),sort:!('@timestamp',desc))" }, { "url_name": "Data dashboard", diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 645625f92df295..8ccd359137b670 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -342,8 +342,8 @@ export class DataVisualizer { aggregatableFields: string[], samplerShardSize: number, timeFieldName: string, - earliestMs: number, - latestMs: number + earliestMs?: number, + latestMs?: number ) { const index = indexPatternTitle; const size = 0; diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 6024ecf4925e61..225cd43e411a4f 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -328,7 +328,7 @@ export function jobsProvider(callAsCurrentUser: APICaller) { // create jobs objects containing job stats, datafeeds, datafeed stats and calendars if (jobResults && jobResults.jobs) { jobResults.jobs.forEach(job => { - const tempJob = job as CombinedJobWithStats; + let tempJob = job as CombinedJobWithStats; const calendars: string[] = [ ...(calendarsByJobId[tempJob.job_id] || []), @@ -341,9 +341,7 @@ export function jobsProvider(callAsCurrentUser: APICaller) { if (jobStatsResults && jobStatsResults.jobs) { const jobStats = jobStatsResults.jobs.find(js => js.job_id === tempJob.job_id); if (jobStats !== undefined) { - tempJob.state = jobStats.state; - tempJob.data_counts = jobStats.data_counts; - tempJob.model_size_stats = jobStats.model_size_stats; + tempJob = { ...tempJob, ...jobStats }; if (jobStats.node) { tempJob.node = jobStats.node; } diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_cardinality.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_cardinality.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_search_response.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_search_response.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_field_caps.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_field_caps.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_it_search_response.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_it_search_response.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field_nested.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field_nested.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_range.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_range.json diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts index 33f5d5ec95fad5..6a9a7a0c13395c 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts @@ -6,14 +6,19 @@ import { APICaller } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; + +import { DeepPartial } from '../../../common/types/common'; + import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; -type ValidateJobPayload = TypeOf; +import { ValidationMessage } from './messages'; + +export type ValidateJobPayload = TypeOf; export function validateJob( callAsCurrentUser: APICaller, - payload: ValidateJobPayload, - kbnVersion: string, - callAsInternalUser: APICaller, - isSecurityDisabled: boolean -): string[]; + payload?: DeepPartial, + kbnVersion?: string, + callAsInternalUser?: APICaller, + isSecurityDisabled?: boolean +): Promise; diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts similarity index 78% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js rename to x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index 726a8e8d8db853..d907677855c125 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -4,16 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { validateJob } from '../job_validation'; +import { APICaller } from 'kibana/server'; + +import { validateJob } from './job_validation'; // mock callWithRequest -const callWithRequest = () => { +const callWithRequest: APICaller = (method: string) => { return new Promise(resolve => { + if (method === 'fieldCaps') { + resolve({ fields: [] }); + return; + } else if (method === 'ml.info') { + resolve({ + limits: { + effective_max_model_memory_limit: '100MB', + max_model_memory_limit: '1GB', + }, + }); + } resolve({}); - }); + }) as Promise; }; +// Note: The tests cast `payload` as any +// so we can simulate possible runtime payloads +// that don't satisfy the TypeScript specs. describe('ML - validateJob', () => { it('calling factory without payload throws an error', done => { validateJob(callWithRequest).then( @@ -61,7 +76,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_empty', 'detectors_empty', 'bucket_span_empty', @@ -70,10 +85,14 @@ describe('ML - validateJob', () => { }); }); - const jobIdTests = (testIds, messageId) => { + const jobIdTests = (testIds: string[], messageId: string) => { const promises = testIds.map(id => { - const payload = { job: { analysis_config: { detectors: [] } } }; - payload.job.job_id = id; + const payload = { + job: { + analysis_config: { detectors: [] }, + job_id: id, + }, + }; return validateJob(callWithRequest, payload).catch(() => { new Error('Promise should not fail for jobIdTests.'); }); @@ -81,19 +100,21 @@ describe('ML - validateJob', () => { return Promise.all(promises).then(testResults => { testResults.forEach(messages => { - const ids = messages.map(m => m.id); - expect(ids.includes(messageId)).to.equal(true); + expect(Array.isArray(messages)).toBe(true); + if (Array.isArray(messages)) { + const ids = messages.map(m => m.id); + expect(ids.includes(messageId)).toBe(true); + } }); }); }; - const jobGroupIdTest = (testIds, messageId) => { - const payload = { job: { analysis_config: { detectors: [] } } }; - payload.job.groups = testIds; + const jobGroupIdTest = (testIds: string[], messageId: string) => { + const payload = { job: { analysis_config: { detectors: [] }, groups: testIds } }; return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes(messageId)).to.equal(true); + expect(ids.includes(messageId)).toBe(true); }); }; @@ -126,10 +147,9 @@ describe('ML - validateJob', () => { return jobGroupIdTest(validTestIds, 'job_group_id_valid'); }); - const bucketSpanFormatTests = (testFormats, messageId) => { + const bucketSpanFormatTests = (testFormats: string[], messageId: string) => { const promises = testFormats.map(format => { - const payload = { job: { analysis_config: { detectors: [] } } }; - payload.job.analysis_config.bucket_span = format; + const payload = { job: { analysis_config: { bucket_span: format, detectors: [] } } }; return validateJob(callWithRequest, payload).catch(() => { new Error('Promise should not fail for bucketSpanFormatTests.'); }); @@ -137,8 +157,11 @@ describe('ML - validateJob', () => { return Promise.all(promises).then(testResults => { testResults.forEach(messages => { - const ids = messages.map(m => m.id); - expect(ids.includes(messageId)).to.equal(true); + expect(Array.isArray(messages)).toBe(true); + if (Array.isArray(messages)) { + const ids = messages.map(m => m.id); + expect(ids.includes(messageId)).toBe(true); + } }); }); }; @@ -152,7 +175,7 @@ describe('ML - validateJob', () => { }); it('at least one detector function is empty', () => { - const payload = { job: { analysis_config: { detectors: [] } } }; + const payload = { job: { analysis_config: { detectors: [] as Array<{ function?: string }> } } }; payload.job.analysis_config.detectors.push({ function: 'count', }); @@ -165,19 +188,19 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes('detectors_function_empty')).to.equal(true); + expect(ids.includes('detectors_function_empty')).toBe(true); }); }); it('detector function is not empty', () => { - const payload = { job: { analysis_config: { detectors: [] } } }; + const payload = { job: { analysis_config: { detectors: [] as Array<{ function?: string }> } } }; payload.job.analysis_config.detectors.push({ function: 'count', }); return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes('detectors_function_not_empty')).to.equal(true); + expect(ids.includes('detectors_function_not_empty')).toBe(true); }); }); @@ -189,7 +212,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes('index_fields_invalid')).to.equal(true); + expect(ids.includes('index_fields_invalid')).toBe(true); }); }); @@ -201,11 +224,11 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes('index_fields_valid')).to.equal(true); + expect(ids.includes('index_fields_valid')).toBe(true); }); }); - const getBasicPayload = () => ({ + const getBasicPayload = (): any => ({ job: { job_id: 'test', analysis_config: { @@ -214,7 +237,7 @@ describe('ML - validateJob', () => { { function: 'count', }, - ], + ] as Array<{ function: string; by_field_name?: string; partition_field_name?: string }>, influencers: [], }, data_description: { time_field: '@timestamp' }, @@ -224,7 +247,7 @@ describe('ML - validateJob', () => { }); it('throws an error because job.analysis_config.influencers is not an Array', done => { - const payload = getBasicPayload(); + const payload = getBasicPayload() as any; delete payload.job.analysis_config.influencers; validateJob(callWithRequest, payload).then( @@ -237,11 +260,11 @@ describe('ML - validateJob', () => { }); it('detect duplicate detectors', () => { - const payload = getBasicPayload(); + const payload = getBasicPayload() as any; payload.job.analysis_config.detectors.push({ function: 'count' }); return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'detectors_duplicates', @@ -253,7 +276,7 @@ describe('ML - validateJob', () => { }); it('dedupe duplicate messages', () => { - const payload = getBasicPayload(); + const payload = getBasicPayload() as any; // in this test setup, the following configuration passes // the duplicate detectors check, but would return the same // 'field_not_aggregatable' message for both detectors. @@ -264,7 +287,7 @@ describe('ML - validateJob', () => { ]; return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -274,11 +297,12 @@ describe('ML - validateJob', () => { }); }); + // Failing https://github.com/elastic/kibana/issues/65865 it('basic validation passes, extended checks return some messages', () => { const payload = getBasicPayload(); return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -287,8 +311,9 @@ describe('ML - validateJob', () => { }); }); + // Failing https://github.com/elastic/kibana/issues/65866 it('categorization job using mlcategory passes aggregatable field check', () => { - const payload = { + const payload: any = { job: { job_id: 'categorization_test', analysis_config: { @@ -310,7 +335,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -322,7 +347,7 @@ describe('ML - validateJob', () => { }); it('non-existent field reported as non aggregatable', () => { - const payload = { + const payload: any = { job: { job_id: 'categorization_test', analysis_config: { @@ -343,7 +368,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -353,8 +378,9 @@ describe('ML - validateJob', () => { }); }); + // Failing https://github.com/elastic/kibana/issues/65867 it('script field not reported as non aggregatable', () => { - const payload = { + const payload: any = { job: { job_id: 'categorization_test', analysis_config: { @@ -385,7 +411,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -399,19 +425,19 @@ describe('ML - validateJob', () => { // the following two tests validate the correct template rendering of // urls in messages with {{version}} in them to be replaced with the // specified version. (defaulting to 'current') - const docsTestPayload = getBasicPayload(); + const docsTestPayload = getBasicPayload() as any; docsTestPayload.job.analysis_config.detectors = [{ function: 'count', by_field_name: 'airline' }]; it('creates a docs url pointing to the current docs version', () => { return validateJob(callWithRequest, docsTestPayload).then(messages => { const message = messages[messages.findIndex(m => m.id === 'field_not_aggregatable')]; - expect(message.url.search('/current/')).not.to.be(-1); + expect(message.url.search('/current/')).not.toBe(-1); }); }); it('creates a docs url pointing to the master docs version', () => { return validateJob(callWithRequest, docsTestPayload, 'master').then(messages => { const message = messages[messages.findIndex(m => m.id === 'field_not_aggregatable')]; - expect(message.url.search('/master/')).not.to.be(-1); + expect(message.url.search('/master/')).not.toBe(-1); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/messages.d.ts b/x-pack/plugins/ml/server/models/job_validation/messages.d.ts new file mode 100644 index 00000000000000..772d78b4187dd0 --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_validation/messages.d.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ValidationMessage { + id: string; + url: string; +} diff --git a/x-pack/plugins/ml/server/models/job_validation/messages.js b/x-pack/plugins/ml/server/models/job_validation/messages.js index 3fd90d0a356a18..6cdbc457e6adef 100644 --- a/x-pack/plugins/ml/server/models/job_validation/messages.js +++ b/x-pack/plugins/ml/server/models/job_validation/messages.js @@ -433,6 +433,17 @@ export const getMessages = () => { } ), }, + mml_greater_than_effective_max_mml: { + status: 'WARNING', + text: i18n.translate( + 'xpack.ml.models.jobValidation.messages.mmlGreaterThanEffectiveMaxMmlMessage', + { + defaultMessage: + 'Job will not be able to run in the current cluster because model memory limit is higher than {effectiveMaxModelMemoryLimit}.', + values: { effectiveMaxModelMemoryLimit: '{{effectiveMaxModelMemoryLimit}}' }, + } + ), + }, mml_greater_than_max_mml: { status: 'ERROR', text: i18n.translate('xpack.ml.models.jobValidation.messages.mmlGreaterThanMaxMmlMessage', { diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts similarity index 81% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js rename to x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts index 3dc2bee1e8705f..4001697d743200 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts @@ -4,22 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { validateBucketSpan } from '../validate_bucket_span'; -import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../common/constants/validation'; +import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../common/constants/validation'; + +import { ValidationMessage } from './messages'; +// @ts-ignore +import { validateBucketSpan } from './validate_bucket_span'; // farequote2017 snapshot snapshot mock search response // it returns a mock for the response of PolledDataChecker's search request // to get an aggregation of non_empty_buckets with an interval of 1m. // this allows us to test bucket span estimation. -import mockFareQuoteSearchResponse from './mock_farequote_search_response'; +import mockFareQuoteSearchResponse from './__mocks__/mock_farequote_search_response.json'; // it_ops_app_logs 2017 snapshot mock search response // sparse data with a low number of buckets -import mockItSearchResponse from './mock_it_search_response'; +import mockItSearchResponse from './__mocks__/mock_it_search_response.json'; // mock callWithRequestFactory -const callWithRequestFactory = mockSearchResponse => { +const callWithRequestFactory = (mockSearchResponse: any) => { return () => { return new Promise(resolve => { resolve(mockSearchResponse); @@ -86,17 +88,17 @@ describe('ML - validateBucketSpan', () => { }; return validateBucketSpan(callWithRequestFactory(mockFareQuoteSearchResponse), job).then( - messages => { + (messages: ValidationMessage[]) => { const ids = messages.map(m => m.id); - expect(ids).to.eql([]); + expect(ids).toStrictEqual([]); } ); }); - const getJobConfig = bucketSpan => ({ + const getJobConfig = (bucketSpan: string) => ({ analysis_config: { bucket_span: bucketSpan, - detectors: [], + detectors: [] as Array<{ function?: string }>, influencers: [], }, data_description: { time_field: '@timestamp' }, @@ -111,9 +113,9 @@ describe('ML - validateBucketSpan', () => { callWithRequestFactory(mockFareQuoteSearchResponse), job, duration - ).then(messages => { + ).then((messages: ValidationMessage[]) => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_bucket_span']); + expect(ids).toStrictEqual(['success_bucket_span']); }); }); @@ -125,9 +127,9 @@ describe('ML - validateBucketSpan', () => { callWithRequestFactory(mockFareQuoteSearchResponse), job, duration - ).then(messages => { + ).then((messages: ValidationMessage[]) => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['bucket_span_high']); + expect(ids).toStrictEqual(['bucket_span_high']); }); }); @@ -135,14 +137,18 @@ describe('ML - validateBucketSpan', () => { return; } - const testBucketSpan = (bucketSpan, mockSearchResponse, test) => { + const testBucketSpan = ( + bucketSpan: string, + mockSearchResponse: any, + test: (ids: string[]) => void + ) => { const job = getJobConfig(bucketSpan); job.analysis_config.detectors.push({ function: 'count', }); return validateBucketSpan(callWithRequestFactory(mockSearchResponse), job, {}).then( - messages => { + (messages: ValidationMessage[]) => { const ids = messages.map(m => m.id); test(ids); } @@ -151,13 +157,13 @@ describe('ML - validateBucketSpan', () => { it('farequote count detector, bucket span estimation matches 15m', () => { return testBucketSpan('15m', mockFareQuoteSearchResponse, ids => { - expect(ids).to.eql(['success_bucket_span']); + expect(ids).toStrictEqual(['success_bucket_span']); }); }); it('farequote count detector, bucket span estimation does not match 1m', () => { return testBucketSpan('1m', mockFareQuoteSearchResponse, ids => { - expect(ids).to.eql(['bucket_span_estimation_mismatch']); + expect(ids).toStrictEqual(['bucket_span_estimation_mismatch']); }); }); @@ -167,7 +173,7 @@ describe('ML - validateBucketSpan', () => { // should result in a lower bucket span estimation. it('it_ops_app_logs count detector, bucket span estimation matches 6h', () => { return testBucketSpan('6h', mockItSearchResponse, ids => { - expect(ids).to.eql(['success_bucket_span']); + expect(ids).toStrictEqual(['success_bucket_span']); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts similarity index 69% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js rename to x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts index 9617982a66b0e5..e5111629f1182a 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts @@ -5,11 +5,15 @@ */ import _ from 'lodash'; -import expect from '@kbn/expect'; -import { validateCardinality } from '../validate_cardinality'; -import mockFareQuoteCardinality from './mock_farequote_cardinality'; -import mockFieldCaps from './mock_field_caps'; +import { APICaller } from 'kibana/server'; + +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; + +import mockFareQuoteCardinality from './__mocks__/mock_farequote_cardinality.json'; +import mockFieldCaps from './__mocks__/mock_field_caps.json'; + +import { validateCardinality } from './validate_cardinality'; const mockResponses = { search: mockFareQuoteCardinality, @@ -17,8 +21,8 @@ const mockResponses = { }; // mock callWithRequestFactory -const callWithRequestFactory = (responses, fail = false) => { - return requestName => { +const callWithRequestFactory = (responses: Record, fail = false): APICaller => { + return (requestName: string) => { return new Promise((resolve, reject) => { const response = responses[requestName]; if (fail) { @@ -26,7 +30,7 @@ const callWithRequestFactory = (responses, fail = false) => { } else { resolve(response); } - }); + }) as Promise; }; }; @@ -39,21 +43,23 @@ describe('ML - validateCardinality', () => { }); it('called with non-valid job argument #1, missing analysis_config', done => { - validateCardinality(callWithRequestFactory(mockResponses), {}).then( + validateCardinality(callWithRequestFactory(mockResponses), {} as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); }); it('called with non-valid job argument #2, missing datafeed_config', done => { - validateCardinality(callWithRequestFactory(mockResponses), { analysis_config: {} }).then( + validateCardinality(callWithRequestFactory(mockResponses), { + analysis_config: {}, + } as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); }); it('called with non-valid job argument #3, missing datafeed_config.indices', done => { - const job = { analysis_config: {}, datafeed_config: {} }; + const job = { analysis_config: {}, datafeed_config: {} } as CombinedJob; validateCardinality(callWithRequestFactory(mockResponses), job).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() @@ -61,7 +67,10 @@ describe('ML - validateCardinality', () => { }); it('called with non-valid job argument #4, missing data_description', done => { - const job = { analysis_config: {}, datafeed_config: { indices: [] } }; + const job = ({ + analysis_config: {}, + datafeed_config: { indices: [] }, + } as unknown) as CombinedJob; validateCardinality(callWithRequestFactory(mockResponses), job).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() @@ -69,7 +78,11 @@ describe('ML - validateCardinality', () => { }); it('called with non-valid job argument #5, missing data_description.time_field', done => { - const job = { analysis_config: {}, data_description: {}, datafeed_config: { indices: [] } }; + const job = ({ + analysis_config: {}, + data_description: {}, + datafeed_config: { indices: [] }, + } as unknown) as CombinedJob; validateCardinality(callWithRequestFactory(mockResponses), job).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() @@ -77,11 +90,11 @@ describe('ML - validateCardinality', () => { }); it('called with non-valid job argument #6, missing analysis_config.influencers', done => { - const job = { + const job = ({ analysis_config: {}, datafeed_config: { indices: [] }, data_description: { time_field: '@timestamp' }, - }; + } as unknown) as CombinedJob; validateCardinality(callWithRequestFactory(mockResponses), job).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() @@ -89,21 +102,21 @@ describe('ML - validateCardinality', () => { }); it('minimum job configuration to pass cardinality check code', () => { - const job = { + const job = ({ analysis_config: { detectors: [], influencers: [] }, data_description: { time_field: '@timestamp' }, datafeed_config: { indices: [], }, - }; + } as unknown) as CombinedJob; return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([]); + expect(ids).toStrictEqual([]); }); }); - const getJobConfig = fieldName => ({ + const getJobConfig = (fieldName: string) => ({ analysis_config: { detectors: [ { @@ -119,11 +132,18 @@ describe('ML - validateCardinality', () => { }, }); - const testCardinality = (fieldName, cardinality, test) => { + const testCardinality = ( + fieldName: string, + cardinality: number, + test: (ids: string[]) => void + ) => { const job = getJobConfig(fieldName); const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; - return validateCardinality(callWithRequestFactory(mockCardinality), job, {}).then(messages => { + return validateCardinality( + callWithRequestFactory(mockCardinality), + (job as unknown) as CombinedJob + ).then(messages => { const ids = messages.map(m => m.id); test(ids); }); @@ -132,26 +152,34 @@ describe('ML - validateCardinality', () => { it(`field '_source' not aggregatable`, () => { const job = getJobConfig('partition_field_name'); job.analysis_config.detectors[0].partition_field_name = '_source'; - return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => { + return validateCardinality( + callWithRequestFactory(mockResponses), + (job as unknown) as CombinedJob + ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['field_not_aggregatable']); + expect(ids).toStrictEqual(['field_not_aggregatable']); }); }); it(`field 'airline' aggregatable`, () => { const job = getJobConfig('partition_field_name'); - return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => { + return validateCardinality( + callWithRequestFactory(mockResponses), + (job as unknown) as CombinedJob + ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it('field not aggregatable', () => { const job = getJobConfig('partition_field_name'); - return validateCardinality(callWithRequestFactory({}), job).then(messages => { - const ids = messages.map(m => m.id); - expect(ids).to.eql(['field_not_aggregatable']); - }); + return validateCardinality(callWithRequestFactory({}), (job as unknown) as CombinedJob).then( + messages => { + const ids = messages.map(m => m.id); + expect(ids).toStrictEqual(['field_not_aggregatable']); + } + ); }); it('fields not aggregatable', () => { @@ -160,107 +188,110 @@ describe('ML - validateCardinality', () => { function: 'count', partition_field_name: 'airline', }); - return validateCardinality(callWithRequestFactory({}, true), job).then(messages => { + return validateCardinality( + callWithRequestFactory({}, true), + (job as unknown) as CombinedJob + ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['fields_not_aggregatable']); + expect(ids).toStrictEqual(['fields_not_aggregatable']); }); }); it('valid partition field cardinality', () => { return testCardinality('partition_field_name', 50, ids => { - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it('too high partition field cardinality', () => { return testCardinality('partition_field_name', 1001, ids => { - expect(ids).to.eql(['cardinality_partition_field']); + expect(ids).toStrictEqual(['cardinality_partition_field']); }); }); it('valid by field cardinality', () => { return testCardinality('by_field_name', 50, ids => { - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it('too high by field cardinality', () => { return testCardinality('by_field_name', 1001, ids => { - expect(ids).to.eql(['cardinality_by_field']); + expect(ids).toStrictEqual(['cardinality_by_field']); }); }); it('valid over field cardinality', () => { return testCardinality('over_field_name', 50, ids => { - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it('too low over field cardinality', () => { return testCardinality('over_field_name', 9, ids => { - expect(ids).to.eql(['cardinality_over_field_low']); + expect(ids).toStrictEqual(['cardinality_over_field_low']); }); }); it('too high over field cardinality', () => { return testCardinality('over_field_name', 1000001, ids => { - expect(ids).to.eql(['cardinality_over_field_high']); + expect(ids).toStrictEqual(['cardinality_over_field_high']); }); }); const cardinality = 10000; it(`disabled model_plot, over field cardinality of ${cardinality} doesn't trigger a warning`, () => { - const job = getJobConfig('over_field_name'); + const job = (getJobConfig('over_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: false }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it(`enabled model_plot, over field cardinality of ${cardinality} triggers a model plot warning`, () => { - const job = getJobConfig('over_field_name'); + const job = (getJobConfig('over_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: true }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['cardinality_model_plot_high']); + expect(ids).toStrictEqual(['cardinality_model_plot_high']); }); }); it(`disabled model_plot, by field cardinality of ${cardinality} triggers a field cardinality warning`, () => { - const job = getJobConfig('by_field_name'); + const job = (getJobConfig('by_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: false }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['cardinality_by_field']); + expect(ids).toStrictEqual(['cardinality_by_field']); }); }); it(`enabled model_plot, by field cardinality of ${cardinality} triggers a model plot warning and field cardinality warning`, () => { - const job = getJobConfig('by_field_name'); + const job = (getJobConfig('by_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: true }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['cardinality_model_plot_high', 'cardinality_by_field']); + expect(ids).toStrictEqual(['cardinality_model_plot_high', 'cardinality_by_field']); }); }); it(`enabled model_plot with terms, by field cardinality of ${cardinality} triggers just field cardinality warning`, () => { - const job = getJobConfig('by_field_name'); + const job = (getJobConfig('by_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: true, terms: 'AAL,AAB' }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['cardinality_by_field']); + expect(ids).toStrictEqual(['cardinality_by_field']); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts similarity index 76% rename from x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js rename to x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts index 22e0510782e115..cf3d6d004c37e2 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts @@ -4,21 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; - +import { APICaller } from 'kibana/server'; import { DataVisualizer } from '../data_visualizer'; import { validateJobObject } from './validate_job_object'; +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import { Detector } from '../../../common/types/anomaly_detection_jobs'; -function isValidCategorizationConfig(job, fieldName) { +function isValidCategorizationConfig(job: CombinedJob, fieldName: string): boolean { return ( typeof job.analysis_config.categorization_field_name !== 'undefined' && fieldName === 'mlcategory' ); } -function isScriptField(job, fieldName) { - const scriptFields = Object.keys(_.get(job, 'datafeed_config.script_fields', {})); +function isScriptField(job: CombinedJob, fieldName: string): boolean { + const scriptFields = Object.keys(job.datafeed_config.script_fields ?? {}); return scriptFields.includes(fieldName); } @@ -30,10 +31,21 @@ const PARTITION_FIELD_CARDINALITY_THRESHOLD = 1000; const BY_FIELD_CARDINALITY_THRESHOLD = 1000; const MODEL_PLOT_THRESHOLD_HIGH = 100; -const validateFactory = (callWithRequest, job) => { +type Messages = Array<{ id: string; fieldName?: string }>; + +type Validator = (obj: { + type: string; + isInvalid: (cardinality: number) => boolean; + messageId?: string; +}) => Promise<{ + modelPlotCardinality: number; + messages: Messages; +}>; + +const validateFactory = (callWithRequest: APICaller, job: CombinedJob): Validator => { const dv = new DataVisualizer(callWithRequest); - const modelPlotConfigTerms = _.get(job, ['model_plot_config', 'terms'], ''); + const modelPlotConfigTerms = job?.model_plot_config?.terms ?? ''; const modelPlotConfigFieldCount = modelPlotConfigTerms.length > 0 ? modelPlotConfigTerms.split(',').length : 0; @@ -42,8 +54,11 @@ const validateFactory = (callWithRequest, job) => { // if model_plot_config.terms is used, it doesn't count the real cardinality of the field // but adds only the count of fields used in model_plot_config.terms let modelPlotCardinality = 0; - const messages = []; - const fieldName = `${type}_field_name`; + const messages: Messages = []; + const fieldName = `${type}_field_name` as keyof Pick< + Detector, + 'by_field_name' | 'over_field_name' | 'partition_field_name' + >; const detectors = job.analysis_config.detectors; const relevantDetectors = detectors.filter(detector => { @@ -52,7 +67,7 @@ const validateFactory = (callWithRequest, job) => { if (relevantDetectors.length > 0) { try { - const uniqueFieldNames = _.uniq(relevantDetectors.map(f => f[fieldName])); + const uniqueFieldNames = [...new Set(relevantDetectors.map(f => f[fieldName]))] as string[]; // use fieldCaps endpoint to get data about whether fields are aggregatable const fieldCaps = await callWithRequest('fieldCaps', { @@ -60,7 +75,7 @@ const validateFactory = (callWithRequest, job) => { fields: uniqueFieldNames, }); - let aggregatableFieldNames = []; + let aggregatableFieldNames: string[] = []; // parse fieldCaps to return an array of just the fields which are aggregatable if (typeof fieldCaps === 'object' && typeof fieldCaps.fields === 'object') { aggregatableFieldNames = uniqueFieldNames.filter(field => { @@ -81,12 +96,14 @@ const validateFactory = (callWithRequest, job) => { ); uniqueFieldNames.forEach(uniqueFieldName => { - const field = _.find(stats.aggregatableExistsFields, { fieldName: uniqueFieldName }); - if (typeof field === 'object') { + const field = stats.aggregatableExistsFields.find( + fieldData => fieldData.fieldName === uniqueFieldName + ); + if (field !== undefined && typeof field === 'object' && field.stats) { modelPlotCardinality += - modelPlotConfigFieldCount > 0 ? modelPlotConfigFieldCount : field.stats.cardinality; + modelPlotConfigFieldCount > 0 ? modelPlotConfigFieldCount : field.stats.cardinality!; - if (isInvalid(field.stats.cardinality)) { + if (isInvalid(field.stats.cardinality!)) { messages.push({ id: messageId || `cardinality_${type}_field`, fieldName: uniqueFieldName, @@ -115,7 +132,7 @@ const validateFactory = (callWithRequest, job) => { if (relevantDetectors.length === 1) { messages.push({ id: 'field_not_aggregatable', - fieldName: relevantDetectors[0][fieldName], + fieldName: relevantDetectors[0][fieldName]!, }); } else { messages.push({ id: 'fields_not_aggregatable' }); @@ -129,10 +146,16 @@ const validateFactory = (callWithRequest, job) => { }; }; -export async function validateCardinality(callWithRequest, job) { +export async function validateCardinality( + callWithRequest: APICaller, + job?: CombinedJob +): Promise> | never { const messages = []; - validateJobObject(job); + if (!validateJobObject(job)) { + // required for TS type casting, validateJobObject throws an error internally. + throw new Error(); + } // find out if there are any relevant detector field names // where cardinality checks could be run against. @@ -140,14 +163,13 @@ export async function validateCardinality(callWithRequest, job) { return d.by_field_name || d.over_field_name || d.partition_field_name; }); if (numDetectorsWithFieldNames.length === 0) { - return Promise.resolve([]); + return []; } // validate({ type, isInvalid }) asynchronously returns an array of validation messages const validate = validateFactory(callWithRequest, job); - const modelPlotEnabled = - (job.model_plot_config && job.model_plot_config.enabled === true) || false; + const modelPlotEnabled = job.model_plot_config?.enabled ?? false; // check over fields (population analysis) const validateOverFieldsLow = validate({ diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts similarity index 63% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js rename to x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts index 06b2e5205fdbde..df3310ad9f5e82 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts @@ -4,19 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { validateInfluencers } from '../validate_influencers'; +import { APICaller } from 'kibana/server'; + +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; + +import { validateInfluencers } from './validate_influencers'; describe('ML - validateInfluencers', () => { it('called without arguments throws an error', done => { - validateInfluencers().then( + validateInfluencers( + (undefined as unknown) as APICaller, + (undefined as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without job argument.')), () => done() ); }); it('called with non-valid job argument #1, missing analysis_config', done => { - validateInfluencers(undefined, {}).then( + validateInfluencers((undefined as unknown) as APICaller, ({} as unknown) as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -28,7 +34,7 @@ describe('ML - validateInfluencers', () => { datafeed_config: { indices: [] }, data_description: { time_field: '@timestamp' }, }; - validateInfluencers(undefined, job).then( + validateInfluencers((undefined as unknown) as APICaller, (job as unknown) as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -40,25 +46,29 @@ describe('ML - validateInfluencers', () => { datafeed_config: { indices: [] }, data_description: { time_field: '@timestamp' }, }; - validateInfluencers(undefined, job).then( + validateInfluencers((undefined as unknown) as APICaller, (job as unknown) as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); }); - const getJobConfig = (influencers = [], detectors = []) => ({ - analysis_config: { detectors, influencers }, - data_description: { time_field: '@timestamp' }, - datafeed_config: { - indices: [], - }, - }); + const getJobConfig: ( + influencers?: string[], + detectors?: CombinedJob['analysis_config']['detectors'] + ) => CombinedJob = (influencers = [], detectors = []) => + (({ + analysis_config: { detectors, influencers }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [], + }, + } as unknown) as CombinedJob); it('success_influencer', () => { const job = getJobConfig(['airline']); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_influencers']); + expect(ids).toStrictEqual(['success_influencers']); }); }); @@ -69,31 +79,30 @@ describe('ML - validateInfluencers', () => { { detector_description: 'count', function: 'count', - rules: [], detector_index: 0, }, ] ); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([]); + expect(ids).toStrictEqual([]); }); }); it('influencer_low', () => { const job = getJobConfig(); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['influencer_low']); + expect(ids).toStrictEqual(['influencer_low']); }); }); it('influencer_high', () => { const job = getJobConfig(['i1', 'i2', 'i3', 'i4']); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['influencer_high']); + expect(ids).toStrictEqual(['influencer_high']); }); }); @@ -105,14 +114,13 @@ describe('ML - validateInfluencers', () => { detector_description: 'count', function: 'count', partition_field_name: 'airline', - rules: [], detector_index: 0, }, ] ); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['influencer_low_suggestion']); + expect(ids).toStrictEqual(['influencer_low_suggestion']); }); }); @@ -124,27 +132,24 @@ describe('ML - validateInfluencers', () => { detector_description: 'count', function: 'count', partition_field_name: 'partition_field', - rules: [], detector_index: 0, }, { detector_description: 'count', function: 'count', by_field_name: 'by_field', - rules: [], detector_index: 0, }, { detector_description: 'count', function: 'count', over_field_name: 'over_field', - rules: [], detector_index: 0, }, ] ); - return validateInfluencers(undefined, job).then(messages => { - expect(messages).to.eql([ + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { + expect(messages).toStrictEqual([ { id: 'influencer_low_suggestions', influencerSuggestion: '["partition_field","by_field","over_field"]', diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts similarity index 89% rename from x-pack/plugins/ml/server/models/job_validation/validate_influencers.js rename to x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts index 60fd5c37b99586..e54ffc4586a8e3 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_influencers.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts @@ -4,19 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; + import { validateJobObject } from './validate_job_object'; const INFLUENCER_LOW_THRESHOLD = 0; const INFLUENCER_HIGH_THRESHOLD = 4; const DETECTOR_FIELD_NAMES_THRESHOLD = 1; -export async function validateInfluencers(callWithRequest, job) { +export async function validateInfluencers(callWithRequest: APICaller, job: CombinedJob) { validateJobObject(job); const messages = []; const influencers = job.analysis_config.influencers; - const detectorFieldNames = []; + const detectorFieldNames: string[] = []; job.analysis_config.detectors.forEach(d => { if (d.by_field_name) { detectorFieldNames.push(d.by_field_name); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_job_object.ts b/x-pack/plugins/ml/server/models/job_validation/validate_job_object.ts index b0271fb5b4f45c..0d89656e051173 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_job_object.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_job_object.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; -export function validateJobObject(job: CombinedJob | null) { +export function validateJobObject(job: CombinedJob | null | undefined): job is CombinedJob | never { if (job === null || typeof job !== 'object') { throw new Error( i18n.translate('xpack.ml.models.jobValidation.validateJobObject.jobIsNotObjectErrorMessage', { @@ -93,4 +93,5 @@ export function validateJobObject(job: CombinedJob | null) { ) ); } + return true; } diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts index 6b5d5614325bfc..bf88716181bb3d 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts @@ -24,6 +24,7 @@ describe('ML - validateModelMemoryLimit', () => { }, limits: { max_model_memory_limit: '30mb', + effective_max_model_memory_limit: '40mb', }, }; @@ -211,6 +212,30 @@ describe('ML - validateModelMemoryLimit', () => { }); }); + it('Called with no duration or split and mml above limit, no max setting', () => { + const job = getJobConfig(); + const duration = undefined; + // @ts-ignore + job.analysis_limits.model_memory_limit = '31mb'; + + return validateModelMemoryLimit(getMockCallWithRequest(), job, duration).then(messages => { + const ids = messages.map(m => m.id); + expect(ids).toEqual([]); + }); + }); + + it('Called with no duration or split and mml above limit, no max setting, above effective max mml', () => { + const job = getJobConfig(); + const duration = undefined; + // @ts-ignore + job.analysis_limits.model_memory_limit = '41mb'; + + return validateModelMemoryLimit(getMockCallWithRequest(), job, duration).then(messages => { + const ids = messages.map(m => m.id); + expect(ids).toEqual(['mml_greater_than_effective_max_mml']); + }); + }); + it('Called with small number of detectors, so estimated mml is under specified mml, no max setting', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts index 16a48addfeaf4a..5c3250af6ef468 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts @@ -10,6 +10,7 @@ import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { validateJobObject } from './validate_job_object'; import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_limit'; import { ALLOWED_DATA_UNITS } from '../../../common/constants/validation'; +import { MlInfoResponse } from '../../../common/types/ml_server_info'; // The minimum value the backend expects is 1MByte const MODEL_MEMORY_LIMIT_MINIMUM_BYTES = 1048576; @@ -50,9 +51,9 @@ export async function validateModelMemoryLimit( // retrieve the max_model_memory_limit value from the server // this will be unset unless the user has set this on their cluster - const maxModelMemoryLimit: string | undefined = ( - await callWithRequest('ml.info') - )?.limits?.max_model_memory_limit?.toUpperCase(); + const info = await callWithRequest('ml.info'); + const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase(); + const effectiveMaxModelMemoryLimit = info.limits.effective_max_model_memory_limit?.toUpperCase(); if (runCalcModelMemoryTest) { const { modelMemoryLimit } = await calculateModelMemoryLimitProvider(callWithRequest)( @@ -113,17 +114,35 @@ export async function validateModelMemoryLimit( // if max_model_memory_limit has been set, // make sure the user defined MML is not greater than it - if (maxModelMemoryLimit !== undefined && mml !== null) { - // @ts-ignore - const maxMmlBytes = numeral(maxModelMemoryLimit).value(); + if (mml !== null) { + let maxMmlExceeded = false; // @ts-ignore const mmlBytes = numeral(mml).value(); - if (mmlBytes > maxMmlBytes) { - messages.push({ - id: 'mml_greater_than_max_mml', - maxModelMemoryLimit, - mml, - }); + + if (maxModelMemoryLimit !== undefined) { + // @ts-ignore + const maxMmlBytes = numeral(maxModelMemoryLimit).value(); + if (mmlBytes > maxMmlBytes) { + maxMmlExceeded = true; + messages.push({ + id: 'mml_greater_than_max_mml', + maxModelMemoryLimit, + mml, + }); + } + } + + if (effectiveMaxModelMemoryLimit !== undefined && maxMmlExceeded === false) { + // @ts-ignore + const effectiveMaxMmlBytes = numeral(effectiveMaxModelMemoryLimit).value(); + if (mmlBytes > effectiveMaxMmlBytes) { + messages.push({ + id: 'mml_greater_than_effective_max_mml', + maxModelMemoryLimit, + mml, + effectiveMaxModelMemoryLimit, + }); + } } } diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts similarity index 76% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js rename to x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts index e3ef62e5074850..2c3b2dd4dc6ae1 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts @@ -5,28 +5,32 @@ */ import _ from 'lodash'; -import expect from '@kbn/expect'; -import { isValidTimeField, validateTimeRange } from '../validate_time_range'; -import mockTimeField from './mock_time_field'; -import mockTimeFieldNested from './mock_time_field_nested'; -import mockTimeRange from './mock_time_range'; +import { APICaller } from 'kibana/server'; + +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; + +import { isValidTimeField, validateTimeRange } from './validate_time_range'; + +import mockTimeField from './__mocks__/mock_time_field.json'; +import mockTimeFieldNested from './__mocks__/mock_time_field_nested.json'; +import mockTimeRange from './__mocks__/mock_time_range.json'; const mockSearchResponse = { fieldCaps: mockTimeField, search: mockTimeRange, }; -const callWithRequestFactory = resp => { - return path => { +const callWithRequestFactory = (resp: any): APICaller => { + return (path: string) => { return new Promise(resolve => { resolve(resp[path]); - }); + }) as Promise; }; }; function getMinimalValidJob() { - return { + return ({ analysis_config: { bucket_span: '15m', detectors: [], @@ -36,12 +40,15 @@ function getMinimalValidJob() { datafeed_config: { indices: [], }, - }; + } as unknown) as CombinedJob; } describe('ML - isValidTimeField', () => { it('called without job config argument triggers Promise rejection', done => { - isValidTimeField(callWithRequestFactory(mockSearchResponse)).then( + isValidTimeField( + callWithRequestFactory(mockSearchResponse), + (undefined as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without job argument.')), () => done() ); @@ -50,7 +57,7 @@ describe('ML - isValidTimeField', () => { it('time_field `@timestamp`', done => { isValidTimeField(callWithRequestFactory(mockSearchResponse), getMinimalValidJob()).then( valid => { - expect(valid).to.be(true); + expect(valid).toBe(true); done(); }, () => done(new Error('isValidTimeField Promise failed for time_field `@timestamp`.')) @@ -71,7 +78,7 @@ describe('ML - isValidTimeField', () => { mockJobConfigNestedDate ).then( valid => { - expect(valid).to.be(true); + expect(valid).toBe(true); done(); }, () => done(new Error('isValidTimeField Promise failed for time_field `metadata.timestamp`.')) @@ -81,14 +88,19 @@ describe('ML - isValidTimeField', () => { describe('ML - validateTimeRange', () => { it('called without arguments', done => { - validateTimeRange(callWithRequestFactory(mockSearchResponse)).then( + validateTimeRange( + callWithRequestFactory(mockSearchResponse), + (undefined as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without job argument.')), () => done() ); }); it('called with non-valid job argument #2, missing datafeed_config', done => { - validateTimeRange(callWithRequestFactory(mockSearchResponse), { analysis_config: {} }).then( + validateTimeRange(callWithRequestFactory(mockSearchResponse), ({ + analysis_config: {}, + } as unknown) as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -96,7 +108,10 @@ describe('ML - validateTimeRange', () => { it('called with non-valid job argument #3, missing datafeed_config.indices', done => { const job = { analysis_config: {}, datafeed_config: {} }; - validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then( + validateTimeRange( + callWithRequestFactory(mockSearchResponse), + (job as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -104,7 +119,10 @@ describe('ML - validateTimeRange', () => { it('called with non-valid job argument #4, missing data_description', done => { const job = { analysis_config: {}, datafeed_config: { indices: [] } }; - validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then( + validateTimeRange( + callWithRequestFactory(mockSearchResponse), + (job as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -112,7 +130,10 @@ describe('ML - validateTimeRange', () => { it('called with non-valid job argument #5, missing data_description.time_field', done => { const job = { analysis_config: {}, data_description: {}, datafeed_config: { indices: [] } }; - validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then( + validateTimeRange( + callWithRequestFactory(mockSearchResponse), + (job as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -128,7 +149,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_field_invalid']); + expect(ids).toStrictEqual(['time_field_invalid']); }); }); @@ -142,7 +163,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_range_short']); + expect(ids).toStrictEqual(['time_range_short']); }); }); @@ -154,7 +175,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_range_short']); + expect(ids).toStrictEqual(['time_range_short']); }); }); @@ -166,7 +187,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_range_short']); + expect(ids).toStrictEqual(['time_range_short']); }); }); @@ -178,7 +199,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_time_range']); + expect(ids).toStrictEqual(['success_time_range']); }); }); @@ -190,7 +211,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_range_before_epoch']); + expect(ids).toStrictEqual(['time_range_before_epoch']); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts index 5f734387698512..4fb09af94dcc68 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts @@ -37,9 +37,9 @@ export async function isValidTimeField(callAsCurrentUser: APICaller, job: Combin fields: [timeField], }); - let fieldType = fieldCaps.fields[timeField]?.date?.type; + let fieldType = fieldCaps?.fields[timeField]?.date?.type; if (fieldType === undefined) { - fieldType = fieldCaps.fields[timeField]?.date_nanos?.type; + fieldType = fieldCaps?.fields[timeField]?.date_nanos?.type; } return fieldType === ES_FIELD_TYPES.DATE || fieldType === ES_FIELD_TYPES.DATE_NANOS; } @@ -47,7 +47,7 @@ export async function isValidTimeField(callAsCurrentUser: APICaller, job: Combin export async function validateTimeRange( callAsCurrentUser: APICaller, job: CombinedJob, - timeRange: TimeRange | undefined + timeRange?: TimeRange ) { const messages: ValidateTimeRangeMessage[] = []; diff --git a/x-pack/plugins/ml/server/shared.ts b/x-pack/plugins/ml/server/shared.ts new file mode 100644 index 00000000000000..1e50950bc3bced --- /dev/null +++ b/x-pack/plugins/ml/server/shared.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 * from '../common/types/anomalies'; diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index b90a9aa7d139ae..0722a80dc2c111 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -140,7 +140,7 @@ export class BulkUploader { async _fetchAndUpload(usageCollection) { const collectorsReady = await usageCollection.areAllCollectorsReady(); const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector); - if (!collectorsReady) { + if (!collectorsReady || typeof this.kibanaStatusGetter !== 'function') { this._log.debug('Skipping bulk uploading because not all collectors are ready'); if (hasUsageCollectors) { this._lastFetchUsageTime = null; @@ -151,7 +151,7 @@ export class BulkUploader { const data = await usageCollection.bulkFetch(this._cluster.callAsInternalUser); const payload = this.toBulkUploadFormat(compact(data), usageCollection); - if (payload) { + if (payload && payload.length > 0) { try { this._log.debug(`Uploading bulk stats payload to the local cluster`); const result = await this._onPayload(payload); @@ -244,7 +244,7 @@ export class BulkUploader { */ toBulkUploadFormat(rawData, usageCollection) { if (rawData.length === 0) { - return; + return []; } // convert the raw data to a nested object by taking each payload through diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details.tsx index ac2a2997515d52..6579d18556cc06 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details.tsx +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details.tsx @@ -44,6 +44,7 @@ export const ShardDetails = ({ index, shard, operations }: Props) => { setShardVisibility(!shardVisibility)} + data-test-subj="openCloseShardDetails" > [{shard.id[0]}][ {shard.id[2]}] diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details_tree_node.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details_tree_node.tsx index 1d8f915d3d47d3..d89046090a9611 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details_tree_node.tsx +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details_tree_node.tsx @@ -94,6 +94,7 @@ export const ShardDetailsTreeNode = ({ operation, index, shard }: Props) => { highlight({ indexName: index.name, operation, shard })} > {i18n.translate('xpack.searchProfiler.profileTree.body.viewDetailsLabel', { diff --git a/x-pack/plugins/searchprofiler/public/application/components/searchprofiler_tabs.tsx b/x-pack/plugins/searchprofiler/public/application/components/searchprofiler_tabs.tsx index 19224e7099fd66..7e6dad7df5528a 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/searchprofiler_tabs.tsx +++ b/x-pack/plugins/searchprofiler/public/application/components/searchprofiler_tabs.tsx @@ -24,6 +24,7 @@ export const SearchProfilerTabs = ({ activeTab, activateTab, has }: Props) => { return ( activateTab('searches')} @@ -33,6 +34,7 @@ export const SearchProfilerTabs = ({ activeTab, activateTab, has }: Props) => { })} activateTab('aggregations')} diff --git a/x-pack/plugins/searchprofiler/public/application/containers/profile_query_editor.tsx b/x-pack/plugins/searchprofiler/public/application/containers/profile_query_editor.tsx index 5348c55ad52139..f6377d2b4f9067 100644 --- a/x-pack/plugins/searchprofiler/public/application/containers/profile_query_editor.tsx +++ b/x-pack/plugins/searchprofiler/public/application/containers/profile_query_editor.tsx @@ -120,7 +120,12 @@ export const ProfileQueryEditor = memo(() => { - handleProfileClick()}> + handleProfileClick()} + > {i18n.translate('xpack.searchProfiler.formProfileButtonLabel', { defaultMessage: 'Profile', diff --git a/x-pack/plugins/siem/common/constants.ts b/x-pack/plugins/siem/common/constants.ts index 9bfd89e8bb5764..bcf5c78be36445 100644 --- a/x-pack/plugins/siem/common/constants.ts +++ b/x-pack/plugins/siem/common/constants.ts @@ -91,8 +91,6 @@ export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/ export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`; export const TIMELINE_URL = '/api/timeline'; -export const TIMELINE_DRAFT_CLEAN_URL = `${TIMELINE_URL}/_draft/clean`; -export const TIMELINE_DRAFT_URL = `${TIMELINE_URL}/_draft`; export const TIMELINE_EXPORT_URL = `${TIMELINE_URL}/_export`; export const TIMELINE_IMPORT_URL = `${TIMELINE_URL}/_import`; diff --git a/x-pack/plugins/siem/common/types/timeline/index.ts b/x-pack/plugins/siem/common/types/timeline/index.ts index e87986fd1bdf22..43f66da6109df1 100644 --- a/x-pack/plugins/siem/common/types/timeline/index.ts +++ b/x-pack/plugins/siem/common/types/timeline/index.ts @@ -136,13 +136,11 @@ const SavedSortRuntimeType = runtimeTypes.partial({ export enum TimelineType { default = 'default', - draft = 'draft', template = 'template', } export const TimelineTypeLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineType.template), - runtimeTypes.literal(TimelineType.draft), runtimeTypes.literal(TimelineType.default), ]); 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 00000000000000..2d650b1bbd9d18 --- /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/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts index 26006ec5030b68..7b2c6f3b55b2e9 100644 --- a/x-pack/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts @@ -27,10 +27,10 @@ import { HOSTS_PAGE } from '../urls/navigation'; describe('toggle column in timeline', () => { before(() => { loginAndWaitForPage(HOSTS_PAGE); - openTimeline(); }); beforeEach(() => { + openTimeline(); populateTimeline(); }); diff --git a/x-pack/plugins/siem/cypress/integration/url_state.spec.ts b/x-pack/plugins/siem/cypress/integration/url_state.spec.ts index 489315a8196473..cd60745b190406 100644 --- a/x-pack/plugins/siem/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/url_state.spec.ts @@ -30,7 +30,7 @@ import { openAllHosts } from '../tasks/hosts/main'; import { waitForIpsTableToBeLoaded } from '../tasks/network/flows'; import { clearSearchBar, kqlSearch, navigateFromHeaderTo } from '../tasks/siem_header'; -import { openTimeline, openTimelineIfClosed } from '../tasks/siem_main'; +import { openTimeline } from '../tasks/siem_main'; import { addDescriptionToTimeline, addNameToTimeline, @@ -174,11 +174,10 @@ describe('url state', () => { kqlSearch('source.ip: "10.142.0.9" {enter}'); navigateFromHeaderTo(HOSTS); - cy.get(NETWORK).should($div => - // @ts-ignore - expect($div[0].attributes.href.value).to.include( - "#/link-to/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" - ) + cy.get(NETWORK).should( + 'have.attr', + 'href', + "#/link-to/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" ); }); @@ -188,20 +187,16 @@ describe('url state', () => { openAllHosts(); waitForAllHostsToBeLoaded(); - cy.get(HOSTS).should($div => - // @ts-ignore - expect($div[0].attributes.href.value).to.include( - "#/link-to/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" - ) + cy.get(HOSTS).should( + 'have.attr', + 'href', + "#/link-to/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" ); - - cy.get(NETWORK).should($div => - // @ts-ignore - expect($div[0].attributes.href.value).to.include( - "#/link-to/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" - ) + cy.get(NETWORK).should( + 'have.attr', + 'href', + "#/link-to/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" ); - cy.get(HOSTS_NAMES) .first() .invoke('text') @@ -211,29 +206,24 @@ describe('url state', () => { clearSearchBar(); kqlSearch('agent.type: "auditbeat" {enter}'); - cy.get(ANOMALIES_TAB).should($div => - // @ts-ignore - expect($div[0].attributes.href.value).to.include( - "#/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" - ) + cy.get(ANOMALIES_TAB).should( + 'have.attr', + 'href', + "#/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" ); - cy.get(BREADCRUMBS) .eq(1) - .should($div => - // @ts-ignore - expect($div[0].attributes.href.value).to.include( - "#/link-to/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" - ) + .should( + 'have.attr', + 'href', + "#/link-to/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" ); - cy.get(BREADCRUMBS) .eq(2) - .should($div => - // @ts-ignore - expect($div[0].attributes.href.value).to.include( - "#/link-to/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" - ) + .should( + 'have.attr', + 'href', + "#/link-to/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" ); }); @@ -246,7 +236,7 @@ describe('url state', () => { it('sets and reads the url state for timeline by id', () => { loginAndWaitForPage(HOSTS_PAGE); - openTimelineIfClosed(); + openTimeline(); executeTimelineKQL('host.name: *'); cy.get(SERVER_SIDE_EVENT_COUNT) diff --git a/x-pack/plugins/siem/cypress/objects/case.ts b/x-pack/plugins/siem/cypress/objects/case.ts index 1c7bc34bca417a..12d3f925169afb 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 b1e4c665153522..4fa6b69eea7c31 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 00000000000000..5a1e897c43e27c --- /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 f3745322013241..8ebe35e173e59b 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 00000000000000..9172e02708ae76 --- /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/cypress/tasks/create_new_case.ts b/x-pack/plugins/siem/cypress/tasks/create_new_case.ts index 491fdd84e9b38e..b7078a1033de89 100644 --- a/x-pack/plugins/siem/cypress/tasks/create_new_case.ts +++ b/x-pack/plugins/siem/cypress/tasks/create_new_case.ts @@ -32,7 +32,6 @@ export const createNewCase = (newCase: TestCase) => { cy.get(INSERT_TIMELINE_BTN).click({ force: true }); cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`); cy.get(TIMELINE).should('be.visible'); - cy.wait(300); cy.get(TIMELINE) .eq(1) .click({ force: true }); diff --git a/x-pack/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/plugins/siem/public/components/events_viewer/index.tsx index 5131033d994a17..bc6a1b3b77bfa9 100644 --- a/x-pack/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/plugins/siem/public/components/events_viewer/index.tsx @@ -16,7 +16,6 @@ import { SubsetTimelineModel, TimelineModel, } from '../../store/timeline/model'; -import { getNewTimeline } from '../../store/timeline/helpers'; import { OnChangeItemsPerPage } from '../timeline/events'; import { Filter } from '../../../../../../src/plugins/data/public'; import { useUiSetting } from '../../lib/kibana'; @@ -45,7 +44,7 @@ const defaultTimelineTypeContext = { }; const StatefulEventsViewerComponent: React.FC = ({ - addTimeline, + createTimeline, columns, dataProviders, deletedEventIds, @@ -76,18 +75,8 @@ const StatefulEventsViewerComponent: React.FC = ({ ); useEffect(() => { - if (addTimeline != null) { - addTimeline({ - id, - timeline: getNewTimeline({ - id, - columns, - sort, - itemsPerPage, - showCheckboxes, - showRowRenderers, - }), - }); + if (createTimeline != null) { + createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); } return () => { deleteEventQuery({ id, inputId: 'global' }); @@ -191,7 +180,7 @@ const makeMapStateToProps = () => { }; const mapDispatchToProps = { - addTimeline: timelineActions.addTimeline, + createTimeline: timelineActions.createTimeline, deleteEventQuery: inputsActions.deleteOneQuery, updateItemsPerPage: timelineActions.updateItemsPerPage, removeColumn: timelineActions.removeColumn, diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap index e378afd9c7fec9..c4bdff7ea649a7 100644 --- a/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"
      "`; +exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"
      "`; -exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"
      "`; +exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"
      "`; diff --git a/x-pack/plugins/siem/public/components/ml_popover/types.ts b/x-pack/plugins/siem/public/components/ml_popover/types.ts index 58d40c298b3294..005f93650a8eb0 100644 --- a/x-pack/plugins/siem/public/components/ml_popover/types.ts +++ b/x-pack/plugins/siem/public/components/ml_popover/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditMessageBase } from '../../../../ml/common/types/audit_message'; +import { AuditMessageBase } from '../../../../ml/public'; import { MlError } from '../ml/types'; export interface Group { diff --git a/x-pack/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx b/x-pack/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx index 7fb03b98432d0c..74b9a8cad98dc9 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx @@ -20,7 +20,7 @@ const getExportedIds = (selectedTimelines: OpenTimelineResult[]) => { ); }; -export const useEditTimelineBatchActions = ({ +export const useEditTimelinBatchActions = ({ deleteTimelines, selectedItems, tableRef, diff --git a/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts index 700fd15cdea053..a7c0b08fc8a21e 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -300,7 +300,7 @@ describe('helpers', () => { sortDirection: 'desc', }, title: '', - timelineType: TimelineType.draft, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, version: '1', @@ -397,14 +397,13 @@ describe('helpers', () => { sortDirection: 'desc', }, title: '', - timelineType: TimelineType.draft, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, version: '1', width: 1100, }); }); - test('should merge columns when event.action is deleted without two extra column names of user.name', () => { const timeline = { savedObjectId: 'savedObject-1', @@ -417,80 +416,38 @@ describe('helpers', () => { savedObjectId: 'savedObject-1', columns: [ { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: '@timestamp', - placeholder: undefined, - type: undefined, width: 190, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'message', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'event.category', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'host.name', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'source.ip', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'destination.ip', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'user.name', - placeholder: undefined, - type: undefined, width: 180, }, ], @@ -517,7 +474,7 @@ describe('helpers', () => { }, loadingEventIds: [], title: '', - timelineType: TimelineType.draft, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, noteIds: [], @@ -685,7 +642,7 @@ describe('helpers', () => { }, loadingEventIds: [], title: '', - timelineType: TimelineType.draft, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, noteIds: [], diff --git a/x-pack/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts index 103040c3b0d9e8..681d39feb09f81 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts @@ -251,10 +251,6 @@ export const queryTimelineById = ({ } }; -export const epicUpdateTimeline = ({ id, timeline }: UpdateTimeline) => [ - dispatchAddTimeline({ id, timeline }), -]; - export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeline => ({ duplicate, id, diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx index fee6e6614b863c..731c6d1ca9806a 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount } from 'enzyme'; import { MockedProvider } from 'react-apollo/test-utils'; import React from 'react'; +import { ThemeProvider } from 'styled-components'; import { wait } from '../../lib/helpers'; -import { TestProviders, apolloClient } from '../../mock/test_providers'; +import { TestProviderWithoutDragAndDrop, apolloClient } from '../../mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../mock/timeline_results'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; @@ -39,6 +41,7 @@ jest.mock('./use_timeline_types', () => { }); describe('StatefulOpenTimeline', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); const title = 'All Timelines / Open Timelines'; beforeEach(() => { ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ @@ -55,17 +58,19 @@ describe('StatefulOpenTimeline', () => { test('it has the expected initial state', () => { const wrapper = mount( - - - - - + + + + + + + ); const componentProps = wrapper @@ -89,17 +94,19 @@ describe('StatefulOpenTimeline', () => { describe('#onQueryChange', () => { test('it updates the query state with the expected trimmed value when the user enters a query', () => { const wrapper = mount( - - - - - + + + + + + + ); wrapper .find('[data-test-subj="search-bar"] input') @@ -114,16 +121,18 @@ describe('StatefulOpenTimeline', () => { test('it appends the word "with" to the Showing in Timelines message when the user enters a query', async () => { const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -142,16 +151,18 @@ describe('StatefulOpenTimeline', () => { test('echos (renders) the query when the user enters a query', async () => { const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -172,16 +183,18 @@ describe('StatefulOpenTimeline', () => { describe('#focusInput', () => { test('focuses the input when the component mounts', async () => { const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -201,16 +214,18 @@ describe('StatefulOpenTimeline', () => { const addTimelinesToFavorites = jest.fn(); const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -245,16 +260,18 @@ describe('StatefulOpenTimeline', () => { const deleteTimelines = jest.fn(); const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -286,17 +303,19 @@ describe('StatefulOpenTimeline', () => { describe('#onSelectionChange', () => { test('it updates the selection state when timelines are selected', async () => { const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -318,17 +337,19 @@ describe('StatefulOpenTimeline', () => { describe('#onTableChange', () => { test('it updates the sort state when the user clicks on a column to sort it', () => { const wrapper = mount( - - - - - + + + + + + + ); expect( @@ -355,17 +376,19 @@ describe('StatefulOpenTimeline', () => { describe('#onToggleOnlyFavorites', () => { test('it updates the onlyFavorites state when the user clicks the Only Favorites button', () => { const wrapper = mount( - - - - - + + + + + + + ); expect( @@ -392,17 +415,19 @@ describe('StatefulOpenTimeline', () => { describe('#onToggleShowNotes', () => { test('it updates the itemIdToExpandedNotesRowMap state when the user clicks the expand notes button', async () => { const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -442,17 +467,19 @@ describe('StatefulOpenTimeline', () => { test('it renders the expanded notes when the expand button is clicked', async () => { const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -475,19 +502,21 @@ describe('StatefulOpenTimeline', () => { ).toEqual('elastic'); }); - test('it renders the tabs', async () => { + test('it renders the title', async () => { const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -501,17 +530,19 @@ describe('StatefulOpenTimeline', () => { describe('#resetSelectionState', () => { test('when the user deletes selected timelines, resetSelectionState is invoked to clear the selection state', async () => { const wrapper = mount( - - - - - + + + + + + + ); const getSelectedItem = (): [] => wrapper @@ -530,17 +561,19 @@ describe('StatefulOpenTimeline', () => { test('it renders the expected count of matching timelines when no query has been entered', async () => { const wrapper = mount( - + - + + + - + ); await wait(); @@ -560,17 +593,19 @@ describe('StatefulOpenTimeline', () => { const onOpenTimeline = jest.fn(); const wrapper = mount( - - - - - + + + + + + + ); await wait(); @@ -596,17 +631,19 @@ describe('StatefulOpenTimeline', () => { const onOpenTimeline = jest.fn(); const wrapper = mount( - - - - - + + + + + + + ); await wait(); diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx index ce0eb52bc38392..e172a006abe4b0 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiBasicTable } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; -import { OpenTimelineProps, OpenTimelineResult, ActionTimelineToShow } from './types'; +import { OpenTimelineProps, OpenTimelineResult } from './types'; import { SearchRow } from './search_row'; import { TimelinesTable } from './timelines_table'; import { ImportDataModal } from '../import_data_modal'; @@ -22,7 +22,7 @@ import { UtilityBarSection, UtilityBarAction, } from '../utility_bar'; -import { useEditTimelineBatchActions } from './edit_timeline_batch_actions'; +import { useEditTimelinBatchActions } from './edit_timeline_batch_actions'; import { useEditTimelineActions } from './edit_timeline_actions'; import { EditOneTimelineAction } from './export_timeline'; @@ -65,7 +65,7 @@ export const OpenTimeline = React.memo( onCompleteEditTimelineAction, } = useEditTimelineActions(); - const { getBatchItemsPopoverContent } = useEditTimelineBatchActions({ + const { getBatchItemsPopoverContent } = useEditTimelinBatchActions({ deleteTimelines, selectedItems, tableRef, @@ -106,7 +106,6 @@ export const OpenTimeline = React.memo( setImportDataModalToggle(false); } }, [setImportDataModalToggle]); - const handleComplete = useCallback(() => { if (setImportDataModalToggle != null) { setImportDataModalToggle(false); @@ -116,14 +115,6 @@ export const OpenTimeline = React.memo( } }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); - const actionTimelineToShow = useMemo( - () => - onDeleteSelected != null && deleteTimelines != null - ? ['delete', 'duplicate', 'export', 'selectable'] - : ['duplicate', 'export', 'selectable'], - [onDeleteSelected, deleteTimelines] - ); - return ( <> ( + onDeleteSelected != null && deleteTimelines != null + ? ['delete', 'duplicate', 'export', 'selectable'] + : ['duplicate', 'export', 'selectable'], + [onDeleteSelected, deleteTimelines] + )} data-test-subj="timelines-table" deleteTimelines={deleteTimelines} defaultPageSize={defaultPageSize} diff --git a/x-pack/plugins/siem/public/components/open_timeline/types.ts b/x-pack/plugins/siem/public/components/open_timeline/types.ts index bb7e5085180470..4d953f6fa775e1 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/types.ts @@ -8,7 +8,7 @@ import { SetStateAction, Dispatch } from 'react'; import { AllTimelinesVariables } from '../../containers/timeline/all'; import { TimelineModel } from '../../store/timeline/model'; import { NoteResult } from '../../graphql/types'; -import { TimelineTypeLiteral } from '../../../common/types/timeline'; +import { TimelineType, TimelineTypeLiteral } from '../../../common/types/timeline'; /** The users who added a timeline to favorites */ export interface FavoriteTimelineResult { @@ -48,7 +48,7 @@ export interface OpenTimelineResult { savedObjectId?: string | null; title?: string | null; templateTimelineId?: string | null; - type?: TimelineTypeLiteral; + type?: TimelineType.template | TimelineType.default; updated?: number | null; updatedBy?: string | null; } diff --git a/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap index e209ca3b069d2b..e15ce0ae5f5437 100644 --- a/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -38,18 +38,18 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] = data-test-subj="stat-item" >

      @@ -258,18 +258,18 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] = data-test-subj="stat-item" >

      @@ -548,18 +548,18 @@ exports[`Stat Items Component rendering kpis with charts it renders the default data-test-subj="stat-item" >

      1,714 @@ -734,10 +734,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default key="stat-items-field-uniqueDestinationIps" >

      2,359 @@ -815,10 +815,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default >

      - + {content} - + {endpointRefUrl != null && canYouAddEndpointLogo(moduleName, endpointRefUrl) && ( ( ({ columns, + createTimeline, dataProviders, eventType, end, @@ -141,13 +143,19 @@ const StatefulTimelineComponent = React.memo( [columns, id] ); + useEffect(() => { + if (createTimeline != null) { + createTimeline({ id, columns: defaultHeaders, show: false }); + } + }, []); + return ( {({ indexPattern, browserFields }) => ( ( onDataProviderRemoved={onDataProviderRemoved} onToggleDataProviderEnabled={onToggleDataProviderEnabled} onToggleDataProviderExcluded={onToggleDataProviderExcluded} - show={show} + show={show!} showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg} - sort={sort} + sort={sort!} start={start} toggleColumn={toggleColumn} usersViewing={usersViewing} @@ -247,6 +255,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = { addProvider: timelineActions.addProvider, + createTimeline: timelineActions.createTimeline, onDataProviderEdited: timelineActions.dataProviderEdited, removeColumn: timelineActions.removeColumn, removeProvider: timelineActions.removeProvider, diff --git a/x-pack/plugins/siem/public/components/top_n/index.test.tsx b/x-pack/plugins/siem/public/components/top_n/index.test.tsx index 08d1b4d5ffd4cd..9325dcf499b2b4 100644 --- a/x-pack/plugins/siem/public/components/top_n/index.test.tsx +++ b/x-pack/plugins/siem/public/components/top_n/index.test.tsx @@ -18,7 +18,6 @@ import { Props } from './top_n'; import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; jest.mock('../../lib/kibana'); -jest.mock('../../store/timeline/actions'); const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; diff --git a/x-pack/plugins/siem/public/components/with_hover_actions/index.tsx b/x-pack/plugins/siem/public/components/with_hover_actions/index.tsx index 86a9acc486b6de..8679dae4483321 100644 --- a/x-pack/plugins/siem/public/components/with_hover_actions/index.tsx +++ b/x-pack/plugins/siem/public/components/with_hover_actions/index.tsx @@ -6,8 +6,17 @@ import { EuiPopover } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; + import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const WithHoverActionsPopover = (styled(EuiPopover as any)` + .euiPopover__anchor { + width: 100%; + } +` as unknown) as typeof EuiPopover; + interface Props { /** * Always show the hover menu contents (default: false) @@ -59,7 +68,7 @@ export const WithHoverActions = React.memo( const popover = useMemo(() => { return ( - ( panelPaddingSize={!alwaysShow ? 's' : 'none'} > {isOpen ? hoverContent : null} - + ); }, [content, onMouseLeave, isOpen, alwaysShow, hoverContent]); diff --git a/x-pack/plugins/siem/public/containers/case/translations.ts b/x-pack/plugins/siem/public/containers/case/translations.ts index d5ea287fd2cdd0..79edcc56b0362f 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 0848d12c8d308b..1603beddbb1dc8 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 a2290f946be9bb..5afe06a9828e5f 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 72609e15d1ec4f..96fa824c1cadd0 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 3d0836cdc8adfe..7f4c4a42761721 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/containers/timeline/api.ts b/x-pack/plugins/siem/public/containers/timeline/api.ts index 3a9cd2b0dc375c..023e2e6af9f88e 100644 --- a/x-pack/plugins/siem/public/containers/timeline/api.ts +++ b/x-pack/plugins/siem/public/containers/timeline/api.ts @@ -13,13 +13,7 @@ import { TimelineResponse, TimelineResponseType, } from '../../../common/types/timeline'; -import { - TIMELINE_URL, - TIMELINE_DRAFT_CLEAN_URL, - TIMELINE_DRAFT_URL, - TIMELINE_IMPORT_URL, - TIMELINE_EXPORT_URL, -} from '../../../common/constants'; +import { TIMELINE_URL, TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../common/constants'; import { KibanaServices } from '../../lib/kibana'; import { ExportSelectedData } from '../../components/generic_downloader'; @@ -119,15 +113,3 @@ export const exportSelectedTimeline: ExportSelectedData = async ({ return response.body!; }; - -export const getDraftTimeline = async (): Promise => { - const response = await KibanaServices.get().http.get(TIMELINE_DRAFT_URL); - - return decodeTimelineResponse(response); -}; - -export const cleanDraftTimeline = async (): Promise => { - const response = await KibanaServices.get().http.post(TIMELINE_DRAFT_CLEAN_URL); - - return decodeTimelineResponse(response); -}; diff --git a/x-pack/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json index d6f34255bcc1c5..c2b21957a90565 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -10377,7 +10377,6 @@ "isDeprecated": false, "deprecationReason": null }, - { "name": "draft", "description": "", "isDeprecated": false, "deprecationReason": null }, { "name": "template", "description": "", diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index 3436ee84a2f30f..dd4e967b185b9b 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -342,7 +342,6 @@ export enum TlsFields { export enum TimelineType { default = 'default', - draft = 'draft', template = 'template', } diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx index 9c3d1c90e67d7a..337ca2e3c918e0 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx @@ -109,3 +109,6 @@ export const JiraConnectorFlyout = withConnectorFlyout({ configKeys: ['projectKey'], connectorActionTypeId: '.jira', }); + +// eslint-disable-next-line import/no-default-export +export { JiraConnectorFlyout as default }; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx index ada9608e37c983..049ccb7cf17b7e 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { lazy } from 'react'; import { ValidationResult, // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -13,7 +14,6 @@ import { connector } from './config'; import { createActionType } from '../utils'; import logo from './logo.svg'; import { JiraActionConnector } from './types'; -import { JiraConnectorFlyout } from './flyout'; import * as i18n from './translations'; interface Errors { @@ -50,5 +50,5 @@ export const getActionType = createActionType({ selectMessage: i18n.JIRA_DESC, actionTypeTitle: connector.name, validateConnector, - actionConnectorFields: JiraConnectorFlyout, + actionConnectorFields: lazy(() => import('./flyout')), }); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx index 5d5d08dacf90c9..2783e988a64052 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx @@ -82,3 +82,6 @@ export const ServiceNowConnectorFlyout = withConnectorFlyout import('./flyout')), }); diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index ffb013c347e59e..3d3692c9806e48 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -8,6 +8,7 @@ /* eslint-disable @kbn/eslint/no-restricted-paths */ import { ActionType } from '../../../../triggers_actions_ui/public'; +import { IErrorObject } from '../../../../triggers_actions_ui/public/types'; import { ExternalIncidentServiceConfiguration } from '../../../../actions/server/builtin_action_types/case/types'; import { ActionType as ThirdPartySupportedActions, CaseField } from '../../../../case/common/api'; @@ -42,7 +43,7 @@ export interface ActionConnectorValidationErrors { export type Optional = Omit & Partial; export interface ConnectorFlyoutFormProps { - errors: { [key: string]: string[] }; + errors: IErrorObject; action: T; onChangeSecret: (key: string, value: string) => void; onBlurSecret: (key: string) => void; diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts index 169b4758876e8c..cc1608a05e2ce8 100644 --- a/x-pack/plugins/siem/public/lib/connectors/utils.ts +++ b/x-pack/plugins/siem/public/lib/connectors/utils.ts @@ -7,7 +7,6 @@ import { ActionTypeModel, ValidationResult, - ActionParamsProps, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../triggers_actions_ui/public/types'; @@ -31,7 +30,7 @@ export const createActionType = ({ validateConnector, validateParams = connectorParamsValidator, actionConnectorFields, - actionParamsFields = ConnectorParamsFields, + actionParamsFields = null, }: Optional) => (): ActionTypeModel => { return { id, @@ -59,15 +58,6 @@ export const createActionType = ({ }; }; -const ConnectorParamsFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - errors, -}) => { - return null; -}; - const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { return { errors: {} }; }; diff --git a/x-pack/plugins/siem/public/mock/timeline_results.ts b/x-pack/plugins/siem/public/mock/timeline_results.ts index 86acb2981be062..1af0f533a7ca95 100644 --- a/x-pack/plugins/siem/public/mock/timeline_results.ts +++ b/x-pack/plugins/siem/public/mock/timeline_results.ts @@ -2243,7 +2243,7 @@ export const defaultTimelineProps: CreateTimelineProps = { showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc }, title: '', - timelineType: TimelineType.draft, + timelineType: TimelineType.default, templateTimelineVersion: null, templateTimelineId: null, version: null, 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 718eb95767f2e3..f48d9a68ffaf0a 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 8b6ee76dd783db..24fbd59b3282b2 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 216180eb2cf0a2..4acdaef6ca51f8 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 7ce9d7b8533e43..a6e6b19a071ce5 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 14039dc2cbc304..fed8ec8edbe8b5 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 1e4fd92058e8d2..0613c40d1181d6 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 2a36fcf8a6bc4b..bdd6ae98a5d01a 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 736974545a1df8..b9a94f83fded1a 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/siem/public/pages/detection_engine/components/signals/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx index ac31d12186d349..b66a9fc881045b 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SignalsTableComponent, PropsFromRedux } from './index'; +import { SignalsTableComponent } from './index'; describe('SignalsTableComponent', () => { it('renders correctly', () => { @@ -28,15 +28,13 @@ describe('SignalsTableComponent', () => { loadingEventIds={[]} selectedEventIds={{}} isSelectAllChecked={false} - clearSelected={(jest.fn() as unknown) as PropsFromRedux['clearSelected']} - setEventsLoading={(jest.fn() as unknown) as PropsFromRedux['setEventsLoading']} - clearEventsLoading={(jest.fn() as unknown) as PropsFromRedux['clearEventsLoading']} - setEventsDeleted={(jest.fn() as unknown) as PropsFromRedux['setEventsDeleted']} - clearEventsDeleted={(jest.fn() as unknown) as PropsFromRedux['clearEventsDeleted']} - createTimeline={(jest.fn() as unknown) as PropsFromRedux['createTimeline']} - updateTimelineIsLoading={ - (jest.fn() as unknown) as PropsFromRedux['updateTimelineIsLoading'] - } + clearSelected={jest.fn()} + setEventsLoading={jest.fn()} + clearEventsLoading={jest.fn()} + setEventsDeleted={jest.fn()} + clearEventsDeleted={jest.fn()} + updateTimelineIsLoading={jest.fn()} + updateTimeline={jest.fn()} /> ); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index f1c7b50e497238..5442c8c19b5a70 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -8,6 +8,7 @@ import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; import { Filter, esQuery } from '../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; @@ -44,6 +45,7 @@ import { UpdateSignalsStatusCallback, UpdateSignalsStatusProps, } from './types'; +import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers'; export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; @@ -64,7 +66,6 @@ export const SignalsTableComponent: React.FC = ({ clearEventsDeleted, clearEventsLoading, clearSelected, - createTimeline, defaultFilters, from, globalFilters, @@ -78,6 +79,7 @@ export const SignalsTableComponent: React.FC = ({ setEventsLoading, signalsIndex, to, + updateTimeline, updateTimelineIsLoading, }) => { const [selectAll, setSelectAll] = useState(false); @@ -112,13 +114,22 @@ export const SignalsTableComponent: React.FC = ({ // Callback for creating a new timeline -- utilized by row/batch actions const createTimelineCallback = useCallback( - ({ timeline }: CreateTimelineProps) => - createTimeline({ + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimeline({ + duplicate: true, + from: fromTimeline, id: 'timeline-1', - ...timeline, - show: true, - }), - [createTimeline] + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + ruleNote, + })(); + }, + [updateTimeline, updateTimelineIsLoading] ); const setEventsLoadingCallback = useCallback( @@ -322,18 +333,37 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -const mapDispatchToProps = { - clearSelected: timelineActions.clearSelected, - setEventsLoading: timelineActions.setEventsLoading, - clearEventsLoading: timelineActions.clearEventsLoading, - setEventsDeleted: timelineActions.setEventsDeleted, - clearEventsDeleted: timelineActions.clearEventsDeleted, - updateTimelineIsLoading: timelineActions.updateIsLoading, - createTimeline: timelineActions.createTimeline, -}; +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + setEventsLoading: ({ + id, + eventIds, + isLoading, + }: { + id: string; + eventIds: string[]; + isLoading: boolean; + }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + setEventsDeleted: ({ + id, + eventIds, + isDeleted, + }: { + id: string; + eventIds: string[]; + isDeleted: boolean; + }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(timelineActions.updateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); const connector = connect(makeMapStateToProps, mapDispatchToProps); -export type PropsFromRedux = ConnectedProps; +type PropsFromRedux = ConnectedProps; export const SignalsTable = connector(React.memo(SignalsTableComponent)); diff --git a/x-pack/plugins/siem/public/pages/timelines/index.tsx b/x-pack/plugins/siem/public/pages/timelines/index.tsx index 992174f337f1e2..343be5cbe3839e 100644 --- a/x-pack/plugins/siem/public/pages/timelines/index.tsx +++ b/x-pack/plugins/siem/public/pages/timelines/index.tsx @@ -23,7 +23,7 @@ import { appendSearch } from '../../components/link_to/helpers'; const timelinesPagePath = `/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`; const timelinesDefaultPath = `/${SiemPageName.timelines}/${TimelineType.default}`; -const TabNameMappedToI18nKey: Record = { +const TabNameMappedToI18nKey: Record = { [TimelineType.default]: TAB_TIMELINES, [TimelineType.template]: TAB_TEMPLATES, }; diff --git a/x-pack/plugins/siem/public/store/epic.ts b/x-pack/plugins/siem/public/store/epic.ts index d7c15ac88e2a93..336960588f48c6 100644 --- a/x-pack/plugins/siem/public/store/epic.ts +++ b/x-pack/plugins/siem/public/store/epic.ts @@ -9,13 +9,11 @@ import { createTimelineEpic } from './timeline/epic'; import { createTimelineFavoriteEpic } from './timeline/epic_favorite'; import { createTimelineNoteEpic } from './timeline/epic_note'; import { createTimelinePinnedEventEpic } from './timeline/epic_pinned_event'; -import { createDraftTimelineEpic } from './timeline/epic_draft_timeline'; export const createRootEpic = () => combineEpics( createTimelineEpic(), createTimelineFavoriteEpic(), createTimelineNoteEpic(), - createTimelinePinnedEventEpic(), - createDraftTimelineEpic() + createTimelinePinnedEventEpic() ); diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts index 205c96e57f8830..12155decf40d44 100644 --- a/x-pack/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/plugins/siem/public/store/timeline/actions.ts @@ -69,10 +69,6 @@ export const createTimeline = actionCreator<{ showRowRenderers?: boolean; }>('CREATE_TIMELINE'); -export const getDraftTimeline = actionCreator<{ id: string }>('GET_DRAFT_TIMELINE'); - -export const cleanDraftTimeline = actionCreator<{ id: string }>('CLEAN_DRAFT_TIMELINE'); - export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); export const removeColumn = actionCreator<{ diff --git a/x-pack/plugins/siem/public/store/timeline/defaults.ts b/x-pack/plugins/siem/public/store/timeline/defaults.ts index f40b0078c499ff..9203720e2e28ce 100644 --- a/x-pack/plugins/siem/public/store/timeline/defaults.ts +++ b/x-pack/plugins/siem/public/store/timeline/defaults.ts @@ -35,7 +35,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick { timelineByIdSelector: (state: State) => TimelineById; @@ -150,13 +148,8 @@ export const createTimelineEpic = (): Epic< return true; } if (action.type === createTimeline.type && isItAtimelineAction(timelineId)) { - if (timelineObj.timelineType !== 'draft') { - myEpicTimelineId.setTimelineVersion(null); - myEpicTimelineId.setTimelineId(null); - } - return true; - } else if (action.type === getDraftTimeline.type && isItAtimelineAction(timelineId)) { - return true; + myEpicTimelineId.setTimelineId(null); + myEpicTimelineId.setTimelineVersion(null); } else if (action.type === addTimeline.type && isItAtimelineAction(timelineId)) { const addNewTimeline: TimelineModel = get('payload.timeline', action); myEpicTimelineId.setTimelineId(addNewTimeline.savedObjectId); @@ -214,10 +207,6 @@ export const createTimelineEpic = (): Epic< timeline$, allTimelineQuery$ ); - } else if (action.type === getDraftTimeline.type) { - return epicDraftTimeline(action, action$, timeline$, false); - } else if (action.type === createTimeline.type) { - return epicDraftTimeline(action, action$, timeline$, true); } else if (timelineActionsType.includes(action.type)) { return from( persistTimeline({ diff --git a/x-pack/plugins/siem/public/store/timeline/epic_draft_timeline.ts b/x-pack/plugins/siem/public/store/timeline/epic_draft_timeline.ts deleted file mode 100644 index 9e8d861ea7584d..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/epic_draft_timeline.ts +++ /dev/null @@ -1,110 +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 { of, Observable, from } from 'rxjs'; -import { get } from 'lodash/fp'; -import { filter, withLatestFrom, mergeMap, takeUntil, startWith } from 'rxjs/operators'; -import { Epic } from 'redux-observable'; -import { Action } from 'redux'; -import { - addTimeline, - createTimeline, - showCallOutUnauthorizedMsg, - endTimelineSaving, -} from './actions'; -import { getDraftTimeline, cleanDraftTimeline } from '../../containers/timeline/api'; -import { ActionTimeline, TimelineById } from './types'; -import { myEpicTimelineId } from './my_epic_timeline_id'; -import { addError } from '../app/actions'; -import { - formatTimelineResultToModel, - epicUpdateTimeline, -} from '../../components/open_timeline/helpers'; -import { getTimeRangeSettings } from '../../utils/default_date_settings'; -import { ResponseTimeline } from '../../graphql/types'; -import { timelineDefaults } from './defaults'; - -export const epicDraftTimeline = ( - action: ActionTimeline, - action$: Observable, - timeline$: Observable, - clean: boolean -) => - from(clean ? cleanDraftTimeline() : getDraftTimeline()).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimelines]) => { - const savedTimeline = recentTimelines[action.payload.id]; - const response: ResponseTimeline = get('data.persistTimeline', result); - const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; - - const { timeline: timelineModel, notes } = formatTimelineResultToModel( - { - savedObjectId: response.timeline.savedObjectId, - version: response.timeline.version, - timelineType: response.timeline.timelineType, - }, - false - ); - const { from: settingsFrom, to: settingsTo } = getTimeRangeSettings(); - - return [ - ...callOutMsg, - ...epicUpdateTimeline({ - duplicate: false, - from: savedTimeline?.dateRange.start ?? settingsFrom, - id: 'timeline-1', - notes, - timeline: { - ...savedTimeline, - ...timelineModel, - ...action.payload, - id: timelineModel.savedObjectId!, - savedObjectId: timelineModel.savedObjectId!, - version: timelineModel.version, - // @ts-ignore - show: action.payload.show ?? savedTimeline.show, - }, - to: savedTimeline?.dateRange.end ?? settingsTo, - }), - endTimelineSaving({ - id: action.payload.id, - }), - ]; - }), - takeUntil( - action$.pipe( - withLatestFrom(timeline$), - filter(([checkAction, updatedTimeline]) => { - if (checkAction.type === addError.type) { - return true; - } - if (checkAction.type === addTimeline.type) { - return true; - } - if ( - checkAction.type === endTimelineSaving.type && - updatedTimeline[get('payload.id', checkAction)].savedObjectId != null - ) { - myEpicTimelineId.setTimelineId( - updatedTimeline[get('payload.id', checkAction)].savedObjectId - ); - myEpicTimelineId.setTimelineVersion( - updatedTimeline[get('payload.id', checkAction)].version - ); - return true; - } - return false; - }) - ) - ) - ); - -export const createDraftTimelineEpic = (): Epic => () => - of(createTimeline({ id: 'timeline-1', columns: timelineDefaults.columns })).pipe( - startWith( - addTimeline({ id: 'timeline-1', timeline: { id: 'timeline-1', ...timelineDefaults } }) - ) - ); diff --git a/x-pack/plugins/siem/public/store/timeline/helpers.ts b/x-pack/plugins/siem/public/store/timeline/helpers.ts index 7b8d49119f3230..adab029c11150d 100644 --- a/x-pack/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/plugins/siem/public/store/timeline/helpers.ts @@ -125,7 +125,7 @@ export const addTimelineToStore = ({ ...timelineById, [id]: { ...timeline, - isLoading: timelineById[id]?.isLoading ?? timeline.isSaving, + isLoading: timelineById[id].isLoading, }, }); @@ -147,10 +147,11 @@ interface AddNewTimelineParams { sort?: Sort; showCheckboxes?: boolean; showRowRenderers?: boolean; + timelineById: TimelineById; } /** Adds a new `Timeline` to the provided collection of `TimelineById` */ -export const getNewTimeline = ({ +export const addNewTimeline = ({ columns, dataProviders = [], dateRange = { start: 0, end: 0 }, @@ -162,19 +163,27 @@ export const getNewTimeline = ({ show = false, showCheckboxes = false, showRowRenderers = true, -}: AddNewTimelineParams): TimelineModel => ({ - id, - ...timelineDefaults, - columns, - dataProviders, - dateRange, - filters, - itemsPerPage, - kqlQuery, - sort, - show, - showCheckboxes, - showRowRenderers, + timelineById, +}: AddNewTimelineParams): TimelineById => ({ + ...timelineById, + [id]: { + id, + ...timelineDefaults, + columns, + dataProviders, + dateRange, + filters, + itemsPerPage, + kqlQuery, + sort, + show, + savedObjectId: null, + version: null, + isSaving: false, + isLoading: false, + showCheckboxes, + showRowRenderers, + }, }); interface PinTimelineEventParams { diff --git a/x-pack/plugins/siem/public/store/timeline/model.ts b/x-pack/plugins/siem/public/store/timeline/model.ts index b01c7cef1c6d2a..54e19812634ac2 100644 --- a/x-pack/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/plugins/siem/public/store/timeline/model.ts @@ -152,7 +152,6 @@ export type SubsetTimelineModel = Readonly< | 'isLoading' | 'savedObjectId' | 'version' - | 'timelineType' > >; diff --git a/x-pack/plugins/siem/public/store/timeline/reducer.test.ts b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts index a2a74b24223700..42c6d6ecb0e51b 100644 --- a/x-pack/plugins/siem/public/store/timeline/reducer.test.ts +++ b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts @@ -23,10 +23,10 @@ import { Direction } from '../../graphql/types'; import { defaultHeaders } from '../../mock'; import { + addNewTimeline, addTimelineProvider, addTimelineToStore, applyDeltaToTimelineColumnWidth, - getNewTimeline, removeTimelineColumn, removeTimelineProvider, updateTimelineColumns, @@ -132,32 +132,41 @@ describe('Timeline', () => { }); }); - describe('#getNewTimeline', () => { + describe('#addNewTimeline', () => { test('should return a new reference and not the same reference', () => { - const update = getNewTimeline({ + const update = addNewTimeline({ id: 'bar', columns: defaultHeaders, + timelineById: timelineByIdMock, }); - expect(update).not.toBe(timelineByIdMock.foo); + expect(update).not.toBe(timelineByIdMock); }); - test('should return a new timeline', () => { - const update = getNewTimeline({ + test('should add a new timeline', () => { + const update = addNewTimeline({ id: 'bar', columns: timelineDefaults.columns, + timelineById: timelineByIdMock, + }); + expect(update).toEqual({ + foo: timelineByIdMock.foo, + bar: set('id', 'bar', timelineDefaults), }); - expect(update).toEqual(set('id', 'bar', timelineDefaults)); }); test('should add the specified columns to the timeline', () => { const barWithEmptyColumns = set('id', 'bar', timelineDefaults); const barWithPopulatedColumns = set('columns', defaultHeaders, barWithEmptyColumns); - const update = getNewTimeline({ + const update = addNewTimeline({ id: 'bar', columns: defaultHeaders, + timelineById: timelineByIdMock, + }); + expect(update).toEqual({ + foo: timelineByIdMock.foo, + bar: barWithPopulatedColumns, }); - expect(update).toEqual(barWithPopulatedColumns); }); }); diff --git a/x-pack/plugins/siem/public/store/timeline/reducer.ts b/x-pack/plugins/siem/public/store/timeline/reducer.ts index b6b3a6deb1e5b8..e99644daf50d86 100644 --- a/x-pack/plugins/siem/public/store/timeline/reducer.ts +++ b/x-pack/plugins/siem/public/store/timeline/reducer.ts @@ -14,6 +14,7 @@ import { applyDeltaToColumnWidth, applyDeltaToWidth, applyKqlFilterQuery, + createTimeline, dataProviderEdited, endTimelineSaving, pinEvent, @@ -55,6 +56,7 @@ import { updateEventType, } from './actions'; import { + addNewTimeline, addTimelineHistory, addTimelineNote, addTimelineNoteToEvent, @@ -112,6 +114,41 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: addTimelineToStore({ id, timeline, timelineById: state.timelineById }), })) + .case( + createTimeline, + ( + state, + { + id, + dataProviders, + dateRange, + show, + columns, + itemsPerPage, + kqlQuery, + sort, + showCheckboxes, + showRowRenderers, + filters, + } + ) => ({ + ...state, + timelineById: addNewTimeline({ + columns, + dataProviders, + dateRange, + filters, + id, + itemsPerPage, + kqlQuery, + sort, + show, + showCheckboxes, + showRowRenderers, + timelineById: state.timelineById, + }), + }) + ) .case(upsertColumn, (state, { column, id, index }) => ({ ...state, timelineById: upsertTimelineColumn({ column, id, index, timelineById: state.timelineById }), diff --git a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts index 2432af9a379a1a..a1c13fd21a88e9 100644 --- a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -127,7 +127,6 @@ export const timelineSchema = gql` enum TimelineType { default - draft template } diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index 5313f4b9df268e..d74086357edbeb 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -344,7 +344,6 @@ export enum TlsFields { export enum TimelineType { default = 'default', - draft = 'draft', template = 'template', } diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts index 80594ca74a3535..30362392898d13 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts @@ -7,10 +7,18 @@ import { getSignalsTemplate } from './get_signals_template'; describe('get_signals_template', () => { - test('it should set the lifecycle name and the rollover alias to be the name of the index passed in', () => { + test('it should set the lifecycle "name" and "rollover_alias" to be the name of the index passed in', () => { const template = getSignalsTemplate('test-index'); expect(template.settings).toEqual({ - index: { lifecycle: { name: 'test-index', rollover_alias: 'test-index' } }, + index: { + lifecycle: { + name: 'test-index', + rollover_alias: 'test-index', + }, + }, + mapping: { + total_fields: { limit: 10000 }, + }, }); }); @@ -28,4 +36,9 @@ describe('get_signals_template', () => { const template = getSignalsTemplate('test-index'); expect(typeof template.mappings.properties.signal).toEqual('object'); }); + + test('it should have a "total_fields" section that is at least 10k in size', () => { + const template = getSignalsTemplate('test-index'); + expect(template.settings.mapping.total_fields.limit).toBeGreaterThanOrEqual(10000); + }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts index c6580f0bdda427..01d7182e253cec 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -17,6 +17,11 @@ export const getSignalsTemplate = (index: string) => { rollover_alias: index, }, }, + mapping: { + total_fields: { + limit: 10000, + }, + }, }, index_patterns: [`${index}-*`], mappings: ecsMapping.mappings, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index e7db2282258804..91685a68a60ae9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -122,20 +122,11 @@ describe('import_rules_route', () => { clients.siemClient.getSignalsIndex.mockReturnValue('mockSignalsIndex'); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); const response = await server.inject(request, context); - expect(response.status).toEqual(200); + expect(response.status).toEqual(400); expect(response.body).toEqual({ - errors: [ - { - error: { - message: - 'To create a rule, the index must exist first. Index mockSignalsIndex does not exist', - status_code: 409, - }, - rule_id: 'rule-1', - }, - ], - success: false, - success_count: 0, + message: + 'To create a rule, the index must exist first. Index mockSignalsIndex does not exist', + status_code: 400, }); }); @@ -145,19 +136,10 @@ describe('import_rules_route', () => { }); const response = await server.inject(request, context); - expect(response.status).toEqual(200); + expect(response.status).toEqual(500); expect(response.body).toEqual({ - errors: [ - { - error: { - message: 'Test error', - status_code: 400, - }, - rule_id: 'rule-1', - }, - ], - success: false, - success_count: 0, + message: 'Test error', + status_code: 500, }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 4d86f0bec6502a..9ba083ae48086e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -75,6 +75,14 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { body: `Invalid file extension ${fileExtension}`, }); } + const signalsIndex = siemClient.getSignalsIndex(); + const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, signalsIndex); + if (!indexExists) { + return siemResponse.error({ + statusCode: 400, + body: `To create a rule, the index must exist first. Index ${signalsIndex} does not exist`, + }); + } const objectLimit = config.maxRuleImportExportSize; const readStream = createRulesStreamFromNdJson(objectLimit); @@ -94,166 +102,150 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { const batchParseObjects = chunkParseObjects.shift() ?? []; const newImportRuleResponse = await Promise.all( batchParseObjects.reduce>>((accum, parsedRule) => { - const importsWorkerPromise = new Promise( - async (resolve, reject) => { - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedRule.message, - }) - ); - return null; - } - const { - anomaly_threshold: anomalyThreshold, - description, - enabled, - false_positives: falsePositives, - from, - immutable, - query, - language, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - note, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - exceptions_list, - } = parsedRule; + const importsWorkerPromise = new Promise(async resolve => { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + statusCode: 400, + message: parsedRule.message, + }) + ); + return null; + } + const { + anomaly_threshold: anomalyThreshold, + description, + enabled, + false_positives: falsePositives, + from, + immutable, + query, + language, + machine_learning_job_id: machineLearningJobId, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + note, + timeline_id: timelineId, + timeline_title: timelineTitle, + version, + exceptions_list, + } = parsedRule; - try { - validateLicenseForRuleType({ - license: context.licensing.license, - ruleType: type, - }); + try { + validateLicenseForRuleType({ + license: context.licensing.license, + ruleType: type, + }); - const signalsIndex = siemClient.getSignalsIndex(); - const indexExists = await getIndexExists( - clusterClient.callAsCurrentUser, - signalsIndex - ); - if (!indexExists) { - resolve( - createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `To create a rule, the index must exist first. Index ${signalsIndex} does not exist`, - }) - ); - } - const rule = await readRules({ alertsClient, ruleId }); - if (rule == null) { - await createRules({ - alertsClient, - anomalyThreshold, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - machineLearningJobId, - outputIndex: signalsIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - note, - version, - exceptions_list, - actions: [], // Actions are not imported nor exported at this time - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null && request.query.overwrite) { - await patchRules({ - alertsClient, - savedObjectsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - id: undefined, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - note, - version, - exceptions_list, - anomalyThreshold, - machineLearningJobId, - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null) { - resolve( - createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, - }) - ); - } - } catch (err) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule == null) { + await createRules({ + alertsClient, + anomalyThreshold, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + machineLearningJobId, + outputIndex: signalsIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + note, + version, + exceptions_list, + actions: [], // Actions are not imported nor exported at this time + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null && request.query.overwrite) { + await patchRules({ + alertsClient, + savedObjectsClient, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + id: undefined, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + note, + version, + exceptions_list, + anomalyThreshold, + machineLearningJobId, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null) { resolve( createBulkErrorObject({ ruleId, - statusCode: 400, - message: err.message, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, }) ); } + } catch (err) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + }) + ); } - ); + }); return [...accum, importsWorkerPromise]; }, []) ); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts index e50f82bb482a70..a7556d975da407 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts @@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../../src/core/server'; export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions'; -export const ruleActionsSavedObjectMappings = { +export const ruleActionsSavedObjectMappings: SavedObjectsType['mappings'] = { properties: { alertThrottle: { type: 'keyword', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts index 2dcc90240ad407..c01bc2497d677b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts @@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../../src/core/server'; export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status'; -export const ruleStatusSavedObjectMappings = { +export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = { properties: { alertId: { type: 'keyword', diff --git a/x-pack/plugins/siem/server/lib/machine_learning/index.ts b/x-pack/plugins/siem/server/lib/machine_learning/index.ts index eb09fdde3cce31..865a3cf51604d4 100644 --- a/x-pack/plugins/siem/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/siem/server/lib/machine_learning/index.ts @@ -6,7 +6,7 @@ import { SearchResponse, SearchParams } from 'elasticsearch'; -import { AnomalyRecordDoc as Anomaly } from '../../../../ml/common/types/anomalies'; +import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; export { Anomaly }; export type AnomalyResults = SearchResponse; diff --git a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts index 0f079571b868b5..de0bb3468e5248 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts @@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../src/core/server'; export const noteSavedObjectType = 'siem-ui-timeline-note'; -export const noteSavedObjectMappings = { +export const noteSavedObjectMappings: SavedObjectsType['mappings'] = { properties: { timelineId: { type: 'keyword', diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts index 1a4cd3fce575d6..d352764930d7f9 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts @@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../src/core/server'; export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event'; -export const pinnedEventSavedObjectMappings = { +export const pinnedEventSavedObjectMappings: SavedObjectsType['mappings'] = { properties: { timelineId: { type: 'keyword', diff --git a/x-pack/plugins/siem/server/lib/timeline/default_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/default_timeline.ts deleted file mode 100644 index 710a43df1221dc..00000000000000 --- a/x-pack/plugins/siem/server/lib/timeline/default_timeline.ts +++ /dev/null @@ -1,27 +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 { Direction } from '../../graphql/types'; -import { defaultHeaders } from './default_timeline_headers'; -import { SavedTimeline, TimelineType } from '../../../common/types/timeline'; - -export const draftTimelineDefaults: SavedTimeline = { - columns: defaultHeaders, - dataProviders: [], - description: '', - eventType: 'all', - filters: [], - kqlMode: 'filter', - timelineType: TimelineType.draft, - kqlQuery: { - filterQuery: null, - }, - title: '', - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, -}; diff --git a/x-pack/plugins/siem/server/lib/timeline/default_timeline_headers.ts b/x-pack/plugins/siem/server/lib/timeline/default_timeline_headers.ts deleted file mode 100644 index 77b64bf2754c05..00000000000000 --- a/x-pack/plugins/siem/server/lib/timeline/default_timeline_headers.ts +++ /dev/null @@ -1,44 +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 { SavedTimeline } from '../../../common/types/timeline'; - -export const defaultColumnHeaderType = 'not-filtered'; - -export const defaultHeaders: SavedTimeline['columns'] = [ - { - columnHeaderType: defaultColumnHeaderType, - id: '@timestamp', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'message', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'event.category', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'event.action', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'host.name', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'source.ip', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'destination.ip', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'user.name', - }, -]; diff --git a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index 3b06adf1b751e3..6de10bffb1325c 100644 --- a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -5,7 +5,6 @@ */ import uuid from 'uuid'; -import { isEmpty } from 'lodash/fp'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; import { SavedTimeline, TimelineType } from '../../../common/types/timeline'; @@ -39,14 +38,8 @@ export const pickSavedTimeline = ( savedTimeline.templateTimelineVersion = savedTimeline.templateTimelineVersion + 1; } } - } else if (savedTimeline.timelineType === TimelineType.draft) { - savedTimeline.timelineType = !isEmpty(savedTimeline.title) - ? TimelineType.default - : TimelineType.draft; - savedTimeline.templateTimelineId = null; - savedTimeline.templateTimelineVersion = null; } else { - savedTimeline.timelineType = savedTimeline.timelineType ?? TimelineType.default; + savedTimeline.timelineType = TimelineType.default; savedTimeline.templateTimelineId = null; savedTimeline.templateTimelineVersion = null; } diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts index e06e6c60ac65f4..a832c818d48b07 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -156,29 +156,6 @@ export const mockGetTemplateTimelineValue = { templateTimelineVersion: 1, }; -export const mockGetDraftTimelineValue = { - savedObjectId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', - version: 'WzEyMjUsMV0=', - columns: [], - dataProviders: [], - description: 'description', - eventType: 'all', - filters: [], - kqlMode: 'filter', - kqlQuery: { filterQuery: [] }, - title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, - savedQueryId: null, - sort: { columnId: '@timestamp', sortDirection: 'desc' }, - created: 1584828930463, - createdBy: 'angela', - updated: 1584868346013, - updatedBy: 'angela', - noteIds: [], - pinnedEventIds: ['k-gi8nABm-sIqJ_scOoS'], - timelineType: TimelineType.draft, -}; - export const mockParsedTimelineObject = omit( [ 'globalNotes', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts index 6f03f5888f3b44..2827c7a1c0ac6b 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -5,8 +5,6 @@ */ import * as rt from 'io-ts'; import { - TIMELINE_DRAFT_URL, - TIMELINE_DRAFT_CLEAN_URL, TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL, TIMELINE_URL, @@ -82,14 +80,6 @@ export const createTimelineWithoutTimelineId = { timelineType: TimelineType.default, }; -export const createDraftTimelineWithoutTimelineId = { - templateTimelineId: null, - timeline: inputTimeline, - timelineId: null, - version: null, - timelineType: TimelineType.draft, -}; - export const createTemplateTimelineWithoutTimelineId = { templateTimelineId: null, timeline: inputTemplateTimeline, @@ -103,11 +93,6 @@ export const createTimelineWithTimelineId = { timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', }; -export const createDraftTimelineWithTimelineId = { - ...createDraftTimelineWithoutTimelineId, - timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', -}; - export const createTemplateTimelineWithTimelineId = { ...createTemplateTimelineWithoutTimelineId, timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', @@ -154,18 +139,6 @@ export const getImportTimelinesRequestEnableOverwrite = (filename?: string) => }, }); -export const getDraftTimelinesRequest = () => - requestMock.create({ - method: 'get', - path: TIMELINE_DRAFT_URL, - }); - -export const cleanDraftTimelinesRequest = () => - requestMock.create({ - method: 'post', - path: TIMELINE_DRAFT_CLEAN_URL, - }); - export const mockTimelinesSavedObjects = () => ({ saved_objects: [ { diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/draft_clean_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/draft_clean_timelines_route.test.ts deleted file mode 100644 index 2902b0d119b7f1..00000000000000 --- a/x-pack/plugins/siem/server/lib/timeline/routes/draft_clean_timelines_route.test.ts +++ /dev/null @@ -1,114 +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 { SecurityPluginSetup } from '../../../../../../plugins/security/server'; - -import { - serverMock, - requestContextMock, - createMockConfig, -} from '../../detection_engine/routes/__mocks__'; - -import { mockGetCurrentUser, mockGetDraftTimelineValue } from './__mocks__/import_timelines'; -import { - cleanDraftTimelinesRequest, - createTimelineWithTimelineId, -} from './__mocks__/request_responses'; - -describe('draft clean timelines', () => { - let server: ReturnType; - let securitySetup: SecurityPluginSetup; - let { context } = requestContextMock.createTools(); - let mockGetTimeline: jest.Mock; - let mockGetDraftTimeline: jest.Mock; - let mockPersistTimeline: jest.Mock; - let mockPersistPinnedEventOnTimeline: jest.Mock; - let mockPersistNote: jest.Mock; - let mockResetTimeline: jest.Mock; - - beforeEach(() => { - jest.resetModules(); - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); - - server = serverMock.create(); - context = requestContextMock.createTools().context; - - securitySetup = ({ - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown) as SecurityPluginSetup; - - mockGetTimeline = jest.fn(); - mockGetDraftTimeline = jest.fn(); - mockPersistTimeline = jest.fn(); - mockPersistPinnedEventOnTimeline = jest.fn(); - mockPersistNote = jest.fn(); - mockResetTimeline = jest.fn(); - - jest.doMock('../saved_object', () => ({ - getTimeline: mockGetTimeline, - getDraftTimeline: mockGetDraftTimeline, - resetTimeline: mockResetTimeline, - persistTimeline: mockPersistTimeline.mockReturnValue({ - code: 200, - timeline: createTimelineWithTimelineId, - }), - })); - - jest.doMock('../../pinned_event/saved_object', () => ({ - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, - })); - - jest.doMock('../../note/saved_object', () => ({ - persistNote: mockPersistNote, - })); - - const draftCleanTimelinesRoute = jest.requireActual('./draft_clean_timelines_route') - .draftCleanTimelinesRoute; - draftCleanTimelinesRoute(server.router, createMockConfig(), securitySetup); - }); - - test('should create new draft if none is available', async () => { - mockGetDraftTimeline.mockResolvedValue({ - timeline: [], - }); - - const response = await server.inject(cleanDraftTimelinesRequest(), context); - expect(mockPersistTimeline).toHaveBeenCalled(); - expect(response.status).toEqual(200); - expect(response.body).toEqual({ - data: { - persistTimeline: { - timeline: createTimelineWithTimelineId, - }, - }, - }); - }); - - test('should return clean existing draft if draft available ', async () => { - mockGetDraftTimeline.mockResolvedValue({ - timeline: [mockGetDraftTimelineValue], - }); - mockResetTimeline.mockResolvedValue({}); - mockGetTimeline.mockResolvedValue({ ...mockGetDraftTimelineValue }); - - const response = await server.inject(cleanDraftTimelinesRequest(), context); - expect(mockPersistTimeline).not.toHaveBeenCalled(); - expect(mockResetTimeline).toHaveBeenCalled(); - expect(mockGetTimeline).toHaveBeenCalled(); - expect(response.status).toEqual(200); - expect(response.body).toEqual({ - data: { - persistTimeline: { - timeline: mockGetDraftTimelineValue, - }, - }, - }); - }); -}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/draft_clean_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/draft_clean_timelines_route.ts deleted file mode 100644 index eb876adab34d03..00000000000000 --- a/x-pack/plugins/siem/server/lib/timeline/routes/draft_clean_timelines_route.ts +++ /dev/null @@ -1,86 +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 { IRouter } from '../../../../../../../src/core/server'; -import { ConfigType } from '../../..'; -import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; -import { TIMELINE_DRAFT_CLEAN_URL } from '../../../../common/constants'; -import { buildFrameworkRequest } from './utils/common'; -import { SetupPlugins } from '../../../plugin'; -import { getDraftTimeline, resetTimeline, getTimeline, persistTimeline } from '../saved_object'; -import { draftTimelineDefaults } from '../default_timeline'; - -export const draftCleanTimelinesRoute = ( - router: IRouter, - config: ConfigType, - security: SetupPlugins['security'] -) => { - router.post( - { - path: TIMELINE_DRAFT_CLEAN_URL, - validate: {}, - options: { - tags: ['access:siem'], - }, - }, - async (context, request, response) => { - const frameworkRequest = await buildFrameworkRequest(context, security, request); - const siemResponse = buildSiemResponse(response); - - try { - const { - timeline: [draftTimeline], - } = await getDraftTimeline(frameworkRequest); - - if (draftTimeline?.savedObjectId) { - await resetTimeline(frameworkRequest, [draftTimeline.savedObjectId]); - const cleanedDraftTimeline = await getTimeline( - frameworkRequest, - draftTimeline.savedObjectId - ); - - return response.ok({ - body: { - data: { - persistTimeline: { - timeline: cleanedDraftTimeline, - }, - }, - }, - }); - } - - const newTimelineResponse = await persistTimeline( - frameworkRequest, - null, - null, - draftTimelineDefaults - ); - - if (newTimelineResponse.code === 200) { - return response.ok({ - body: { - data: { - persistTimeline: { - timeline: newTimelineResponse.timeline, - }, - }, - }, - }); - } - - return response.ok({}); - } catch (err) { - const error = transformError(err); - - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } - ); -}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/draft_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/draft_timelines_route.test.ts deleted file mode 100644 index 1bea832790d8cc..00000000000000 --- a/x-pack/plugins/siem/server/lib/timeline/routes/draft_timelines_route.test.ts +++ /dev/null @@ -1,113 +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 { SecurityPluginSetup } from '../../../../../../plugins/security/server'; - -import { - serverMock, - requestContextMock, - createMockConfig, -} from '../../detection_engine/routes/__mocks__'; - -import { mockGetCurrentUser, mockGetDraftTimelineValue } from './__mocks__/import_timelines'; -import { - getDraftTimelinesRequest, - createTimelineWithTimelineId, -} from './__mocks__/request_responses'; - -describe('draft timelines', () => { - let server: ReturnType; - let securitySetup: SecurityPluginSetup; - let { context } = requestContextMock.createTools(); - let mockGetTimeline: jest.Mock; - let mockGetDraftTimeline: jest.Mock; - let mockPersistTimeline: jest.Mock; - let mockPersistPinnedEventOnTimeline: jest.Mock; - let mockPersistNote: jest.Mock; - - beforeEach(() => { - jest.resetModules(); - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); - - server = serverMock.create(); - context = requestContextMock.createTools().context; - - securitySetup = ({ - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown) as SecurityPluginSetup; - - mockGetTimeline = jest.fn(); - mockGetDraftTimeline = jest.fn(); - mockPersistTimeline = jest.fn(); - mockPersistPinnedEventOnTimeline = jest.fn(); - mockPersistNote = jest.fn(); - }); - - describe('Manipulate timeline', () => { - describe('Create a new timeline', () => { - beforeEach(async () => { - jest.doMock('../saved_object', () => ({ - getTimeline: mockGetTimeline, - getDraftTimeline: mockGetDraftTimeline, - persistTimeline: mockPersistTimeline.mockReturnValue({ - code: 200, - timeline: createTimelineWithTimelineId, - }), - })); - - jest.doMock('../../pinned_event/saved_object', () => ({ - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, - })); - - jest.doMock('../../note/saved_object', () => ({ - persistNote: mockPersistNote, - })); - - const draftTimelinesRoute = jest.requireActual('./draft_timelines_route') - .draftTimelinesRoute; - draftTimelinesRoute(server.router, createMockConfig(), securitySetup); - }); - - test('should create new draft if none is available', async () => { - mockGetDraftTimeline.mockResolvedValue({ - timeline: [], - }); - - const response = await server.inject(getDraftTimelinesRequest(), context); - expect(mockPersistTimeline).toHaveBeenCalled(); - expect(response.status).toEqual(200); - expect(response.body).toEqual({ - data: { - persistTimeline: { - timeline: createTimelineWithTimelineId, - }, - }, - }); - }); - - test('should return an existing draft if available', async () => { - mockGetDraftTimeline.mockResolvedValue({ - timeline: [mockGetDraftTimelineValue], - }); - - const response = await server.inject(getDraftTimelinesRequest(), context); - expect(mockPersistTimeline).not.toHaveBeenCalled(); - expect(response.status).toEqual(200); - expect(response.body).toEqual({ - data: { - persistTimeline: { - timeline: mockGetDraftTimelineValue, - }, - }, - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/draft_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/draft_timelines_route.ts deleted file mode 100644 index 14d7fb18017192..00000000000000 --- a/x-pack/plugins/siem/server/lib/timeline/routes/draft_timelines_route.ts +++ /dev/null @@ -1,80 +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 { IRouter } from '../../../../../../../src/core/server'; -import { ConfigType } from '../../..'; -import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; -import { TIMELINE_DRAFT_URL } from '../../../../common/constants'; -import { buildFrameworkRequest } from './utils/common'; -import { SetupPlugins } from '../../../plugin'; -import { getDraftTimeline, persistTimeline } from '../saved_object'; -import { draftTimelineDefaults } from '../default_timeline'; - -export const draftTimelinesRoute = ( - router: IRouter, - config: ConfigType, - security: SetupPlugins['security'] -) => { - router.get( - { - path: TIMELINE_DRAFT_URL, - validate: {}, - options: { - tags: ['access:siem'], - }, - }, - async (context, request, response) => { - const frameworkRequest = await buildFrameworkRequest(context, security, request); - const siemResponse = buildSiemResponse(response); - - try { - const { - timeline: [draftTimeline], - } = await getDraftTimeline(frameworkRequest); - - if (draftTimeline?.savedObjectId) { - return response.ok({ - body: { - data: { - persistTimeline: { - timeline: draftTimeline, - }, - }, - }, - }); - } - - const newTimelineResponse = await persistTimeline( - frameworkRequest, - null, - null, - draftTimelineDefaults - ); - - if (newTimelineResponse.code === 200) { - return response.ok({ - body: { - data: { - persistTimeline: { - timeline: newTimelineResponse.timeline, - }, - }, - }, - }); - } - - return response.ok({}); - } catch (err) { - const error = transformError(err); - - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } - ); -}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts index e74069fb748a56..e0eefbf811a565 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { set as _set } from 'lodash/fp'; + import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; import { IRouter } from '../../../../../../../src/core/server'; import { ConfigType } from '../../../config'; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index 2f5200c87137da..11f93a9c48bf60 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -12,7 +12,6 @@ import { createMockConfig, } from '../../detection_engine/routes/__mocks__'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { TimelineType } from '../../../../common/types/timeline'; import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; import { @@ -34,7 +33,6 @@ describe('import timelines', () => { let mockPersistTimeline: jest.Mock; let mockPersistPinnedEventOnTimeline: jest.Mock; let mockPersistNote: jest.Mock; - let mockGetTupleDuplicateErrorsAndUniqueTimeline: jest.Mock; const newTimelineSavedObjectId = '79deb4c0-6bc1-11ea-9999-f5341fb7a189'; const newTimelineVersion = '9999'; beforeEach(() => { @@ -58,7 +56,6 @@ describe('import timelines', () => { mockPersistTimeline = jest.fn(); mockPersistPinnedEventOnTimeline = jest.fn(); mockPersistNote = jest.fn(); - mockGetTupleDuplicateErrorsAndUniqueTimeline = jest.fn(); jest.doMock('../create_timelines_stream_from_ndjson', () => { return { @@ -76,9 +73,9 @@ describe('import timelines', () => { const originalModule = jest.requireActual('./utils/import_timelines'); return { ...originalModule, - getTupleDuplicateErrorsAndUniqueTimeline: mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue( - [mockDuplicateIdErrors, mockUniqueParsedObjects] - ), + getTupleDuplicateErrorsAndUniqueTimeline: jest + .fn() + .mockReturnValue([mockDuplicateIdErrors, mockUniqueParsedObjects]), }; }); }); @@ -136,25 +133,12 @@ describe('import timelines', () => { expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); }); - test('should Create a new timeline savedObject with given timeline', async () => { + test('should Create a new timeline savedObject witn given timeline', async () => { const mockRequest = getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3]).toEqual(mockParsedTimelineObject); }); - test('should Create a new timeline savedObject with given draft timeline', async () => { - mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([ - mockDuplicateIdErrors, - [{ ...mockUniqueParsedObjects[0], timelineType: TimelineType.draft }], - ]); - const mockRequest = getImportTimelinesRequest(); - await server.inject(mockRequest, context); - expect(mockPersistTimeline.mock.calls[0][3]).toEqual({ - ...mockParsedTimelineObject, - timelineType: TimelineType.default, - }); - }); - test('should Create new pinned events', async () => { const mockRequest = getImportTimelinesRequest(); await server.inject(mockRequest, context); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index bb63d1dce55541..4a79dada071711 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -152,13 +152,7 @@ export const importTimelinesRoute = ( // create timeline / template timeline newTimeline = await createTimelines( frameworkRequest, - { - ...parsedTimelineObject, - timelineType: - parsedTimelineObject.timelineType === TimelineType.draft - ? TimelineType.default - : parsedTimelineObject.timelineType, - }, + parsedTimelineObject, null, // timelineSavedObjectId null, // timelineVersion pinnedEventIds, diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts index 1e64e38ea5b0a9..ea9a5fab66805c 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts @@ -16,7 +16,6 @@ import { ExportedNotes, TimelineSavedObject, ExportTimelineNotFoundError, - TimelineType, } from '../../../../../common/types/timeline'; import { NoteSavedObject } from '../../../../../common/types/timeline/note'; import { PinnedEventSavedObject } from '../../../../../common/types/timeline/pinned_event'; @@ -180,10 +179,6 @@ const getTimelinesFromObjects = async ( ...acc, { ...myTimeline, - timelineType: - myTimeline.timelineType === TimelineType.draft - ? TimelineType.default - : myTimeline.timelineType, ...getGlobalEventNotesByTimelineId(timelineNotes), pinnedEventIds: getPinnedEventsIdsByTimelineId(timelinePinnedEventIds), }, diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index f95cd01b2b788c..6d022ab42fa7b1 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -25,7 +25,6 @@ import * as pinnedEvent from '../pinned_event/saved_object'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; import { pickSavedTimeline } from './pick_saved_timeline'; import { timelineSavedObjectType } from './saved_object_mappings'; -import { draftTimelineDefaults } from './default_timeline'; interface ResponseTimelines { timeline: TimelineSavedObject[]; @@ -104,7 +103,7 @@ const getTimelineTypeFilter = (timelineType: string | null) => { : /** Show me every timeline whose timelineType is not "template". * which includes timelineType === 'default' and * those timelineType doesn't exists */ - `not siem-ui-timeline.attributes.timelineType: ${TimelineType.template} and not siem-ui-timeline.attributes.timelineType: ${TimelineType.draft}`; + `not siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; }; export const getAllTimeline = async ( @@ -130,17 +129,6 @@ export const getAllTimeline = async ( return getAllSavedTimeline(request, options); }; -export const getDraftTimeline = async (request: FrameworkRequest): Promise => { - const options: SavedObjectsFindOptions = { - type: timelineSavedObjectType, - perPage: 1, - filter: `siem-ui-timeline.attributes.timelineType: ${TimelineType.draft}`, - sortField: 'created', - sortOrder: 'desc', - }; - return getAllSavedTimeline(request, options); -}; - export const persistFavorite = async ( request: FrameworkRequest, timelineId: string | null @@ -269,54 +257,6 @@ export const persistTimeline = async ( } }; -const updatePartialSavedTimeline = async ( - request: FrameworkRequest, - timelineId: string, - timeline: SavedTimeline -) => { - const savedObjectsClient = request.context.core.savedObjects.client; - const currentSavedTimeline = await savedObjectsClient.get( - timelineSavedObjectType, - timelineId - ); - - return savedObjectsClient.update( - timelineSavedObjectType, - timelineId, - pickSavedTimeline( - null, - { - ...timeline, - dateRange: currentSavedTimeline.attributes.dateRange, - }, - request.user - ) - ); -}; - -export const resetTimeline = async (request: FrameworkRequest, timelineIds: string[]) => { - if (!timelineIds.length) { - return Promise.reject(new Error('timelineIds is empty')); - } - - await Promise.all( - timelineIds.map(timelineId => - Promise.all([ - note.deleteNoteByTimelineId(request, timelineId), - pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), - ]) - ) - ); - - const response = await Promise.all( - timelineIds.map(timelineId => - updatePartialSavedTimeline(request, timelineId, draftTimelineDefaults) - ) - ); - - return response; -}; - export const deleteTimeline = async (request: FrameworkRequest, timelineIds: string[]) => { const savedObjectsClient = request.context.core.savedObjects.client; diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts index 1cab24d0879ff6..4d9ae19bfd6a2d 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../src/core/server'; export const timelineSavedObjectType = 'siem-ui-timeline'; -export const timelineSavedObjectMappings = { +export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = { properties: { columns: { properties: { diff --git a/x-pack/plugins/siem/server/routes/index.ts b/x-pack/plugins/siem/server/routes/index.ts index 4a7bc4a4aa02fe..ffad86a09cee72 100644 --- a/x-pack/plugins/siem/server/routes/index.ts +++ b/x-pack/plugins/siem/server/routes/index.ts @@ -32,8 +32,6 @@ import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_ro import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route'; import { createTimelinesRoute } from '../lib/timeline/routes/create_timelines_route'; import { updateTimelinesRoute } from '../lib/timeline/routes/update_timelines_route'; -import { draftTimelinesRoute } from '../lib/timeline/routes/draft_timelines_route'; -import { draftCleanTimelinesRoute } from '../lib/timeline/routes/draft_clean_timelines_route'; import { SetupPlugins } from '../plugin'; import { ConfigType } from '../config'; @@ -66,8 +64,6 @@ export const initRoutes = ( importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config); - draftTimelinesRoute(router, config, security); - draftCleanTimelinesRoute(router, config, security); findRulesStatusesRoute(router); diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 9d8106a1366d6d..e115e086f45b56 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -14,8 +14,8 @@ export const useRequest = jest.fn(() => ({ })); // just passing through the reimports -export { getErrorMessage } from '../../../ml/common/util/errors'; export { + getErrorMessage, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, @@ -27,5 +27,5 @@ export { SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, -} from '../../../ml/public/application/components/data_grid'; -export { INDEX_STATUS } from '../../../ml/public/application/data_frame_analytics/common'; + INDEX_STATUS, +} from '../../../ml/public'; diff --git a/x-pack/plugins/transform/public/app/common/aggregations.ts b/x-pack/plugins/transform/public/app/common/aggregations.ts index 038d68ff37d876..397a58006f1d1a 100644 --- a/x-pack/plugins/transform/public/app/common/aggregations.ts +++ b/x-pack/plugins/transform/public/app/common/aggregations.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { composeValidators, patternValidator } from '../../../../ml/common/util/validators'; +import { composeValidators, patternValidator } from '../../../../ml/public'; export type AggName = string; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index a794b7e7c21438..d3dae0a8c8b63c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiFlyout, @@ -18,11 +19,10 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiOverlayMask, + EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; - import { getErrorMessage } from '../../../../../shared_imports'; import { @@ -30,8 +30,7 @@ import { TransformPivotConfig, REFRESH_TRANSFORM_LIST_STATE, } from '../../../../common'; -import { ToastNotificationText } from '../../../../components'; -import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; +import { useToastNotifications } from '../../../../app_dependencies'; import { useApi } from '../../../../hooks/use_api'; @@ -48,13 +47,14 @@ interface EditTransformFlyoutProps { } export const EditTransformFlyout: FC = ({ closeFlyout, config }) => { - const { overlays } = useAppDependencies(); const api = useApi(); const toastNotifications = useToastNotifications(); const [state, dispatch] = useEditTransformFlyout(config); + const [errorMessage, setErrorMessage] = useState(undefined); async function submitFormHandler() { + setErrorMessage(undefined); const requestConfig = applyFormFieldsToTransformConfig(config, state.formFields); const transformId = config.id; @@ -69,12 +69,7 @@ export const EditTransformFlyout: FC = ({ closeFlyout, closeFlyout(); refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.transformList.editTransformGenericErrorMessage', { - defaultMessage: 'An error occurred calling the API endpoint to update transforms.', - }), - text: toMountPoint(), - }); + setErrorMessage(getErrorMessage(e)); } } @@ -97,6 +92,24 @@ export const EditTransformFlyout: FC = ({ closeFlyout, }> + {errorMessage !== undefined && ( + <> + + +

      {errorMessage}

      +
      + + )}
      diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index bcd8e53e3d1919..3737377de2d5ee 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -16,9 +16,8 @@ export { useRequest, } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; -export { getErrorMessage } from '../../ml/common/util/errors'; - export { + getErrorMessage, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, @@ -30,5 +29,5 @@ export { SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, -} from '../../ml/public/application/components/data_grid'; -export { INDEX_STATUS } from '../../ml/public/application/data_frame_analytics/common'; + INDEX_STATUS, +} from '../../ml/public'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 67eec939701c37..956dcb08e5fc11 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4209,6 +4209,7 @@ "xpack.apm.errorRateAlertTrigger.isAbove": "の下限は", "xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "エラーメッセージと原因", "xpack.apm.errorsTable.groupIdColumnLabel": "グループ ID", + "xpack.apm.errorsTable.groupIdColumnDescription": "スタックトレースのハッシュ。動的パラメーターによりエラーメッセージが異なる場合でも、同様のエラーをグループ化します。", "xpack.apm.errorsTable.latestOccurrenceColumnLabel": "最近のオカレンス", "xpack.apm.errorsTable.noErrorsLabel": "エラーが見つかりませんでした", "xpack.apm.errorsTable.occurrencesColumnLabel": "オカレンス", @@ -4314,7 +4315,6 @@ "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "現在 {serviceName} ({transactionType}) の実行中のジョブがあります。", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "既存のジョブを表示", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "ジョブが既に存在します", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription": "ここでは、{serviceName} 数列内の APM トランザクションの期間の異常スコアを計算する機械学習ジョブを作成できます。有効にすると、{transactionDurationGraphText} が予測バウンドを表示し、異常スコアが >=75 の場合グラフに注釈が追加されます。", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText": "トランザクション時間のグラフ", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel": "ジョブを作成", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "異常検知を有効にする", @@ -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": "選択項目を拡張", @@ -10055,7 +10053,6 @@ "xpack.ml.models.jobValidation.messages.jobIdInvalidMessage": "ジョブ ID が無効です。アルファベットの小文字 (a-z と 0-9)、ハイフンまたはアンダーラインが使用でき、最初と最後を英数字にする必要があります。", "xpack.ml.models.jobValidation.messages.jobIdValidHeading": "ジョブ ID のフォーマットは有効です。", "xpack.ml.models.jobValidation.messages.jobIdValidMessage": "アルファベットの小文字 (a-z と 0-9)、ハイフンまたはアンダーライン、最初と最後を英数字にし、{maxLength, plural, one {# 文字} other {# 文字}}以内にする必要があります。", - "xpack.ml.models.jobValidation.messages.mmlGreaterThanMaxMmlMessage": "モデルメモリー制限が、このクラスターに構成された最大モデルメモリー制限を超えています。", "xpack.ml.models.jobValidation.messages.mmlValueInvalidMessage": "{mml} はモデルメモリー制限の有効な値ではありません。この値は最低 1MB で、バイト (例: 10MB) で指定する必要があります。", "xpack.ml.models.jobValidation.messages.skippedExtendedTestsMessage": "ジョブの構成の基本要件が満たされていないため、他のチェックをスキップしました。", "xpack.ml.models.jobValidation.messages.successBucketSpanHeading": "バケットスパン", @@ -12202,7 +12199,6 @@ "xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "{reportObjectType}「{reportObjectTitle}」のレポートが作成されました", "xpack.reporting.registerFeature.reportingDescription": "ディスカバリ、可視化、ダッシュボードから生成されたレポートを管理します。", "xpack.reporting.registerFeature.reportingTitle": "レポート", - "xpack.reporting.screencapture.asyncTook": "{description} にかかった時間は {took}ms でした", "xpack.reporting.screencapture.couldntFinishRendering": "{count} 件のビジュアライゼーションのレンダリングが完了するのを待つ間にエラーが発生しました。「{configKey}」を増やす必要があるかもしれません。 {error}", "xpack.reporting.screencapture.couldntLoadKibana": "Kibana URL を開こうとするときにエラーが発生しました。「{configKey}」を増やす必要があるかもしれません。 {error}", "xpack.reporting.screencapture.injectCss": "Kibana CSS をレポート用に更新しようとしたときにエラーが発生しました。{error}", @@ -13282,8 +13278,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 486bb747a15e0f..cc42647f356bfb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4209,6 +4209,7 @@ "xpack.apm.errorRateAlertTrigger.errors": "错误", "xpack.apm.errorRateAlertTrigger.isAbove": "高于", "xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "错误消息和原因", + "xpack.apm.errorsTable.groupIdColumnDescription": "堆栈跟踪的哈希值。即使由于动态参数而导致错误消息不同,也将相似的错误归为一组。", "xpack.apm.errorsTable.groupIdColumnLabel": "组 ID", "xpack.apm.errorsTable.latestOccurrenceColumnLabel": "最新一次发生", "xpack.apm.errorsTable.noErrorsLabel": "未找到任何错误", @@ -4315,7 +4316,6 @@ "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "当前有 {serviceName}({transactionType})的作业正在运行。", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "查看现有作业", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "作业已存在", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription": "在这里可以创建 Machine Learning 作业以基于 {serviceName} 服务内 APM 事务的持续时间计算异常分数。启用后,一旦异常分数 >=75,{transactionDurationGraphText}将显示预期边界并标注图表。", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText": "事务持续时间图表", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel": "创建作业", "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "启用异常检测", @@ -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": "展开选择内容", @@ -10061,7 +10059,6 @@ "xpack.ml.models.jobValidation.messages.jobIdInvalidMessage": "作业 ID 无效.其可以包含小写字母数字(a-z 和 0-9)字符、连字符或下划线,且必须以字母数字字符开头和结尾。", "xpack.ml.models.jobValidation.messages.jobIdValidHeading": "作业 ID 格式有效", "xpack.ml.models.jobValidation.messages.jobIdValidMessage": "小写字母数字(a-z 和 0-9)字符、连字符或下划线,以字母数字字符开头和结尾,且长度不超过 {maxLength, plural, one {# 个字符} other {# 个字符}}。", - "xpack.ml.models.jobValidation.messages.mmlGreaterThanMaxMmlMessage": "模型内存限制大于为此集群配置的最大模型内存限制。", "xpack.ml.models.jobValidation.messages.mmlValueInvalidMessage": "{mml} 不是有效的模型内存限制值。该值需要至少 1MB,且应以字节为单位(例如 10MB)指定。", "xpack.ml.models.jobValidation.messages.skippedExtendedTestsMessage": "已跳过其他检查,因为未满足作业配置的基本要求。", "xpack.ml.models.jobValidation.messages.successBucketSpanHeading": "存储桶跨度", @@ -12209,7 +12206,6 @@ "xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "已为 {reportObjectType}“{reportObjectTitle}”创建报告", "xpack.reporting.registerFeature.reportingDescription": "管理您从 Discover、Visualize 和 Dashboard 生成的报告。", "xpack.reporting.registerFeature.reportingTitle": "报告", - "xpack.reporting.screencapture.asyncTook": "{description} 花费了 {took}ms", "xpack.reporting.screencapture.couldntFinishRendering": "尝试等候 {count} 个可视化完成渲染时发生错误。您可能需要增加“{configKey}”。{error}", "xpack.reporting.screencapture.couldntLoadKibana": "尝试打开 Kibana URL 时发生了错误。您可能需要增加“{configKey}”。{error}", "xpack.reporting.screencapture.injectCss": "尝试为 Reporting 更新 Kibana CSS 时发生错误。{error}", @@ -13289,8 +13285,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/README.md b/x-pack/plugins/triggers_actions_ui/README.md index ece1791c66e115..c5f02863ba8a19 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -985,8 +985,8 @@ Each action type should be defined as an `ActionTypeModel` object with the follo |selectMessage|Short description of action type responsibility, that will be displayed on the select card in UI.| |validateConnector|Validation function for action connector.| |validateParams|Validation function for action params.| -|actionConnectorFields|React functional component for building UI of current action type connector.| -|actionParamsFields|React functional component for building UI of current action type params. Displayed as a part of Create Alert flyout.| +|actionConnectorFields|A lazy loaded React component for building UI of current action type connector.| +|actionParamsFields|A lazy loaded React component for building UI of current action type params. Displayed as a part of Create Alert flyout.| ## Register action type model @@ -1082,8 +1082,8 @@ export function getActionType(): ActionTypeModel { } return validationResult; }, - actionConnectorFields: ExampleConnectorFields, - actionParamsFields: ExampleParamsFields, + actionConnectorFields: lazy(() => import('./example_connector_fields')), + actionParamsFields: lazy(() => import('./example_params_fields')), }; } ``` @@ -1130,6 +1130,9 @@ const ExampleConnectorFields: React.FunctionComponent ); }; + +// Export as default in order to support lazy loading +export {ExampleConnectorFields as default}; ``` 3. Define action type params fields using the property of `ActionTypeModel` `actionParamsFields`: @@ -1175,6 +1178,9 @@ const ExampleParamsFields: React.FunctionComponent ); }; + +// Export as default in order to support lazy loading +export {ExampleParamsFields as default}; ``` 4. Extend registration code with the new action type register in the file `x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts` diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 0593940a0d105d..63860e062c8da8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -3,8 +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 React from 'react'; -import { Switch, Route, Redirect, HashRouter } from 'react-router-dom'; +import React, { lazy, Suspense } from 'react'; +import { Switch, Route, Redirect, HashRouter, RouteComponentProps } from 'react-router-dom'; import { ChromeStart, DocLinksStart, @@ -15,17 +15,21 @@ import { ChromeBreadcrumb, CoreStart, } from 'kibana/public'; +import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { BASE_PATH, Section, routeToAlertDetails } from './constants'; -import { TriggersActionsUIHome } from './home'; import { AppContextProvider, useAppDependencies } from './app_context'; import { hasShowAlertsCapability } from './lib/capabilities'; import { ActionTypeModel, AlertTypeModel } from '../types'; import { TypeRegistry } from './type_registry'; -import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route'; import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerting/public'; +const TriggersActionsUIHome = lazy(async () => import('./home')); +const AlertDetailsRoute = lazy(() => + import('./sections/alert_details/components/alert_details_route') +); + export interface AppDeps { dataPlugin: DataPublicPluginStart; charts: ChartsPluginStart; @@ -62,9 +66,32 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) = const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors'; return ( - - {canShowAlerts && } + + {canShowAlerts && ( + + )} ); }; + +function suspendedRouteComponent( + RouteComponent: React.ComponentType> +) { + return (props: RouteComponentProps) => ( + + + + + + } + > + + + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx deleted file mode 100644 index dff697297f3e48..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx +++ /dev/null @@ -1,609 +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, { Fragment, useState, useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiFieldText, - EuiFlexItem, - EuiFlexGroup, - EuiFieldNumber, - EuiFieldPassword, - EuiComboBox, - EuiTextArea, - EuiButtonEmpty, - EuiSwitch, - EuiFormRow, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -import { EmailActionParams, EmailActionConnector } from './types'; -import { AddMessageVariables } from '../add_message_variables'; - -export function getActionType(): ActionTypeModel { - const mailformat = /^[^@\s]+@[^@\s]+$/; - return { - id: '.email', - iconClass: 'email', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText', - { - defaultMessage: 'Send email from your server.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle', - { - defaultMessage: 'Send to email', - } - ), - validateConnector: (action: EmailActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - from: new Array(), - port: new Array(), - host: new Array(), - user: new Array(), - password: new Array(), - }; - validationResult.errors = errors; - if (!action.config.from) { - errors.from.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText', - { - defaultMessage: 'Sender is required.', - } - ) - ); - } - if (action.config.from && !action.config.from.trim().match(mailformat)) { - errors.from.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText', - { - defaultMessage: 'Sender is not a valid email address.', - } - ) - ); - } - if (!action.config.port) { - errors.port.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', - { - defaultMessage: 'Port is required.', - } - ) - ); - } - if (!action.config.host) { - errors.host.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText', - { - defaultMessage: 'Host is required.', - } - ) - ); - } - if (action.secrets.user && !action.secrets.password) { - errors.password.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText', - { - defaultMessage: 'Password is required when username is used.', - } - ) - ); - } - if (!action.secrets.user && action.secrets.password) { - errors.user.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText', - { - defaultMessage: 'Username is required when password is used.', - } - ) - ); - } - return validationResult; - }, - validateParams: (actionParams: EmailActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - to: new Array(), - cc: new Array(), - bcc: new Array(), - message: new Array(), - subject: new Array(), - }; - validationResult.errors = errors; - if ( - (!(actionParams.to instanceof Array) || actionParams.to.length === 0) && - (!(actionParams.cc instanceof Array) || actionParams.cc.length === 0) && - (!(actionParams.bcc instanceof Array) || actionParams.bcc.length === 0) - ) { - const errorText = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText', - { - defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.', - } - ); - errors.to.push(errorText); - errors.cc.push(errorText); - errors.bcc.push(errorText); - } - if (!actionParams.message?.length) { - errors.message.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText', - { - defaultMessage: 'Message is required.', - } - ) - ); - } - if (!actionParams.subject?.length) { - errors.subject.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText', - { - defaultMessage: 'Subject is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: EmailActionConnectorFields, - actionParamsFields: EmailParamsFields, - }; -} - -const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { - const { from, host, port, secure } = action.config; - const { user, password } = action.secrets; - - return ( - - - - 0 && from !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel', - { - defaultMessage: 'Sender', - } - )} - > - 0 && from !== undefined} - name="from" - value={from || ''} - data-test-subj="emailFromInput" - onChange={e => { - editActionConfig('from', e.target.value); - }} - onBlur={() => { - if (!from) { - editActionConfig('from', ''); - } - }} - /> - - - - - - 0 && host !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel', - { - defaultMessage: 'Host', - } - )} - > - 0 && host !== undefined} - name="host" - value={host || ''} - data-test-subj="emailHostInput" - onChange={e => { - editActionConfig('host', e.target.value); - }} - onBlur={() => { - if (!host) { - editActionConfig('host', ''); - } - }} - /> - - - - - - 0 && port !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel', - { - defaultMessage: 'Port', - } - )} - > - 0 && port !== undefined} - fullWidth - name="port" - value={port || ''} - data-test-subj="emailPortInput" - onChange={e => { - editActionConfig('port', parseInt(e.target.value, 10)); - }} - onBlur={() => { - if (!port) { - editActionConfig('port', 0); - } - }} - /> - - - - - - { - editActionConfig('secure', e.target.checked); - }} - /> - - - - - - - - - 0} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel', - { - defaultMessage: 'Username', - } - )} - > - 0} - name="user" - value={user || ''} - data-test-subj="emailUserInput" - onChange={e => { - editActionSecrets('user', nullableString(e.target.value)); - }} - /> - - - - 0} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel', - { - defaultMessage: 'Password', - } - )} - > - 0} - name="password" - value={password || ''} - data-test-subj="emailPasswordInput" - onChange={e => { - editActionSecrets('password', nullableString(e.target.value)); - }} - /> - - - - - ); -}; - -const EmailParamsFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - errors, - messageVariables, - defaultMessage, -}) => { - const { to, cc, bcc, subject, message } = actionParams; - const toOptions = to ? to.map((label: string) => ({ label })) : []; - const ccOptions = cc ? cc.map((label: string) => ({ label })) : []; - const bccOptions = bcc ? bcc.map((label: string) => ({ label })) : []; - const [addCC, setAddCC] = useState(false); - const [addBCC, setAddBCC] = useState(false); - - useEffect(() => { - if (!message && defaultMessage && defaultMessage.length > 0) { - editAction('message', defaultMessage, index); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onSelectMessageVariable = (paramsProperty: string, variable: string) => { - editAction( - paramsProperty, - ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), - index - ); - }; - - return ( - - 0 && to !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientTextFieldLabel', - { - defaultMessage: 'To', - } - )} - labelAppend={ - - - {!addCC ? ( - setAddCC(true)}> - - - ) : null} - {!addBCC ? ( - setAddBCC(true)}> - - - ) : null} - - - } - > - 0 && to !== undefined} - fullWidth - data-test-subj="toEmailAddressInput" - selectedOptions={toOptions} - onCreateOption={(searchValue: string) => { - const newOptions = [...toOptions, { label: searchValue }]; - editAction( - 'to', - newOptions.map(newOption => newOption.label), - index - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editAction( - 'to', - selectedOptions.map(selectedOption => selectedOption.label), - index - ); - }} - onBlur={() => { - if (!to) { - editAction('to', [], index); - } - }} - /> - - {addCC ? ( - 0 && cc !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientCopyTextFieldLabel', - { - defaultMessage: 'Cc', - } - )} - > - 0 && cc !== undefined} - fullWidth - data-test-subj="ccEmailAddressInput" - selectedOptions={ccOptions} - onCreateOption={(searchValue: string) => { - const newOptions = [...ccOptions, { label: searchValue }]; - editAction( - 'cc', - newOptions.map(newOption => newOption.label), - index - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editAction( - 'cc', - selectedOptions.map(selectedOption => selectedOption.label), - index - ); - }} - onBlur={() => { - if (!cc) { - editAction('cc', [], index); - } - }} - /> - - ) : null} - {addBCC ? ( - 0 && bcc !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientBccTextFieldLabel', - { - defaultMessage: 'Bcc', - } - )} - > - 0 && bcc !== undefined} - fullWidth - data-test-subj="bccEmailAddressInput" - selectedOptions={bccOptions} - onCreateOption={(searchValue: string) => { - const newOptions = [...bccOptions, { label: searchValue }]; - editAction( - 'bcc', - newOptions.map(newOption => newOption.label), - index - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editAction( - 'bcc', - selectedOptions.map(selectedOption => selectedOption.label), - index - ); - }} - onBlur={() => { - if (!bcc) { - editAction('bcc', [], index); - } - }} - /> - - ) : null} - 0 && subject !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel', - { - defaultMessage: 'Subject', - } - )} - labelAppend={ - - onSelectMessageVariable('subject', variable) - } - paramsProperty="subject" - /> - } - > - 0 && subject !== undefined} - name="subject" - data-test-subj="emailSubjectInput" - value={subject || ''} - onChange={e => { - editAction('subject', e.target.value, index); - }} - onBlur={() => { - if (!subject) { - editAction('subject', '', index); - } - }} - /> - - 0 && message !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.messageTextAreaFieldLabel', - { - defaultMessage: 'Message', - } - )} - labelAppend={ - - onSelectMessageVariable('message', variable) - } - paramsProperty="message" - /> - } - > - 0 && message !== undefined} - value={message || ''} - name="message" - data-test-subj="emailMessageInput" - onChange={e => { - editAction('message', e.target.value, index); - }} - onBlur={() => { - if (!message) { - editAction('message', '', index); - } - }} - /> - - - ); -}; - -// if the string == null or is empty, return null, else return string -function nullableString(str: string | null | undefined) { - if (str == null || str.trim() === '') return null; - return str; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx similarity index 62% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index af9e34071fd095..e823e848f52c2c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -3,12 +3,10 @@ * 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, { FunctionComponent } from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { TypeRegistry } from '../../type_registry'; -import { registerBuiltInActionTypes } from './index'; -import { ActionTypeModel, ActionParamsProps } from '../../../types'; -import { EmailActionParams, EmailActionConnector } from './types'; +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '../index'; +import { ActionTypeModel } from '../../../../types'; +import { EmailActionConnector } from '../types'; const ACTION_TYPE_ID = '.email'; let actionTypeModel: ActionTypeModel; @@ -206,80 +204,3 @@ describe('action params validation', () => { }); }); }); - -describe('EmailActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: { - from: 'test@test.com', - }, - } as EmailActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - /> - ); - expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="emailFromInput"]') - .first() - .prop('value') - ).toBe('test@test.com'); - expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy(); - }); -}); - -describe('EmailParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - cc: [], - bcc: [], - to: ['test@test.com'], - subject: 'test', - message: 'test message', - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="toEmailAddressInput"]') - .first() - .prop('selectedOptions') - ).toStrictEqual([{ label: 'test@test.com' }]); - expect(wrapper.find('[data-test-subj="emailSubjectInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailMessageInput"]').length > 0).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx new file mode 100644 index 00000000000000..abb102c04b0547 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -0,0 +1,150 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { EmailActionParams, EmailActionConnector } from '../types'; + +export function getActionType(): ActionTypeModel { + const mailformat = /^[^@\s]+@[^@\s]+$/; + return { + id: '.email', + iconClass: 'email', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText', + { + defaultMessage: 'Send email from your server.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle', + { + defaultMessage: 'Send to email', + } + ), + validateConnector: (action: EmailActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + from: new Array(), + port: new Array(), + host: new Array(), + user: new Array(), + password: new Array(), + }; + validationResult.errors = errors; + if (!action.config.from) { + errors.from.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText', + { + defaultMessage: 'Sender is required.', + } + ) + ); + } + if (action.config.from && !action.config.from.trim().match(mailformat)) { + errors.from.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText', + { + defaultMessage: 'Sender is not a valid email address.', + } + ) + ); + } + if (!action.config.port) { + errors.port.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', + { + defaultMessage: 'Port is required.', + } + ) + ); + } + if (!action.config.host) { + errors.host.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText', + { + defaultMessage: 'Host is required.', + } + ) + ); + } + if (action.secrets.user && !action.secrets.password) { + errors.password.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText', + { + defaultMessage: 'Password is required when username is used.', + } + ) + ); + } + if (!action.secrets.user && action.secrets.password) { + errors.user.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText', + { + defaultMessage: 'Username is required when password is used.', + } + ) + ); + } + return validationResult; + }, + validateParams: (actionParams: EmailActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + to: new Array(), + cc: new Array(), + bcc: new Array(), + message: new Array(), + subject: new Array(), + }; + validationResult.errors = errors; + if ( + (!(actionParams.to instanceof Array) || actionParams.to.length === 0) && + (!(actionParams.cc instanceof Array) || actionParams.cc.length === 0) && + (!(actionParams.bcc instanceof Array) || actionParams.bcc.length === 0) + ) { + const errorText = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText', + { + defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.', + } + ); + errors.to.push(errorText); + errors.cc.push(errorText); + errors.bcc.push(errorText); + } + if (!actionParams.message?.length) { + errors.message.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText', + { + defaultMessage: 'Message is required.', + } + ) + ); + } + if (!actionParams.subject?.length) { + errors.subject.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText', + { + defaultMessage: 'Subject is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./email_connector')), + actionParamsFields: lazy(() => import('./email_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx new file mode 100644 index 00000000000000..67514e815bc494 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx @@ -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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { EmailActionConnector } from '../types'; +import EmailActionConnectorFields from './email_connector'; +import { DocLinksStart } from 'kibana/public'; + +describe('EmailActionConnectorFields renders', () => { + test('all connector fields is rendered', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + /> + ); + expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="emailFromInput"]') + .first() + .prop('value') + ).toBe('test@test.com'); + expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx new file mode 100644 index 00000000000000..4ef4c8a4d8617f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -0,0 +1,209 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiFieldText, + EuiFlexItem, + EuiFlexGroup, + EuiFieldNumber, + EuiFieldPassword, + EuiSwitch, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { EmailActionConnector } from '../types'; + +export const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { + const { from, host, port, secure } = action.config; + const { user, password } = action.secrets; + + return ( + + + + 0 && from !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel', + { + defaultMessage: 'Sender', + } + )} + > + 0 && from !== undefined} + name="from" + value={from || ''} + data-test-subj="emailFromInput" + onChange={e => { + editActionConfig('from', e.target.value); + }} + onBlur={() => { + if (!from) { + editActionConfig('from', ''); + } + }} + /> + + + + + + 0 && host !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel', + { + defaultMessage: 'Host', + } + )} + > + 0 && host !== undefined} + name="host" + value={host || ''} + data-test-subj="emailHostInput" + onChange={e => { + editActionConfig('host', e.target.value); + }} + onBlur={() => { + if (!host) { + editActionConfig('host', ''); + } + }} + /> + + + + + + 0 && port !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel', + { + defaultMessage: 'Port', + } + )} + > + 0 && port !== undefined} + fullWidth + name="port" + value={port || ''} + data-test-subj="emailPortInput" + onChange={e => { + editActionConfig('port', parseInt(e.target.value, 10)); + }} + onBlur={() => { + if (!port) { + editActionConfig('port', 0); + } + }} + /> + + + + + + { + editActionConfig('secure', e.target.checked); + }} + /> + + + + + + + + + 0} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel', + { + defaultMessage: 'Username', + } + )} + > + 0} + name="user" + value={user || ''} + data-test-subj="emailUserInput" + onChange={e => { + editActionSecrets('user', nullableString(e.target.value)); + }} + /> + + + + 0} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel', + { + defaultMessage: 'Password', + } + )} + > + 0} + name="password" + value={password || ''} + data-test-subj="emailPasswordInput" + onChange={e => { + editActionSecrets('password', nullableString(e.target.value)); + }} + /> + + + + + ); +}; + +// if the string == null or is empty, return null, else return string +function nullableString(str: string | null | undefined) { + if (str == null || str.trim() === '') return null; + return str; +} + +// eslint-disable-next-line import/no-default-export +export { EmailActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx new file mode 100644 index 00000000000000..a2b5ccf988afb0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import EmailParamsFields from './email_params'; + +describe('EmailParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + cc: [], + bcc: [], + to: ['test@test.com'], + subject: 'test', + message: 'test message', + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="toEmailAddressInput"]') + .first() + .prop('selectedOptions') + ).toStrictEqual([{ label: 'test@test.com' }]); + expect(wrapper.find('[data-test-subj="emailSubjectInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailMessageInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx new file mode 100644 index 00000000000000..13e791f1069e3e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -0,0 +1,267 @@ +/* + * 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, { Fragment, useState, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFieldText, EuiComboBox, EuiTextArea, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionParamsProps } from '../../../../types'; +import { EmailActionParams } from '../types'; +import { AddMessageVariables } from '../../add_message_variables'; + +export const EmailParamsFields = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + defaultMessage, +}: ActionParamsProps) => { + const { to, cc, bcc, subject, message } = actionParams; + const toOptions = to ? to.map((label: string) => ({ label })) : []; + const ccOptions = cc ? cc.map((label: string) => ({ label })) : []; + const bccOptions = bcc ? bcc.map((label: string) => ({ label })) : []; + const [addCC, setAddCC] = useState(false); + const [addBCC, setAddBCC] = useState(false); + + useEffect(() => { + if (!message && defaultMessage && defaultMessage.length > 0) { + editAction('message', defaultMessage, index); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction( + paramsProperty, + ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), + index + ); + }; + + return ( + + 0 && to !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientTextFieldLabel', + { + defaultMessage: 'To', + } + )} + labelAppend={ + + + {!addCC ? ( + setAddCC(true)}> + + + ) : null} + {!addBCC ? ( + setAddBCC(true)}> + + + ) : null} + + + } + > + 0 && to !== undefined} + fullWidth + data-test-subj="toEmailAddressInput" + selectedOptions={toOptions} + onCreateOption={(searchValue: string) => { + const newOptions = [...toOptions, { label: searchValue }]; + editAction( + 'to', + newOptions.map(newOption => newOption.label), + index + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editAction( + 'to', + selectedOptions.map(selectedOption => selectedOption.label), + index + ); + }} + onBlur={() => { + if (!to) { + editAction('to', [], index); + } + }} + /> + + {addCC ? ( + 0 && cc !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientCopyTextFieldLabel', + { + defaultMessage: 'Cc', + } + )} + > + 0 && cc !== undefined} + fullWidth + data-test-subj="ccEmailAddressInput" + selectedOptions={ccOptions} + onCreateOption={(searchValue: string) => { + const newOptions = [...ccOptions, { label: searchValue }]; + editAction( + 'cc', + newOptions.map(newOption => newOption.label), + index + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editAction( + 'cc', + selectedOptions.map(selectedOption => selectedOption.label), + index + ); + }} + onBlur={() => { + if (!cc) { + editAction('cc', [], index); + } + }} + /> + + ) : null} + {addBCC ? ( + 0 && bcc !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientBccTextFieldLabel', + { + defaultMessage: 'Bcc', + } + )} + > + 0 && bcc !== undefined} + fullWidth + data-test-subj="bccEmailAddressInput" + selectedOptions={bccOptions} + onCreateOption={(searchValue: string) => { + const newOptions = [...bccOptions, { label: searchValue }]; + editAction( + 'bcc', + newOptions.map(newOption => newOption.label), + index + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editAction( + 'bcc', + selectedOptions.map(selectedOption => selectedOption.label), + index + ); + }} + onBlur={() => { + if (!bcc) { + editAction('bcc', [], index); + } + }} + /> + + ) : null} + 0 && subject !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel', + { + defaultMessage: 'Subject', + } + )} + labelAppend={ + + onSelectMessageVariable('subject', variable) + } + paramsProperty="subject" + /> + } + > + 0 && subject !== undefined} + name="subject" + data-test-subj="emailSubjectInput" + value={subject || ''} + onChange={e => { + editAction('subject', e.target.value, index); + }} + onBlur={() => { + if (!subject) { + editAction('subject', '', index); + } + }} + /> + + 0 && message !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.messageTextAreaFieldLabel', + { + defaultMessage: 'Message', + } + )} + labelAppend={ + + onSelectMessageVariable('message', variable) + } + paramsProperty="message" + /> + } + > + 0 && message !== undefined} + value={message || ''} + name="message" + data-test-subj="emailMessageInput" + onChange={e => { + editAction('message', e.target.value, index); + }} + onBlur={() => { + if (!message) { + editAction('message', '', index); + } + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { EmailParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/index.ts new file mode 100644 index 00000000000000..e0dd24a44aa8f3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/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 { getActionType as getEmailActionType } from './email'; 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 deleted file mode 100644 index 567e96e05881dc..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx +++ /dev/null @@ -1,253 +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, { FunctionComponent } from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { act } from 'react-dom/test-utils'; -import { TypeRegistry } from '../../type_registry'; -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(), - getIndexOptions: jest.fn(), - getIndexPatterns: jest.fn(), -})); - -const ACTION_TYPE_ID = '.index'; -let actionTypeModel: ActionTypeModel; -let deps: any; - -beforeAll(async () => { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - 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: '' }, - }; -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type .index is registered', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('indexOpen'); - }); -}); - -describe('index connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test_es_index', - refresh: false, - executionTimeField: '1', - }, - } as EsIndexActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - index: [], - }, - }); - }); -}); - -describe('index connector validation with minimal config', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test_es_index', - }, - } as EsIndexActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - index: [], - }, - }); - }); -}); - -describe('action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - documents: ['test'], - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: {}, - }); - - const emptyActionParams = {}; - - expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({ - errors: {}, - }); - }); -}); - -describe('IndexActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - - const { getIndexPatterns } = jest.requireMock('../../../common/index_controls'); - getIndexPatterns.mockResolvedValueOnce([ - { - id: 'indexPattern1', - attributes: { - title: 'indexPattern1', - }, - }, - { - id: 'indexPattern2', - attributes: { - title: 'indexPattern2', - }, - }, - ]); - const { getFields } = jest.requireMock('../../../common/index_controls'); - getFields.mockResolvedValueOnce([ - { - type: 'date', - name: 'test1', - }, - { - type: 'text', - name: 'test2', - }, - ]); - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test', - refresh: false, - executionTimeField: 'test1', - }, - } as EsIndexActionConnector; - const wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - docLinks: deps!.docLinks, - }} - > - {}} - editActionSecrets={() => {}} - /> - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); - - const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); - expect(indexSearchBoxValue.first().props().value).toEqual(''); - - const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); - indexComboBox.first().simulate('click'); - const event = { target: { value: 'indexPattern1' } }; - indexComboBox - .find('input') - .first() - .simulate('change', event); - - const indexSearchBoxValueBeforeEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); - - const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); - indexComboBoxClear.first().simulate('click'); - - const indexSearchBoxValueAfterEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); - }); -}); - -describe('IndexParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - documents: [{ test: 123 }], - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect( - wrapper - .find('[data-test-subj="actionIndexDoc"]') - .first() - .prop('value') - ).toBe(`{ - "test": 123 -}`); - expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx new file mode 100644 index 00000000000000..417a9e09086a2c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '../index'; +import { ActionTypeModel } from '../../../../types'; +import { EsIndexActionConnector } from '../types'; + +const ACTION_TYPE_ID = '.index'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type .index is registered', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('indexOpen'); + }); +}); + +describe('index connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: 'test_es_index', + refresh: false, + executionTimeField: '1', + }, + } as EsIndexActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + index: [], + }, + }); + }); +}); + +describe('index connector validation with minimal config', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: 'test_es_index', + }, + } as EsIndexActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + index: [], + }, + }); + }); +}); + +describe('action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + documents: ['test'], + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: {}, + }); + + const emptyActionParams = {}; + + expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({ + errors: {}, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx new file mode 100644 index 00000000000000..3ee663a5fc8a06 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { EsIndexActionConnector, IndexActionParams } from '../types'; + +export function getActionType(): ActionTypeModel { + return { + id: '.index', + iconClass: 'indexOpen', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText', + { + defaultMessage: 'Index data into Elasticsearch.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle', + { + defaultMessage: 'Index data', + } + ), + validateConnector: (action: EsIndexActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + index: new Array(), + }; + validationResult.errors = errors; + if (!action.config.index) { + errors.index.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText', + { + defaultMessage: 'Index is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./es_index_connector')), + actionParamsFields: lazy(() => import('./es_index_params')), + validateParams: (): ValidationResult => { + return { errors: {} }; + }, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx new file mode 100644 index 00000000000000..b0f21afeaa96c7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from 'react-dom/test-utils'; +import { EsIndexActionConnector } from '../types'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import IndexActionConnectorFields from './es_index_connector'; +import { TypeRegistry } from '../../../type_registry'; +import { DocLinksStart } from 'kibana/public'; + +jest.mock('../../../../common/index_controls', () => ({ + firstFieldOption: jest.fn(), + getFields: jest.fn(), + getIndexOptions: jest.fn(), + getIndexPatterns: jest.fn(), +})); + +describe('IndexActionConnectorFields renders', () => { + test('all connector fields is rendered', async () => { + const mocks = coreMock.createSetup(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + const deps = { + toastNotifications: mocks.notifications.toasts, + http: mocks.http, + capabilities: { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }, + actionTypeRegistry: {} as TypeRegistry, + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + + const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls'); + getIndexPatterns.mockResolvedValueOnce([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, + ]); + const { getFields } = jest.requireMock('../../../../common/index_controls'); + getFields.mockResolvedValueOnce([ + { + type: 'date', + name: 'test1', + }, + { + type: 'text', + name: 'test2', + }, + ]); + + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: 'test', + refresh: false, + executionTimeField: 'test1', + }, + } as EsIndexActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + http={deps!.http} + docLinks={deps!.docLinks} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + + const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); + expect(indexSearchBoxValue.first().props().value).toEqual(''); + + const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); + indexComboBox.first().simulate('click'); + const event = { target: { value: 'indexPattern1' } }; + indexComboBox + .find('input') + .first() + .simulate('change', event); + + const indexSearchBoxValueBeforeEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); + + const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); + indexComboBoxClear.first().simulate('click'); + + const indexSearchBoxValueAfterEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); + }); +}); 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/es_index_connector.tsx similarity index 64% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index 028638a4038930..9cd3a185453450 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/es_index_connector.tsx @@ -3,12 +3,11 @@ * 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, { Fragment, useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiFormRow, EuiSwitch, EuiSpacer, - EuiCodeEditor, EuiComboBox, EuiComboBoxOptionOption, EuiSelect, @@ -17,70 +16,19 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useXJsonMode } from '../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -import { IndexActionParams, EsIndexActionConnector } from './types'; -import { getTimeFieldOptions } from '../../../common/lib/get_time_options'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { EsIndexActionConnector } from '.././types'; +import { getTimeFieldOptions } from '../../../../common/lib/get_time_options'; import { firstFieldOption, getFields, getIndexOptions, getIndexPatterns, -} from '../../../common/index_controls'; -import { AddMessageVariables } from '../add_message_variables'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; - -export function getActionType(): ActionTypeModel { - return { - id: '.index', - iconClass: 'indexOpen', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText', - { - defaultMessage: 'Index data into Elasticsearch.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle', - { - defaultMessage: 'Index data', - } - ), - validateConnector: (action: EsIndexActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - index: new Array(), - }; - validationResult.errors = errors; - if (!action.config.index) { - errors.index.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText', - { - defaultMessage: 'Index is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: IndexActionConnectorFields, - actionParamsFields: IndexParamsFields, - validateParams: (): ValidationResult => { - return { errors: {} }; - }, - }; -} +} from '../../../../common/index_controls'; 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 @@ -273,74 +221,11 @@ const IndexActionConnectorFields: React.FunctionComponent> = ({ - actionParams, - index, - editAction, - messageVariables, -}) => { - const { documents } = actionParams; - const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode( - documents && documents.length > 0 ? documents[0] : null - ); - const onSelectMessageVariable = (variable: string) => { - const value = (xJson ?? '').concat(` {{${variable}}}`); - setXJson(value); - // Keep the documents in sync with the editor content - onDocumentsChange(convertToJson(value)); - }; - - function onDocumentsChange(updatedDocuments: string) { - try { - const documentsJSON = JSON.parse(updatedDocuments); - editAction('documents', [documentsJSON], index); - // eslint-disable-next-line no-empty - } catch (e) {} - } - return ( - - onSelectMessageVariable(variable)} - paramsProperty="documents" - /> - } - > - { - setXJson(xjson); - // Keep the documents in sync with the editor content - onDocumentsChange(convertToJson(xjson)); - }} - /> - - - ); -}; - // if the string == null or is empty, return null, else return string function nullableString(str: string | null | undefined) { if (str == null || str.trim() === '') return null; return str; } + +// eslint-disable-next-line import/no-default-export +export { IndexActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx new file mode 100644 index 00000000000000..5f05a56a228e2b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import ParamsFields from './es_index_params'; + +describe('IndexParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + documents: [{ test: 123 }], + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect( + wrapper + .find('[data-test-subj="actionIndexDoc"]') + .first() + .prop('value') + ).toBe(`{ + "test": 123 +}`); + expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx new file mode 100644 index 00000000000000..0b095cdc269847 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.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 React, { Fragment } from 'react'; +import { EuiFormRow, EuiCodeEditor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useXJsonMode } from '../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; +import { ActionParamsProps } from '../../../../types'; +import { IndexActionParams } from '.././types'; +import { AddMessageVariables } from '../../add_message_variables'; + +export const IndexParamsFields = ({ + actionParams, + index, + editAction, + messageVariables, +}: ActionParamsProps) => { + const { documents } = actionParams; + const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode( + documents && documents.length > 0 ? documents[0] : null + ); + const onSelectMessageVariable = (variable: string) => { + const value = (xJson ?? '').concat(` {{${variable}}}`); + setXJson(value); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(value)); + }; + + function onDocumentsChange(updatedDocuments: string) { + try { + const documentsJSON = JSON.parse(updatedDocuments); + editAction('documents', [documentsJSON], index); + // eslint-disable-next-line no-empty + } catch (e) {} + } + return ( + + onSelectMessageVariable(variable)} + paramsProperty="documents" + /> + } + > + { + setXJson(xjson); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(xjson)); + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { IndexParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/index.ts new file mode 100644 index 00000000000000..6a2ebd9c4bc717 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/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 { getActionType as getIndexActionType } from './es_index'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 6ffd9b2c9ffde8..8f49fa46dd54e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getActionType as getServerLogActionType } from './server_log'; -import { getActionType as getSlackActionType } from './slack'; -import { getActionType as getEmailActionType } from './email'; -import { getActionType as getIndexActionType } from './es_index'; -import { getActionType as getPagerDutyActionType } from './pagerduty'; -import { getActionType as getWebhookActionType } from './webhook'; +import { getServerLogActionType } from './server_log'; +import { getSlackActionType } from './slack'; +import { getEmailActionType } from './email'; +import { getIndexActionType } from './es_index'; +import { getPagerDutyActionType } from './pagerduty'; +import { getWebhookActionType } from './webhook'; import { TypeRegistry } from '../../type_registry'; import { ActionTypeModel } from '../../../types'; 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 deleted file mode 100644 index ae894346be59cf..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx +++ /dev/null @@ -1,231 +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, { 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 { - PagerDutyActionParams, - EventActionOptions, - SeverityActionOptions, - PagerDutyActionConnector, -} from './types'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; - -const ACTION_TYPE_ID = '.pagerduty'; -let actionTypeModel: ActionTypeModel; -let deps: any; - -beforeAll(async () => { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - 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: '' }, - }; -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type static data is as expected', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('test-file-stub'); - }); -}); - -describe('pagerduty connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: { - routingKey: 'test', - }, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - routingKey: [], - }, - }); - - delete actionConnector.config.apiUrl; - actionConnector.secrets.routingKey = 'test1'; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - routingKey: [], - }, - }); - }); - - test('connector validation fails when connector config is not valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - routingKey: ['A routing key is required.'], - }, - }); - }); -}); - -describe('pagerduty action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - eventAction: 'trigger', - dedupKey: 'test', - summary: '2323', - source: 'source', - severity: 'critical', - timestamp: new Date().toISOString(), - component: 'test', - group: 'group', - class: 'test class', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { - summary: [], - timestamp: [], - }, - }); - }); -}); - -describe('PagerDutyActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: { - routingKey: 'test', - }, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - const wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - docLinks: deps!.docLinks, - }} - > - {}} - editActionSecrets={() => {}} - /> - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="pagerdutyApiUrlInput"]') - .first() - .prop('value') - ).toBe('http:\\test'); - expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy(); - }); -}); - -describe('PagerDutyParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - eventAction: EventActionOptions.TRIGGER, - dedupKey: 'test', - summary: '2323', - source: 'source', - severity: SeverityActionOptions.CRITICAL, - timestamp: new Date().toISOString(), - component: 'test', - group: 'group', - class: 'test class', - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="severitySelect"]') - .first() - .prop('value') - ).toStrictEqual('critical'); - expect(wrapper.find('[data-test-subj="eventActionSelect"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="dedupKeyInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="timestampInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="componentInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="groupInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="sourceInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="pagerdutySummaryInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="dedupKeyAddVariableButton"]').length > 0).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/index.ts new file mode 100644 index 00000000000000..9128ec81391ab3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/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 { getActionType as getPagerDutyActionType } from './pagerduty'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx new file mode 100644 index 00000000000000..ba7eb598c120d2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { PagerDutyActionConnector } from '.././types'; + +const ACTION_TYPE_ID = '.pagerduty'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('test-file-stub'); + }); +}); + +describe('pagerduty connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http:\\test', + }, + } as PagerDutyActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + routingKey: [], + }, + }); + + delete actionConnector.config.apiUrl; + actionConnector.secrets.routingKey = 'test1'; + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + routingKey: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http:\\test', + }, + } as PagerDutyActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + routingKey: ['A routing key is required.'], + }, + }); + }); +}); + +describe('pagerduty action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + eventAction: 'trigger', + dedupKey: 'test', + summary: '2323', + source: 'source', + severity: 'critical', + timestamp: new Date().toISOString(), + component: 'test', + group: 'group', + class: 'test class', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + summary: [], + timestamp: [], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx new file mode 100644 index 00000000000000..5e29fca3971806 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { PagerDutyActionParams, PagerDutyActionConnector } from '.././types'; +import pagerDutySvg from './pagerduty.svg'; +import { hasMustacheTokens } from '../../../lib/has_mustache_tokens'; + +export function getActionType(): ActionTypeModel { + return { + id: '.pagerduty', + iconClass: pagerDutySvg, + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText', + { + defaultMessage: 'Send an event in PagerDuty.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle', + { + defaultMessage: 'Send to PagerDuty', + } + ), + validateConnector: (action: PagerDutyActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + routingKey: new Array(), + }; + validationResult.errors = errors; + if (!action.secrets.routingKey) { + errors.routingKey.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText', + { + defaultMessage: 'A routing key is required.', + } + ) + ); + } + return validationResult; + }, + validateParams: (actionParams: PagerDutyActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + summary: new Array(), + timestamp: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.summary?.length) { + errors.summary.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText', + { + defaultMessage: 'Summary is required.', + } + ) + ); + } + if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) { + if (isNaN(Date.parse(actionParams.timestamp))) { + const { nowShortFormat, nowLongFormat } = getValidTimestampExamples(); + errors.timestamp.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp', + { + defaultMessage: + 'Timestamp must be a valid date, such as {nowShortFormat} or {nowLongFormat}.', + values: { + nowShortFormat, + nowLongFormat, + }, + } + ) + ); + } + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./pagerduty_connectors')), + actionParamsFields: lazy(() => import('./pagerduty_params')), + }; +} + +function getValidTimestampExamples() { + const now = moment(); + return { + nowShortFormat: now.format('YYYY-MM-DD'), + nowLongFormat: now.format('YYYY-MM-DD h:mm:ss'), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx new file mode 100644 index 00000000000000..3f3fba1599bd2b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from 'react-dom/test-utils'; +import { PagerDutyActionConnector } from '.././types'; +import PagerDutyActionConnectorFields from './pagerduty_connectors'; +import { DocLinksStart } from 'kibana/public'; + +describe('PagerDutyActionConnectorFields renders', () => { + test('all connector fields is rendered', async () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http:\\test', + }, + } as PagerDutyActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="pagerdutyApiUrlInput"]') + .first() + .prop('value') + ).toBe('http:\\test'); + expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx new file mode 100644 index 00000000000000..48da3f1778b488 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { PagerDutyActionConnector } from '.././types'; + +const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets, docLinks }) => { + const { apiUrl } = action.config; + const { routingKey } = action.secrets; + return ( + + + ) => { + editActionConfig('apiUrl', e.target.value); + }} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + } + error={errors.routingKey} + isInvalid={errors.routingKey.length > 0 && routingKey !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel', + { + defaultMessage: 'Integration key', + } + )} + > + 0 && routingKey !== undefined} + name="routingKey" + value={routingKey || ''} + data-test-subj="pagerdutyRoutingKeyInput" + onChange={(e: React.ChangeEvent) => { + editActionSecrets('routingKey', e.target.value); + }} + onBlur={() => { + if (!routingKey) { + editActionSecrets('routingKey', ''); + } + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { PagerDutyActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx new file mode 100644 index 00000000000000..d1b32f545c335d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { EventActionOptions, SeverityActionOptions } from '.././types'; +import PagerDutyParamsFields from './pagerduty_params'; + +describe('PagerDutyParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + eventAction: EventActionOptions.TRIGGER, + dedupKey: 'test', + summary: '2323', + source: 'source', + severity: SeverityActionOptions.CRITICAL, + timestamp: new Date().toISOString(), + component: 'test', + group: 'group', + class: 'test class', + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="severitySelect"]') + .first() + .prop('value') + ).toStrictEqual('critical'); + expect(wrapper.find('[data-test-subj="eventActionSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="dedupKeyInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="timestampInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="componentInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="groupInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sourceInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="pagerdutySummaryInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="dedupKeyAddVariableButton"]').length > 0).toBeTruthy(); + }); +}); 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/pagerduty_params.tsx similarity index 66% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx index 6f30cd41590ed9..590eba5dad9361 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/pagerduty_params.tsx @@ -4,180 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiSelect, - EuiLink, -} from '@elastic/eui'; +import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import moment from 'moment'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -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 { - id: '.pagerduty', - iconClass: pagerDutySvg, - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText', - { - defaultMessage: 'Send an event in PagerDuty.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle', - { - defaultMessage: 'Send to PagerDuty', - } - ), - validateConnector: (action: PagerDutyActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - routingKey: new Array(), - }; - validationResult.errors = errors; - if (!action.secrets.routingKey) { - errors.routingKey.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText', - { - defaultMessage: 'A routing key is required.', - } - ) - ); - } - return validationResult; - }, - validateParams: (actionParams: PagerDutyActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - summary: new Array(), - timestamp: new Array(), - }; - validationResult.errors = errors; - if (!actionParams.summary?.length) { - errors.summary.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText', - { - defaultMessage: 'Summary is required.', - } - ) - ); - } - if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) { - if (isNaN(Date.parse(actionParams.timestamp))) { - const { nowShortFormat, nowLongFormat } = getValidTimestampExamples(); - errors.timestamp.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp', - { - defaultMessage: - 'Timestamp must be a valid date, such as {nowShortFormat} or {nowLongFormat}.', - values: { - nowShortFormat, - nowLongFormat, - }, - } - ) - ); - } - } - return validationResult; - }, - actionConnectorFields: PagerDutyActionConnectorFields, - actionParamsFields: PagerDutyParamsFields, - }; -} - -const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets }) => { - const { docLinks } = useActionsConnectorsContext(); - const { apiUrl } = action.config; - const { routingKey } = action.secrets; - return ( - - - ) => { - editActionConfig('apiUrl', e.target.value); - }} - onBlur={() => { - if (!apiUrl) { - editActionConfig('apiUrl', ''); - } - }} - /> - - - - - } - error={errors.routingKey} - isInvalid={errors.routingKey.length > 0 && routingKey !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel', - { - defaultMessage: 'Integration key', - } - )} - > - 0 && routingKey !== undefined} - name="routingKey" - value={routingKey || ''} - data-test-subj="pagerdutyRoutingKeyInput" - onChange={(e: React.ChangeEvent) => { - editActionSecrets('routingKey', e.target.value); - }} - onBlur={() => { - if (!routingKey) { - editActionSecrets('routingKey', ''); - } - }} - /> - - - ); -}; +import { ActionParamsProps } from '../../../../types'; +import { PagerDutyActionParams } from '.././types'; +import { AddMessageVariables } from '../../add_message_variables'; const PagerDutyParamsFields: React.FunctionComponent> = ({ actionParams, @@ -563,10 +394,5 @@ const PagerDutyParamsFields: React.FunctionComponent { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - if (getResult !== null) { - actionTypeModel = getResult; - } -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type static data is as expected', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('logsApp'); - }); -}); - -describe('server-log connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.server-log', - name: 'server-log', - config: {}, - } as ActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: {}, - }); - }); -}); - -describe('action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - message: 'test message', - level: 'trace', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { message: [] }, - }); - }); -}); - -describe('ServerLogParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - level: ServerLogLevelOptions.TRACE, - message: 'test', - }; - const wrapper = mountWithIntl( - {}} - index={0} - defaultMessage={'test default message'} - /> - ); - expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="loggingLevelSelect"]') - .first() - .prop('value') - ).toStrictEqual('trace'); - expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); - }); - - test('level param field is rendered with default value if not selected', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - message: 'test message', - level: ServerLogLevelOptions.INFO, - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="loggingLevelSelect"]') - .first() - .prop('value') - ).toStrictEqual('info'); - expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); - }); - - test('params validation fails when message is not valid', () => { - const actionParams = { - message: '', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: ['Message is required.'], - }, - }); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/index.ts new file mode 100644 index 00000000000000..f85c7460d2ece1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/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 { getActionType as getServerLogActionType } from './server_log'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx new file mode 100644 index 00000000000000..3bb5ea68a3040a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel, ActionConnector } from '../../../../types'; + +const ACTION_TYPE_ID = '.server-log'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('logsApp'); + }); +}); + +describe('server-log connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.server-log', + name: 'server-log', + config: {}, + } as ActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: {}, + }); + }); +}); + +describe('action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + message: 'test message', + level: 'trace', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { message: [] }, + }); + }); + + test('params validation fails when message is not valid', () => { + const actionParams = { + message: '', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + message: ['Message is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx new file mode 100644 index 00000000000000..390ccf6a494e90 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { ServerLogActionParams } from '../types'; + +export function getActionType(): ActionTypeModel { + return { + id: '.server-log', + iconClass: 'logsApp', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText', + { + defaultMessage: 'Add a message to a Kibana log.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle', + { + defaultMessage: 'Send to Server log', + } + ), + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (actionParams: ServerLogActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + message: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.message?.length) { + errors.message.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText', + { + defaultMessage: 'Message is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: lazy(() => import('./server_log_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx new file mode 100644 index 00000000000000..d2e1d1e4500bc6 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ServerLogLevelOptions } from '.././types'; +import ServerLogParamsFields from './server_log_params'; + +describe('ServerLogParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + level: ServerLogLevelOptions.TRACE, + message: 'test', + }; + const wrapper = mountWithIntl( + {}} + index={0} + defaultMessage={'test default message'} + /> + ); + expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="loggingLevelSelect"]') + .first() + .prop('value') + ).toStrictEqual('trace'); + expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); + }); + + test('level param field is rendered with default value if not selected', () => { + const actionParams = { + message: 'test message', + level: ServerLogLevelOptions.INFO, + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="loggingLevelSelect"]') + .first() + .prop('value') + ).toStrictEqual('info'); + expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx similarity index 67% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx index a4c83ce76f04e8..64d39e238be760 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx @@ -6,51 +6,9 @@ import React, { Fragment, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelect, EuiTextArea, EuiFormRow } from '@elastic/eui'; -import { ActionTypeModel, ValidationResult, ActionParamsProps } from '../../../types'; -import { ServerLogActionParams } from './types'; -import { AddMessageVariables } from '../add_message_variables'; - -export function getActionType(): ActionTypeModel { - return { - id: '.server-log', - iconClass: 'logsApp', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText', - { - defaultMessage: 'Add a message to a Kibana log.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle', - { - defaultMessage: 'Send to Server log', - } - ), - validateConnector: (): ValidationResult => { - return { errors: {} }; - }, - validateParams: (actionParams: ServerLogActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - message: new Array(), - }; - validationResult.errors = errors; - if (!actionParams.message?.length) { - errors.message.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText', - { - defaultMessage: 'Message is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: null, - actionParamsFields: ServerLogParamsFields, - }; -} +import { ActionParamsProps } from '../../../../types'; +import { ServerLogActionParams } from '.././types'; +import { AddMessageVariables } from '../../add_message_variables'; export const ServerLogParamsFields: React.FunctionComponent ); }; + +// eslint-disable-next-line import/no-default-export +export { ServerLogParamsFields as default }; 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 deleted file mode 100644 index 0c9204ae5e1769..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx +++ /dev/null @@ -1,197 +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, { 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; - -let deps: any; - -beforeAll(async () => { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - 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: '' }, - }; -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type static data is as expected', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('logoSlack'); - }); -}); - -describe('slack connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: { - webhookUrl: 'http:\\test', - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: {}, - } as SlackActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - webhookUrl: [], - }, - }); - }); - - test('connector validation fails when connector config is not valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: {}, - } as SlackActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - webhookUrl: ['Webhook URL is required.'], - }, - }); - }); -}); - -describe('slack action params validation', () => { - test('if action params validation succeeds when action params is valid', () => { - const actionParams = { - message: 'message {test}', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { message: [] }, - }); - }); -}); - -describe('SlackActionFields renders', () => { - test('all connector fields is rendered', async () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: { - webhookUrl: 'http:\\test', - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: {}, - } as SlackActionConnector; - const wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - docLinks: deps!.docLinks, - }} - > - {}} - editActionSecrets={() => {}} - /> - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="slackWebhookUrlInput"]') - .first() - .prop('value') - ).toBe('http:\\test'); - }); -}); - -describe('SlackParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - message: 'test message', - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="slackMessageTextArea"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="slackMessageTextArea"]') - .first() - .prop('value') - ).toStrictEqual('test message'); - }); - - test('params validation fails when message is not valid', () => { - const actionParams = { - message: '', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: ['Message is required.'], - }, - }); - }); -}); 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 deleted file mode 100644 index 1cdde6dd779754..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx +++ /dev/null @@ -1,190 +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, { Fragment, useEffect } from 'react'; -import { EuiFieldText, EuiTextArea, EuiFormRow, EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} 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 { - id: '.slack', - iconClass: 'logoSlack', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText', - { - defaultMessage: 'Send a message to a Slack channel or user.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle', - { - defaultMessage: 'Send to Slack', - } - ), - validateConnector: (action: SlackActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - webhookUrl: new Array(), - }; - validationResult.errors = errors; - if (!action.secrets.webhookUrl) { - errors.webhookUrl.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText', - { - defaultMessage: 'Webhook URL is required.', - } - ) - ); - } - return validationResult; - }, - validateParams: (actionParams: SlackActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - message: new Array(), - }; - validationResult.errors = errors; - if (!actionParams.message?.length) { - errors.message.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText', - { - defaultMessage: 'Message is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: SlackActionFields, - actionParamsFields: SlackParamsFields, - }; -} - -const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors }) => { - const { docLinks } = useActionsConnectorsContext(); - const { webhookUrl } = action.secrets; - - return ( - - - - - } - error={errors.webhookUrl} - isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel', - { - defaultMessage: 'Webhook URL', - } - )} - > - 0 && webhookUrl !== undefined} - name="webhookUrl" - placeholder="Example: https://hooks.slack.com/services" - value={webhookUrl || ''} - data-test-subj="slackWebhookUrlInput" - onChange={e => { - editActionSecrets('webhookUrl', e.target.value); - }} - onBlur={() => { - if (!webhookUrl) { - editActionSecrets('webhookUrl', ''); - } - }} - /> - - - ); -}; - -const SlackParamsFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - errors, - messageVariables, - defaultMessage, -}) => { - const { message } = actionParams; - useEffect(() => { - if (!message && defaultMessage && defaultMessage.length > 0) { - editAction('message', defaultMessage, index); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onSelectMessageVariable = (paramsProperty: string, variable: string) => { - editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); - }; - - return ( - - 0 && message !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel', - { - defaultMessage: 'Message', - } - )} - labelAppend={ - - onSelectMessageVariable('message', variable) - } - paramsProperty="message" - /> - } - > - 0 && message !== undefined} - name="message" - value={message || ''} - data-test-subj="slackMessageTextArea" - onChange={e => { - editAction('message', e.target.value, index); - }} - onBlur={() => { - if (!message) { - editAction('message', '', index); - } - }} - /> - - - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/index.ts new file mode 100644 index 00000000000000..64ab6670754c92 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/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 { getActionType as getSlackActionType } from './slack'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx new file mode 100644 index 00000000000000..78f4161cac8273 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { SlackActionConnector } from '../types'; + +const ACTION_TYPE_ID = '.slack'; +let actionTypeModel: ActionTypeModel; + +beforeAll(async () => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('logoSlack'); + }); +}); + +describe('slack connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + webhookUrl: 'http:\\test', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: {}, + } as SlackActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + webhookUrl: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: {}, + } as SlackActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + webhookUrl: ['Webhook URL is required.'], + }, + }); + }); +}); + +describe('slack action params validation', () => { + test('if action params validation succeeds when action params is valid', () => { + const actionParams = { + message: 'message {test}', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { message: [] }, + }); + }); + + test('params validation fails when message is not valid', () => { + const actionParams = { + message: '', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + message: ['Message is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx new file mode 100644 index 00000000000000..5d39cdb5ac387b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { SlackActionParams, SlackActionConnector } from '../types'; + +export function getActionType(): ActionTypeModel { + return { + id: '.slack', + iconClass: 'logoSlack', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText', + { + defaultMessage: 'Send a message to a Slack channel or user.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle', + { + defaultMessage: 'Send to Slack', + } + ), + validateConnector: (action: SlackActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + webhookUrl: new Array(), + }; + validationResult.errors = errors; + if (!action.secrets.webhookUrl) { + errors.webhookUrl.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText', + { + defaultMessage: 'Webhook URL is required.', + } + ) + ); + } + return validationResult; + }, + validateParams: (actionParams: SlackActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + message: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.message?.length) { + errors.message.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText', + { + defaultMessage: 'Message is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./slack_connectors')), + actionParamsFields: lazy(() => import('./slack_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx new file mode 100644 index 00000000000000..7d7f6fc0869283 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from '@testing-library/react'; +import { SlackActionConnector } from '../types'; +import SlackActionFields from './slack_connectors'; +import { DocLinksStart } from 'kibana/public'; + +describe('SlackActionFields renders', () => { + test('all connector fields is rendered', async () => { + const actionConnector = { + secrets: { + webhookUrl: 'http:\\test', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: {}, + } as SlackActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="slackWebhookUrlInput"]') + .first() + .prop('value') + ).toBe('http:\\test'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx new file mode 100644 index 00000000000000..ad3e76ad8ae6cb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -0,0 +1,65 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { SlackActionConnector } from '../types'; + +const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => { + const { webhookUrl } = action.secrets; + + return ( + + + + + } + error={errors.webhookUrl} + isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel', + { + defaultMessage: 'Webhook URL', + } + )} + > + 0 && webhookUrl !== undefined} + name="webhookUrl" + placeholder="Example: https://hooks.slack.com/services" + value={webhookUrl || ''} + data-test-subj="slackWebhookUrlInput" + onChange={e => { + editActionSecrets('webhookUrl', e.target.value); + }} + onBlur={() => { + if (!webhookUrl) { + editActionSecrets('webhookUrl', ''); + } + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SlackActionFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx new file mode 100644 index 00000000000000..4183aeb48dec76 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import SlackParamsFields from './slack_params'; + +describe('SlackParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + message: 'test message', + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="slackMessageTextArea"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="slackMessageTextArea"]') + .first() + .prop('value') + ).toStrictEqual('test message'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx new file mode 100644 index 00000000000000..42fefdd41ef673 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment, useEffect } from 'react'; +import { EuiTextArea, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionParamsProps } from '../../../../types'; +import { SlackActionParams } from '../types'; +import { AddMessageVariables } from '../../add_message_variables'; + +const SlackParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + defaultMessage, +}) => { + const { message } = actionParams; + useEffect(() => { + if (!message && defaultMessage && defaultMessage.length > 0) { + editAction('message', defaultMessage, index); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); + }; + + return ( + + 0 && message !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel', + { + defaultMessage: 'Message', + } + )} + labelAppend={ + + onSelectMessageVariable('message', variable) + } + paramsProperty="message" + /> + } + > + 0 && message !== undefined} + name="message" + value={message || ''} + data-test-subj="slackMessageTextArea" + onChange={e => { + editAction('message', e.target.value, index); + }} + onBlur={() => { + if (!message) { + editAction('message', '', index); + } + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SlackParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx deleted file mode 100644 index 7d0082708075ff..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx +++ /dev/null @@ -1,179 +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, { FunctionComponent } from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { TypeRegistry } from '../../type_registry'; -import { registerBuiltInActionTypes } from './index'; -import { ActionTypeModel, ActionParamsProps } from '../../../types'; -import { WebhookActionParams, WebhookActionConnector } from './types'; - -const ACTION_TYPE_ID = '.webhook'; -let actionTypeModel: ActionTypeModel; - -beforeAll(() => { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - if (getResult !== null) { - actionTypeModel = getResult; - } -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type static data is as expected', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('logoWebhook'); - }); -}); - -describe('webhook connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.webhook', - name: 'webhook', - isPreconfigured: false, - config: { - method: 'PUT', - url: 'http:\\test', - headers: { 'content-type': 'text' }, - }, - } as WebhookActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - url: [], - method: [], - user: [], - password: [], - }, - }); - }); - - test('connector validation fails when connector config is not valid', () => { - const actionConnector = { - secrets: { - user: 'user', - }, - id: 'test', - actionTypeId: '.webhook', - name: 'webhook', - config: { - method: 'PUT', - }, - } as WebhookActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - url: ['URL is required.'], - method: [], - user: [], - password: ['Password is required.'], - }, - }); - }); -}); - -describe('webhook action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - body: 'message {test}', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { body: [] }, - }); - }); -}); - -describe('WebhookActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.webhook', - isPreconfigured: false, - name: 'webhook', - config: { - method: 'PUT', - url: 'http:\\test', - headers: { 'content-type': 'text' }, - }, - } as WebhookActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - /> - ); - expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); - wrapper - .find('[data-test-subj="webhookViewHeadersSwitch"]') - .first() - .simulate('click'); - expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); - }); -}); - -describe('WebhookParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - body: 'test message', - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="webhookBodyEditor"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="webhookBodyEditor"]') - .first() - .prop('value') - ).toStrictEqual('test message'); - expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); - }); - - test('params validation fails when body is not valid', () => { - const actionParams = { - body: '', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { - body: ['Body is required.'], - }, - }); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/index.ts new file mode 100644 index 00000000000000..c43cab26b072e9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/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 { getActionType as getWebhookActionType } from './webhook'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx new file mode 100644 index 00000000000000..3413465d70d935 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { WebhookActionConnector } from '../types'; + +const ACTION_TYPE_ID = '.webhook'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('logoWebhook'); + }); +}); + +describe('webhook connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + isPreconfigured: false, + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + }, + } as WebhookActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + url: [], + method: [], + user: [], + password: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = { + secrets: { + user: 'user', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + }, + } as WebhookActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + url: ['URL is required.'], + method: [], + user: [], + password: ['Password is required.'], + }, + }); + }); +}); + +describe('webhook action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + body: 'message {test}', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { body: [] }, + }); + }); + + test('params validation fails when body is not valid', () => { + const actionParams = { + body: '', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + body: ['Body is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx new file mode 100644 index 00000000000000..9f33e4491233a7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { WebhookActionParams, WebhookActionConnector } from '../types'; + +export function getActionType(): ActionTypeModel { + return { + id: '.webhook', + iconClass: 'logoWebhook', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText', + { + defaultMessage: 'Send a request to a web service.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle', + { + defaultMessage: 'Webhook data', + } + ), + validateConnector: (action: WebhookActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + url: new Array(), + method: new Array(), + user: new Array(), + password: new Array(), + }; + validationResult.errors = errors; + if (!action.config.url) { + errors.url.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText', + { + defaultMessage: 'URL is required.', + } + ) + ); + } + if (!action.config.method) { + errors.method.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText', + { + defaultMessage: 'Method is required.', + } + ) + ); + } + if (!action.secrets.user && action.secrets.password) { + errors.user.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHostText', + { + defaultMessage: 'Username is required.', + } + ) + ); + } + if (!action.secrets.password && action.secrets.user) { + errors.password.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', + { + defaultMessage: 'Password is required.', + } + ) + ); + } + return validationResult; + }, + validateParams: (actionParams: WebhookActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + body: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.body?.length) { + errors.body.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText', + { + defaultMessage: 'Body is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./webhook_connectors')), + actionParamsFields: lazy(() => import('./webhook_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx new file mode 100644 index 00000000000000..842ec517853551 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { WebhookActionConnector } from '../types'; +import WebhookActionConnectorFields from './webhook_connectors'; +import { DocLinksStart } from 'kibana/public'; + +describe('WebhookActionConnectorFields renders', () => { + test('all connector fields is rendered', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + name: 'webhook', + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + }, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + /> + ); + expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + wrapper + .find('[data-test-subj="webhookViewHeadersSwitch"]') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx similarity index 71% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index daa5a6caeabe9c..e163463602d9f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -19,112 +19,15 @@ import { EuiDescriptionListDescription, EuiDescriptionListTitle, EuiTitle, - EuiCodeEditor, EuiSwitch, EuiButtonEmpty, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -import { WebhookActionParams, WebhookActionConnector } from './types'; -import { AddMessageVariables } from '../add_message_variables'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { WebhookActionConnector } from '../types'; const HTTP_VERBS = ['post', 'put']; -export function getActionType(): ActionTypeModel { - return { - id: '.webhook', - iconClass: 'logoWebhook', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText', - { - defaultMessage: 'Send a request to a web service.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle', - { - defaultMessage: 'Webhook data', - } - ), - validateConnector: (action: WebhookActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - url: new Array(), - method: new Array(), - user: new Array(), - password: new Array(), - }; - validationResult.errors = errors; - if (!action.config.url) { - errors.url.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText', - { - defaultMessage: 'URL is required.', - } - ) - ); - } - if (!action.config.method) { - errors.method.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText', - { - defaultMessage: 'Method is required.', - } - ) - ); - } - if (!action.secrets.user && action.secrets.password) { - errors.user.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHostText', - { - defaultMessage: 'Username is required.', - } - ) - ); - } - if (!action.secrets.password && action.secrets.user) { - errors.password.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', - { - defaultMessage: 'Password is required.', - } - ) - ); - } - return validationResult; - }, - validateParams: (actionParams: WebhookActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - body: new Array(), - }; - validationResult.errors = errors; - if (!actionParams.body?.length) { - errors.body.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText', - { - defaultMessage: 'Body is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: WebhookActionConnectorFields, - actionParamsFields: WebhookParamsFields, - }; -} - const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { @@ -457,56 +360,5 @@ const WebhookActionConnectorFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - messageVariables, - errors, -}) => { - const { body } = actionParams; - const onSelectMessageVariable = (paramsProperty: string, variable: string) => { - editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index); - }; - return ( - - 0 && body !== undefined} - fullWidth - error={errors.body} - labelAppend={ - onSelectMessageVariable('body', variable)} - paramsProperty="body" - /> - } - > - { - editAction('body', json, index); - }} - /> - - - ); -}; +// eslint-disable-next-line import/no-default-export +export { WebhookActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx new file mode 100644 index 00000000000000..5ca27a53083f95 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import WebhookParamsFields from './webhook_params'; + +describe('WebhookParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + body: 'test message', + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="webhookBodyEditor"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="webhookBodyEditor"]') + .first() + .prop('value') + ).toStrictEqual('test message'); + expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx new file mode 100644 index 00000000000000..9e802b96e16be3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { EuiFormRow, EuiCodeEditor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionParamsProps } from '../../../../types'; +import { WebhookActionParams } from '../types'; +import { AddMessageVariables } from '../../add_message_variables'; + +const WebhookParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + messageVariables, + errors, +}) => { + const { body } = actionParams; + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index); + }; + return ( + + 0 && body !== undefined} + fullWidth + error={errors.body} + labelAppend={ + onSelectMessageVariable('body', variable)} + paramsProperty="body" + /> + } + > + { + editAction('body', json, index); + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { WebhookParamsFields as default }; 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 09547f5c8ea667..95620a5be84745 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/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 4d0a9980f2231f..b5f3b63c58a93a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -167,3 +167,6 @@ export const TriggersActionsUIHome: React.FunctionComponent ); }; + +// eslint-disable-next-line import/no-default-export +export { TriggersActionsUIHome as default }; 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 3b78096c4c644e..17a1d929a0def4 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 564b38bd0516a6..06ddce39567a40 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 @@ -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 React, { Fragment } from 'react'; +import React, { Fragment, Suspense } from 'react'; import { EuiForm, EuiCallOut, @@ -12,12 +12,16 @@ import { EuiSpacer, EuiFieldText, EuiFormRow, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, } 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 +50,9 @@ interface ActionConnectorProps { body: { message: string; error: string }; }; errors: IErrorObject; + http: HttpSetup; + actionTypeRegistry: TypeRegistry; + docLinks: DocLinksStart; } export const ActionConnectorForm = ({ @@ -54,8 +61,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 } }); }; @@ -145,12 +154,24 @@ export const ActionConnectorForm = ({ {FieldsComponent !== null ? ( - + + + + + + } + > + + ) : null} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 6935dda358d9c7..ae179f56f0c83a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, Suspense, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -27,6 +27,7 @@ import { EuiCallOut, EuiHorizontalRule, EuiText, + EuiLoadingSpinner, } from '@elastic/eui'; import { HttpSetup, ToastsApi, ApplicationStart, DocLinksStart } from 'kibana/public'; import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api'; @@ -282,14 +283,24 @@ export const ActionForm = ({ {ParamsFieldsComponent ? ( - + + + + + + } + > + + ) : 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 80294e8b73dc88..c9844f4e10864b 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 a31336f38bdcdd..8312f2b151082f 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 b86524efe19eaa..4a0effcbd68254 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/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 3440bb28b24684..8511ab468ca802 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -127,26 +127,27 @@ export const AlertDetails: React.FunctionComponent = ({ defaultMessage="Edit" /> - - - + {editFlyoutVisible && ( + + setEditFlyoutVisibility(false)} + /> + + )} ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx index 9198607df7863e..0caa880c4df00f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx @@ -118,6 +118,6 @@ export async function getAlertData( } } -export const AlertDetailsRouteWithApi = withActionOperations( - withBulkAlertOperations(AlertDetailsRoute) -); +const AlertDetailsRouteWithApi = withActionOperations(withBulkAlertOperations(AlertDetailsRoute)); +// eslint-disable-next-line import/no-default-export +export { AlertDetailsRouteWithApi as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 722db146a54cee..4d8801d8b7484d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -131,11 +131,7 @@ describe('alert_edit', () => { capabilities: deps!.capabilities, }} > - {}} - initialAlert={alert} - /> + {}} initialAlert={alert} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 00bc9874face19..747464d2212f40 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -31,15 +31,10 @@ import { PLUGIN } from '../../constants/plugin'; interface AlertEditProps { initialAlert: Alert; - editFlyoutVisible: boolean; - setEditFlyoutVisibility: React.Dispatch>; + onClose(): void; } -export const AlertEdit = ({ - initialAlert, - editFlyoutVisible, - setEditFlyoutVisibility, -}: AlertEditProps) => { +export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); @@ -57,14 +52,10 @@ export const AlertEdit = ({ } = useAlertsContext(); const closeFlyout = useCallback(() => { - setEditFlyoutVisibility(false); + onClose(); setAlert('alert', initialAlert); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setEditFlyoutVisibility]); - - if (!editFlyoutVisible) { - return null; - } + }, [onClose]); const alertType = alertTypeRegistry.get(alert.alertTypeId); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts index 93e61cf5b4f43e..62173a6196b98e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts @@ -23,7 +23,11 @@ const getTestAlertType = (id?: string, name?: string, iconClass?: string) => { }; }; -const getTestActionType = (id?: string, iconClass?: string, selectedMessage?: string) => { +const getTestActionType = ( + id?: string, + iconClass?: string, + selectedMessage?: string +): ActionTypeModel => { return { id: id || 'my-action-type', iconClass: iconClass || 'test', diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 47cb7067296ce7..cc511434267cc2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -3,7 +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 { HttpSetup } from 'kibana/public'; +import { HttpSetup, DocLinksStart } from 'kibana/public'; +import { ComponentType } from 'react'; import { ActionGroup } from '../../alerting/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; @@ -19,14 +20,17 @@ export { ActionType }; export type ActionTypeIndex = Record; export type AlertTypeIndex = Record; -export type ActionTypeRegistryContract = PublicMethodsOf>; +export type ActionTypeRegistryContract = PublicMethodsOf< + TypeRegistry> +>; export type AlertTypeRegistryContract = PublicMethodsOf>; export interface ActionConnectorFieldsProps { action: TActionConnector; editActionConfig: (property: string, value: any) => void; editActionSecrets: (property: string, value: any) => void; - errors: { [key: string]: string[] }; + errors: IErrorObject; + docLinks: DocLinksStart; http?: HttpSetup; } @@ -34,7 +38,7 @@ export interface ActionParamsProps { actionParams: TParams; index: number; editAction: (property: string, value: any, index: number) => void; - errors: { [key: string]: string[] }; + errors: IErrorObject; messageVariables?: string[]; defaultMessage?: string; } @@ -44,15 +48,19 @@ export interface Pagination { size: number; } -export interface ActionTypeModel { +export interface ActionTypeModel { id: string; iconClass: string; selectMessage: string; actionTypeTitle?: string; validateConnector: (connector: any) => ValidationResult; validateParams: (actionParams: any) => ValidationResult; - actionConnectorFields: React.FunctionComponent | null; - actionParamsFields: any; + actionConnectorFields: React.LazyExoticComponent< + ComponentType> + > | null; + actionParamsFields: React.LazyExoticComponent< + ComponentType> + > | null; } export interface ValidationResult { diff --git a/x-pack/plugins/uptime/README.md b/x-pack/plugins/uptime/README.md index 92162341ff4266..10c1fc0edcd00d 100644 --- a/x-pack/plugins/uptime/README.md +++ b/x-pack/plugins/uptime/README.md @@ -75,3 +75,19 @@ We can run these tests like described above, but with some special config. `node scripts/functional_tests_server.js --config=test/functional_with_es_ssl/config.ts` `node scripts/functional_test_runner.js --config=test/functional_with_es_ssl/config.ts` + +#### Running accessibility tests + +We maintain a suite of Accessibility tests (you may see them referred to elsewhere as `a11y` tests). + +These tests render each of our pages and ensure that the inputs and other elements contain the +attributes necessary to ensure all users are able to make use of Kibana (for example, users relying +on screen readers). + +The commands for running these tests are very similar to the other functional tests described above. + +From the `~/x-pack` directory: + +Start the server: `node scripts/functional_tests_server --config test/accessibility/config.ts` + +Run the uptime `a11y` tests: `node scripts/functional_test_runner.js --config test/accessibility/config.ts --grep=uptime` 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 90aa692f89a42f..b3c39e5180adf3 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/plugins/uptime/common/translations.ts b/x-pack/plugins/uptime/common/translations.ts new file mode 100644 index 00000000000000..678fe7cb1f984b --- /dev/null +++ b/x-pack/plugins/uptime/common/translations.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 { i18n } from '@kbn/i18n'; + +export const VALUE_MUST_BE_GREATER_THEN_ZEO = i18n.translate( + 'xpack.uptime.settings.invalid.error', + { + defaultMessage: 'Value must be greater than 0.', + } +); diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index c64ca7c3d48432..c6a7eb261d8fd2 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -12,7 +12,6 @@ import { import { UMFrontendLibs } from '../lib/lib'; import { PLUGIN } from '../../common/constants'; import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; -import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; @@ -61,6 +60,10 @@ export class UptimePlugin implements Plugin